# Проект
Название: Coffee CMS
Назначение: CMS с возможностью статической генерации файлов и максимальной гибкостью для доработки.

# Правила кодинга
- В PHP используем `array()` вместо `[]` для совместимости с PHP 5.2.17+.
- SQL-запросы должны быть совместимыми как с mysql так и с pgsql. Транзакции при запросах к БД не нужны.
- В try...catch заворачиваем минимум кода, чтобы при ошибке можно было сразу ее вернуть из функции.
- Делаем отступы между скобками и параметрами в скобках.
- В PHP предпочитаем использование двойных кавычек перед одинарными, даже внутри строк в двойных кавычках: `$x = "{$a["y"]} - word";`
- Избегаем использования тернарного `?` и null coalescing `??` оператора, предпочитаем усиленное сравнение `===` чтобы была понятна работа с типами. Как в PHP, так и в JS.
- В js предпочитаем двойные кавычки, селекторы в addEventListener( `.class` ) заворачиваем в обратные кавычки - это подчеркивает возможность подстановки переменных.

# Руководство по разработке Coffee CMS

## Основная архитектура
- **Парадигма**: Процедурный PHP. **НИКАКОГО ООП**. Без паттерна MVC.
- **Основные файлы**: `.cms/index.php` и `.cms/index.fn.php`. Все остальные функции являются модулями.
- **Состояние**: Глобальный массив `$cms` управляет конфигурацией, хуками и подключением к БД (`$cms["pdo"]`).
- **Конфигурация**: Хранится в `.cms/config.php`. Используйте `cms_save_config()` для сохранения изменений.

## Работа ядра (`.cms/index.php`)

Ядро выполняет следующую последовательность при каждом запросе:

1. **Инициализация**: Отключение вывода ошибок, задание версии ядра (`$cms["kernel_version"]`).
2. **Пути**: Определение абсолютных путей `$cms["cms_dir"]` (`.cms/`) и `$cms["site_dir"]` (корень сайта).
3. **URL**: Вычисление протокола, хоста, базового пути (`$cms["base_path"]` — относительный путь от `DOCUMENT_ROOT` до корня сайта, например `/cms/`), очистка и парсинг URL через `parse_url()`. Из `REQUEST_URI` вырезается `base_path`, результат декодируется через `urldecode()`, затем из пути удаляется ведущий слэш. Результат: `$cms["url"]["path"]` содержит путь **без** `base_path` и без ведущего слэша (например, `blog/my-post`).
4. **Конфигурация**: Загрузка `$cms["config"]` из `.cms/config.php`, установка локали и часового пояса.
5. **Хуки**: Инициализация цепочек выполнения в `$cms["hooks"]`. Стандартные цепочки:
   - `query → template → echo → write` (обычные страницы)
   - `admin → template` (админ-панель)
   - `api` (API-запросы, возвращают JSON)
   - `cron` (задачи по расписанию)
   Инициализируется `$cms["hooks_stack"]` — массив-стек для отслеживания уже выполняемых хуков и предотвращения рекурсии.
6. **Загрузка кода**: Подключение всех `*.fn.php` (функции) и `*.mod.php` (модули) из `.cms/mod/`.
7. **Маршрутизация**: Сортировка `$cms["urls"]` по длине ключей (длинные пути приоритетнее) через `uksort()`. Перебор регулярных выражений для выбора стартового хука: каждый ключ массива — регулярное выражение, которое оборачивается функцией `preg_prepare()` (добавляет разделители `~` и экранирует служебные символы). `preg_match()` применяется к строке `base_path + url["path"]`. При совпадении `$cms["hook"]` устанавливается в значение ключа, цикл прерывается.
8. **Выполнение**: Цикл `while ( ! empty( $cms["hook"] ) )` — вызов `do_hook($cms["hook"])` и переход к следующему хуку из цепочки (`$cms["hooks"][$current]["next"]`). Внутри `do_hook()` проверяется стек `$cms["hooks_stack"]`: если текущий хук уже находится в стеке — вызов пропускается с записью в `error_log`, иначе хук пушится в стек перед выполнением и poppingается после. Это гарантирует, что рекурсивный вызов одного и того же хука (например, `template` внутри `template`) не приведёт к бесконечному циклу.
9. **Отладка**: Запись времени, памяти и `$cms["debug"]` в лог-файлы (при `$cms["config"]["debug"]`).

## Система модулей и хуки
- **Модули**: Расположены в `.cms/mod/`. Обычно это файл `{name}.mod.php`. Скрипты в `.cms/js/`, стили в `.cms/css/`.
- **Хуки**: Основной механизм расширения.
  - Регистрация: `hook_add_fn( $hook, $function, $priority = 10 )`.
  - Выполнение: `do_hook( $hook, &$params )`.
  - **ВАЖНО**: Параметры хука передаются по **ссылке**. Изменение `$params` влияет на последующие хуки.

## Общие утилиты
- **Перевод**: `__( $string, $module = "" )`. Лениво загружает `.cms/lang/{locale}/{module}` и возвращает переведённую строку. Если `$module` не указан, имя модуля берётся из файла вызывающего. Подробнее — см. раздел 7.2 "Локализация".
- **Виджеты**: Рендеринг через `widget( $name, $options )`. Поддерживает `.php` и `.html` из папки `.cms/.widgets/` (с поддержкой перевода).
- **Безопасность путей**: Используйте `site_esc_file()` для предотвращения обхода директорий.

## Модули в поставке (Core Modules)
Список модулей, входящих в стандартную поставку CMS, можно найти в файле `.cms/mod/admin.mod.php` (в функции `cms_update_create_filelist`).

**Входящие модули (прорабатываемый код, исключены файлы сторонних библиотек):**

- **core**:
  - `.cms/index.php`
  - `.cms/index.fn.php`
- `admin.mod.php`:
  - `.cms/admin.cms/html.php`
  - `.cms/mod/admin.mod.php`
  - `.cms/js/admin_fn.js`
  - `.cms/js/admin.js`
  - `.cms/css/a.mixin.less`
  - `.cms/css/admin.less`
  - `.cms/.widgets/footer_pager.html`
  - `.cms/.widgets/password_widget.php`
  - `.cms/.widgets/search_widget.html`
  - `.cms/lang/ru_RU.UTF-8/admin.mod.php`
  - `.cms/lang/en_US.UTF-8/admin.mod.php`
  - `.cms/lang/uk_UA.UTF-8/admin.mod.php`
- `base.mod.php`:
  - `.cms/css/base.less`
  - `.cms/mod/base.mod.php`
  - `.cms/js/base.js`
  - `.cms/lang/ru_RU.UTF-8/base.mod.php`
  - `.cms/lang/en_US.UTF-8/base.mod.php`
  - `.cms/lang/uk_UA.UTF-8/base.mod.php`
- `menu.mod.php`:
  - `.cms/mod/menu.mod.php`
  - `.cms/css/menu.less`
  - `.cms/js/menu.js`
  - `.cms/lang/ru_RU.UTF-8/menu.mod.php`
  - `.cms/lang/en_US.UTF-8/menu.mod.php`
  - `.cms/lang/uk_UA.UTF-8/menu.mod.php`
  - `.cms/man/ru_RU.UTF-8/menu.html`
- `pages.mod.php`:
  - `.cms/mod/pages.mod.php`
  - `.cms/js/pages.js`
  - `.cms/css/pages.less`
  - `.cms/lang/ru_RU.UTF-8/pages.mod.php`
  - `.cms/lang/en_US.UTF-8/pages.mod.php`
  - `.cms/lang/uk_UA.UTF-8/pages.mod.php`
  - `.cms/lang/translit.php`
  - `.cms/man/ru_RU.UTF-8/book.html`
  - `.cms/man/ru_RU.UTF-8/codemirror.html`
  - `.cms/man/ru_RU.UTF-8/example.html`
  - `.cms/man/ru_RU.UTF-8/hint_css.html`
  - `.cms/man/ru_RU.UTF-8/hint_html.html`
  - `.cms/man/ru_RU.UTF-8/hint_php.html`
- `sitemap.mod.php`:
  - `.cms/mod/sitemap.mod.php`
  - `.cms/css/sitemap.less`
  - `.cms/js/sitemap.js`
  - `.cms/lang/ru_RU.UTF-8/sitemap.mod.php`
  - `.cms/lang/en_US.UTF-8/sitemap.mod.php`
  - `.cms/lang/uk_UA.UTF-8/sitemap.mod.php`
- `rss.mod.php`:
  - `.cms/mod/rss.mod.php`
  - `.cms/js/rss.js`
  - `.cms/css/rss.less`
  - `.cms/lang/ru_RU.UTF-8/rss.mod.php`
  - `.cms/lang/en_US.UTF-8/rss.mod.php`
  - `.cms/lang/uk_UA.UTF-8/rss.mod.php`
- `template.mod.php`:
  - `.cms/mod/template.mod.php`
  - `.cms/css/template.less`
  - `.cms/js/template.js`
  - `.cms/js/template_less.js`
  - `.cms/lang/ru_RU.UTF-8/template.mod.php`
  - `.cms/lang/en_US.UTF-8/template.mod.php`
  - `.cms/lang/uk_UA.UTF-8/template.mod.php`
- `ai.mod.php`:
  - `.cms/mod/ai.mod.php`
  - `.cms/js/ai.js`
  - `.cms/css/ai.less`
  - `.cms/lang/ru_RU.UTF-8/ai.mod.php`
  - `.cms/lang/en_US.UTF-8/ai.mod.php`
  - `.cms/lang/uk_UA.UTF-8/ai.mod.php`
- `demo_for_ai.mod.php`:
  - `.cms/mod/demo_for_ai.mod.php`
  - `.cms/js/demo_for_ai.js`
  - `.cms/js/demo_for_ai_admin.js`
  - `.cms/css/demo_for_ai.less`
  - `.cms/lang/ru_RU.UTF-8/demo_for_ai.mod.php`
  - `.cms/man/ru_RU.UTF-8/demo_for_ai.html`
- `comments.mod.php`:
  - `.cms/mod/comments.mod.php`
  - `.cms/js/comments.js`
  - `.cms/js/comments_admin.js`
  - `.cms/css/comments_admin.less`
  - `.cms/lang/ru_RU.UTF-8/comments.mod.php`
  - `.cms/lang/en_US.UTF-8/comments.mod.php`
  - `.cms/lang/uk_UA.UTF-8/comments.mod.php`
- `ecs.mod.php`: доработка этого модуля поставлена на паузу - пока есть другие задачи
  - `.cms/mod/ecs.mod.php`
  - `.cms/css/ecs.less`
  - `.cms/js/ecs.js`
  - `.cms/lang/ru_RU.UTF-8/ecs.mod.php`

**Как определить список файлов модуля:**
Список файлов, входящих в модуль, указан в начале его основного файла (например, `{name}.mod.php`) в массиве `$cms["modules"]["{name}.mod.php"]["files"]`.

# Создание и редактирование модулей для Coffee CMS

Это справочный файл для ИИ агентов, помогающий понять архитектуру Coffee CMS, структуру модудей и способы взаимодействия с ядром.

Вы сможете создавать расширения, которые полностью соответствуют архитектуре системы (процедурный PHP, отсутствие ООП, использование глобального массива `$cms` и системы хуков).

**Алгоритм разработки модуля:**

1.  **Планирование**: Определите таблицы БД, хуки и элементы админки.
2.  **Регистрация (`01_registration`)**: Создайте `.mod.php`, опишите метаданные и **обязательно** укажите все файлы в массиве `files`.
3.  **Логика и Хуки (`02_hooks_and_logic`)**: Реализуйте бизнес-логику через подписку на события (`query`, `template`, `echo` и т.д.).
4.  **База данных (`03_database`)**: Если нужны таблицы, используйте хук `create_tables` с проверкой совместимости MySQL/PostgreSQL. Хук `create_tables` вызывается при сохранении настроек базы данных в админке, при нажатии админом кнопки "Сохранить" на странице настроек базы данных.
5.  **Маршрутизация (`04_routing`)**: Добавьте свои URL через `$cms["urls"]`.
6.  **Интерфейс админки (`05_admin_ui`)**: Создайте пункты меню и HTML-страницы настроек.
7.  **API & AJAX (`06_api_ajax`)**: Реализуйте взаимодействие с клиентом через хук `api` в PHP и функцию `api()` в JS.
8.  **Ресурсы и Перевод (`07_assets_localization`)**: Подключите CSS/JS и настройте локализацию строк через `__()`.

---

## Важные правила кодинга Coffee CMS (Reminder)
- **НИКАКОГО ООП**. Только функции и массивы.
- Всегда используйте `array()` вместо `[]` для совместимости с PHP 5.2+.
- SQL запросы должны быть универсальными (MySQL/PostgreSQL).
- Передача параметров в хуках — строго **по ссылке** (`&$params`).
- Все файлы модуля должны быть перечислены в массиве `"files"`.

## Хранение настроек модуля

Настройки модуля хранятся в глобальном массиве `$cms["config"]["{name}.mod.php"]`. Этот массив автоматически загружается из `.cms/config.php` при старте CMS.

### Паттерн работы с настройками:

1. **Установка значений по умолчанию**: При загрузке модуля проверяйте наличие ключей и устанавливайте дефолтные значения, если они отсутствуют.

```php
$defaults = array(
    "option1" => "default_value",
    "option2" => 0,
);
foreach ( $defaults as $k => $v ) {
    if ( ! isset( $cms["config"]["{name}.mod.php"][$k] ) ) {
        $cms["config"]["{name}.mod.php"][$k] = $v;
    }
}
```

2. **Чтение настроек**: Обращайтесь напрямую к массиву конфигурации.

```php
$value = $cms["config"]["{name}.mod.php"]["option1"];
```

3. **Сохранение настроек**: Используйте `cms_save_config()` для записи всего массива `$cms["config"]` в файл `.cms/config.php`. Функция выполняет атомарную запись через временный файл и переименование.

```php
$cms["config"]["{name}.mod.php"]["option1"] = $new_value;
if ( cms_save_config() ) {
    // Успех
} else {
    // Ошибка сохранения
}
```

### Важные особенности:
- Конфигурация — это PHP-файл, который возвращается через `include`. Формат: `<?php $cms["config"] = array(...);`
- Запись идет через `var_export()` с последующей инвалидацией opcache.
- При сохранении сохраняются исходные права доступа к файлу.
- Возможна рассинхронизация при параллельных запросах (см. комментарий в `cms_save_config()`).

---

# Подробное руководство

## 1. Регистрация модуля и структура файлов

Чтобы модуль был распознан ядром Coffee CMS, необходимо создать файл `.cms/mod/{name}.mod.php`.

### Основные правила:
1. **Метаданные**: Заполните массив `$cms["modules"]["{name}.mod.php"]` с полями `name`, `description` и `files`.
2. **Список файлов (`files`)**: Это критически важный параметр. В него нужно включить **все** файлы модуля (сам `.mod.php`, JS, CSS, PHP-скрипты, файлы перевода). Если файла нет в списке, он может быть удален системой при обновлении.
3. **Проверка отключения**: Всегда добавляйте проверку на `disabled` в конфиге в начале файла, чтобы не выполнять лишний код.

### Шаблон регистрации:
```php
<?php
$cms["modules"]["{name}.mod.php"] = array(
    "name"        => "{name}_module_name",
    "description" => "{name}_module_description",
    "files"       => array(
        ".cms/mod/{name}.mod.php",
        ".cms/js/{name}.js",
        ".cms/css/{name}.css",
        ".cms/lang/ru_RU.UTF-8/{name}.mod.php",
    ),
);

if ( ! empty( $cms["config"]["{name}.mod.php"]["disabled"] ) ) {
    return;
}
```

Имя и описание модуля проходят через функцию переводов `__( $cms["modules"]["{$name}.mod.php"]["name"], "{$name}.mod.php" )` перед отображением.

## 2. Внедрение логики через хуки

Взаимодействие с ядром происходит исключительно через систему событий (хуков).

### Ключевые концепции:
1. **Регистрация**: Используйте `hook_add_fn( $hook, $function, $priority )`. 
   - `$priority`: Чем меньше число, тем раньше выполнится функция.
2. **Параметры по ссылке**: Всегда принимайте параметры в функции через `&$params`. Это позволяет изменять данные, проходящие через цепочку (например, содержимое страницы).
3. **Основные хуки**:
   - `query`: Самый ранний этап. Подходит для сбора статистики, логирования или подготовки данных.
   - `template`: Этап сборки страницы. Здесь можно модифицировать HTML или добавлять переменные в шаблон.
   - `echo`: Этап вывода контента. Позволяет внедрять (inject) элементы прямо в HTML-код страницы.
   - `write`: Этап после завершения вывода. Используется для кеширования или записи логов на диск.

### Шаблон функции хука:
```php
function {module_name}_{hook_name}_handler( &$params ) {
    global $cms;
    // Ваша логика здесь
}

hook_add_fn( "{hook}", "{module_name}_{hook_name}_handler", 10 );
```

## 3. Работа с базой данных и миграции

Для создания таблиц при установке или обновлении модуля используйте хук `create_tables`. Хук `create_tables` вызывается при сохранении настроек базы данных в админке, при нажатии админом кнопки "Сохранить" на странице настроек базы данных.

### Основные правила:
1. **Инициализация**: Проверяйте наличие `$cms["pdo"]` перед выполнением запросов.
2. **Совместимость (MySQL vs PostgreSQL)**: Разные типы БД требуют разных определений первичных ключей и типов данных для текста. Всегда делайте проверку типа драйвера через `PDO::ATTR_DRIVER_NAME`.
3. **Обработка ошибок**: Оборачивайте выполнение SQL в `try...catch`, чтобы ошибки базы не "клали" весь процесс загрузки страницы.

### Шаблон миграции:
```php
if ( is_admin() ) {
    hook_add_fn( "create_tables", "my_module_init_db" );
}

function my_module_init_db() {
    global $cms;
    if ( empty( $cms["pdo"] ) ) return;

    $type = $cms["pdo"]->getAttribute( PDO::ATTR_DRIVER_NAME );
    
    // Определяем специфичные для БД части SQL
    if ( $type === "mysql" ) {
        $id_col = "id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY";
        $time_col = "visited_at DATETIME NOT NULL";
        $text_col = "text LONGTEXT DEFAULT NULL";
    } elseif ( $type === "pgsql" ) {
        $id_col = "id BIGSERIAL PRIMARY KEY";
        $time_col = "visited_at TIMESTAMP NOT NULL";
        $text_col = "text TEXT DEFAULT NULL";
    } else {
        return; // Не поддерживается
    }

    $sql = "CREATE TABLE IF NOT EXISTS my_table (
        {$id_col},
        {$time_col},
        name VARCHAR(255) NOT NULL,
        {$text_col}
    );";

    try {
        $cms["pdo"]->exec( $sql );
    } catch ( PDOException $e ) {
        error_log( "DB Error: " . $e->getMessage() );
    }
}
```

## 4. Маршрутизация (Routing)

Если модулю нужны собственные URL, они регистрируются в глобальном массиве `$cms["urls"]`.

### Правила:
1. **Регулярные выражения**: Ключом массива является регулярное выражение для сопоставления пути.
2. **Привязка к хуку**: Значением является имя функции (хука), которая должна быть вызвана при совпадении URL.
3. **Base Path**: Помните, что роутинг работает относительно `base_path`. Регулярное выражение должно учитывать это или ожидать путь без него (в зависимости от того, как вызывается `preg_match` в ядре). В Coffee CMS `$cms["url"]["path"]` уже очищен.

### Шаблон регистрации маршрута:
```php
// При создании пути используйте символ ^ для начала строки и $ для конца
$cms["urls"]["^/my-custom-path$"] = "my_module_custom_hook";
hook_add_fn( "my_module_custom_hook", "my_module_custom_handler" );
```

## 5. Интеграция в админ-панель

### Как работает админ-панель (жизненный цикл)

Понимание цикла необходимо, чтобы избежать ошибок с `echo`, неправильной проверкой URL и отсутствием сохранения меню.

1. **Загрузка модулей**: При каждом запросе подключаются все `*.mod.php`. Модуль регистрирует хуки (`admin`, `admin_header`, `api`) и устанавливает значения по умолчанию в `$cms["config"]`.
2. **Хук `admin`**: Выполняется цепочка `admin → template`. Функции хука `admin` заполняют массив `$cms["admin_pages"]` — ключ это якорь (anchor), значение — HTML-страница.
3. **Рендеринг шаблона**: Шаблон `admin.cms/html.php`:
   - В `<aside>` итерирует `$cms["admin_sections"]` и выводит пункты меню как ссылки `#anchor_name`.
   - В `<main>` итерирует `$cms["admin_pages"]` и оборачивает каждую страницу в `<section id={anchor}>`.
4. **Навигация**: JavaScript (`admin.js`) определяет активную секцию по URL-фрагменту (`window.location.hash`) и показывает соответствующий `<section>`. **Фрагмент `#anchor` — это не часть пути URL**, поэтому `$cms["url"]["path"]` его не содержит.

### 1. Регистрация пункта меню

Пункт меню создаётся **лениво** — при первом обращении, с сохранением в конфиг.

**ВАЖНО**: Пункт меню должен создаваться внутри функции хука `admin`, а не при загрузке модуля. Это гарантирует, что `$cms["config"]` уже загружен и меню будет корректно сохранено.

**Поля меню:**
- `title`: Ключ перевода (без вызова `__()` — заголовок локализуется автоматически при выводе).
- `sort`: Порядок сортировки.
- `section`: Секция админки (`settings`, `content`, `help` или произвольная).

**Анкер (anchor)**: Идентификатор пункта меню используется как:
- Фрагмент URL: `/-admin#anchor_name`
- Ключ в `$cms["admin_pages"]`: `$cms["admin_pages"]["anchor_name"]`
- Ключ в `$cms["config"][$module]["menu"]`: `$cms["config"][$module]["menu"]["anchor_name"]`

### 2. Создание страницы настроек

**ВАЖНО**: Содержимое страницы **всегда** записывается в `$cms["admin_pages"]` **внутри** функции хука `admin`. **Никогда не используйте `echo`** в хуке `admin` — это выведет HTML до шаблона, в произвольное место страницы.

**ВАЖНО**: Перед записью страницы **всегда проверяйте видимость** через конфиг меню. Если не проверять — страница будет отображаться даже когда пункт скрыт.

### Полный рабочий пример (на основе guest_book.mod.php):

```php
<?php
// === РЕГИСТРАЦИЯ МОДУЛЯ ===
$cms["modules"]["my_module.mod.php"] = array(
    "name"        => "my_module_name",
    "description" => "my_module_description",
    "files"       => array(
        ".cms/mod/my_module.mod.php",
        ".cms/js/my_module.js",
        ".cms/js/my_module_admin.js",
        ".cms/css/my_module.css",
        ".cms/css/my_module_admin.css",
        ".cms/lang/ru_RU.UTF-8/my_module.mod.php",
        ".cms/lang/en_US.UTF-8/my_module.mod.php",
    ),
);

if ( ! empty( $cms["config"]["my_module.mod.php"]["disabled"] ) ) {
    return;
}

// === ХУКИ (только в админке) ===
if ( is_admin() ) {
    hook_add_fn( "create_tables", "my_module_create_tables" );
    hook_add_fn( "admin_header",  "my_module_admin_header" );
    hook_add_fn( "admin",         "my_module_admin" );
    hook_add_fn( "api",           "my_module_admin_api" );
}

// === ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ ===
if ( ! isset( $cms["config"]["my_module.mod.php"]["option1"] ) ) {
    $cms["config"]["my_module.mod.php"]["option1"] = "default";
}

// === ХУК ADMIN — заполнение admin_pages ===
function my_module_admin() {
    global $cms;

    // Проверка видимости: пункт меню не скрыт И секция не скрыта
    $conf = $cms["config"]["my_module.mod.php"]["menu"]["my_module"];
    if ( empty( $conf["hide"] ) && empty( $cms["config"]["admin_sections"][ $conf["section"] ]["hide"] ) ) {

        // Собираем переводы в переменные
        $tr_title = __( "my_module_settings" );
        $tr_save  = __( "save" );

        // Собираем HTML страницы в переменную $page
        $page = "
    <div class='panel'>
        <h3>{$tr_title}</h3>
        <form method='post' action='{$cms['base_path']}{$cms['config']['admin.mod.php']['api_url']}' class='ajax-form'>
            <input type='hidden' name='fn' value='my_module_save'>
            <label>
                <div class=title>Настройка</div>
                <input type='text' name='option1' value='{$cms["config"]["my_module.mod.php"]["option1"]}' autocomplete=off>
            </label>
            <button class='button save' type=submit><span>{$tr_save}</span></button>
        </form>
    </div>";

        // Ленивое создание пункта меню (только при первом входе)
        if ( empty( $cms["config"]["my_module.mod.php"]["menu"]["my_module"] ) ) {
            $cms["config"]["my_module.mod.php"]["menu"]["my_module"] = array(
                "title"    => "my_module_name",   // ключ перевода, не текст
                "sort"     => 20,
                "section"  => "content",
            );
            cms_save_config();
        }

        // Запись страницы в admin_pages (ключ = якорь меню)
        $cms["admin_pages"]["my_module"] = $page;
    }
}

// === ХУК API — обработка AJAX ===
function my_module_admin_api( &$params ) {
    global $cms;

    if ( ! isset( $_POST["fn"] ) ) return;

    switch ( $_POST["fn"] ) {
        case "my_module_save":
            $cms["config"]["my_module.mod.php"]["option1"] = trim( $_POST["option1"] );
            if ( cms_save_config() ) {
                echo json_encode( array( "result" => array( "notify_html" => __( "settings_saved" ) ) ) );
            } else {
                echo json_encode( array( "error" => array( "notify_html" => __( "save_error" ) ) ) );
            }
            return;
    }
}

// === ХУК ADMIN_HEADER — подключение CSS/JS ===
function my_module_admin_header() {
    global $cms;
    if ( is_admin_header_on( "my_module.mod.php" ) ) {
        echo "<link rel='stylesheet' href='{$cms['base_path']}css/my_module_admin.css'>\n";
        echo "<script src='{$cms['base_path']}js/my_module_admin.js'></script>\n";
    }
}
```

### Типичные ошибки (чего НЕ делать):

| Ошибка | Последствие | Правильно |
|--------|-------------|-----------|
| `hook_add_fn("admin", ...)` вне `if (is_admin())` | Хук вызывается на фронтенде, тратя ресурсы | Регистрировать внутри `if (is_admin())` |
| `echo $html` в функции хука `admin` | HTML выводится до шаблона, в произвольное место | `$cms["admin_pages"]["anchor"] = $page` |
| Проверка `$cms["url"]["path"] === "anchor"` | Фрагмент `#anchor` не входит в path, проверка всегда ложна | Проверять видимость через `$cms["config"][$module]["menu"]` |
| Меню без `cms_save_config()` | Пункт пропадает при следующем запросе (конфиг перезагружается из файла) | Вызывать `cms_save_config()` при первичном создании |
| `title` с вызовом `__(...)` при регистрации | Перевод фиксируется на моменте загрузки модуля, не переключается при смене языка | Использовать ключ перевода: `"title" => "my_module_name"` |

## 6. API и AJAX взаимодействие

Для динамического обновления данных в админке используется паттерн "один обработчик — много функций". Все запросы приходят на хук `api`.

### Клиентская часть (JavaScript)
Используйте встроенную функцию `api()`. Она выполняет POST-запрос и возвращает результат в callback.

```javascript
// Вызов функции сохранения
api({ 
    fn: 'save_settings', // Имя функции для обработки
    new_value: 'some_data' 
}, function(response) {
    if (response.result) {
        notify_answer({ result: { notify_html: "Успешно сохранено!" } });
    } else if (response.error) {
        notify_answer(response.error);
    }
});
```

### Серверная часть (PHP)
Подпишитесь на хук `api`, используйте `switch( $_POST["fn"] )` для распределения задач и отвечайте JSON через `echo json_encode(...)`. Каждый `case` должен завершаться `return` (или `break`, если цепочка хуков должна продолжиться).

```php
if ( is_admin() ) {
    hook_add_fn( "api", "my_module_api_handler" );
}

function my_module_api_handler( &$params ) {
    global $cms;

    if ( ! isset( $_POST["fn"] ) ) return;

    switch ( $_POST["fn"] ) {
        case "save_settings":
            $val = trim( $_POST["new_value"] );
            
            $cms["config"]["my_module.mod.php"]["api_key"] = $val;

            if ( cms_save_config() ) {
                echo json_encode( array( "result" => array( "notify_html" => "Настройки обновлены!" ) ) );
            } else {
                echo json_encode( array( "error" => array( "notify_html" => "Ошибка сохранения в файл конфигурации!" ) ) );
            }
            return;

        case "delete_item":
            // Логика удаления...
            break;
    }
    // Если функция не найдена — модуль молча пропускает запрос.
    // Не добавляйте default-ветку, чтобы не ломать обработку другими модулями.
}
```

## 7. Ресурсы и Локализация

### 1. Управление ресурсами (CSS/JS)
Все файлы должны быть зарегистрированы в массиве `files` вашего модуля. Если вы хотите, чтобы стили или скрипты подгружались только в админке, используйте проверку `is_admin_header_on()`.

```php
// В .mod.php файле
if ( is_admin() ) {
    hook_add_fn( "admin_header", "my_module_assets" );
}

function my_module_assets() {
    global $cms;
    if ( is_admin_header_on( "my_module.mod.php" ) ) {
        echo "<link rel='stylesheet' href='{$cms['base_path']}css/my_module.css'>\n";
        echo "<script src='{$cms['base_path']}js/my_module.js'></script>\n";
    }
}
```

### 2. Локализация (Перевод)

#### Как работает механизм переводов

Coffee CMS использует **ленивую загрузку** переводов через функцию `__()` из `index.fn.php`.

**Порядок работы `__( $string, $module = "" )`:**

1. **Определение модуля**: Если `$module` передан явно — используется он. Если нет — имя модуля берётся из имени файла вызывающего (через `debug_backtrace()`). Исключение: шаблон админки (`admin.cms/html.php`) автоматически маппится на `admin.mod.php`.
2. **Ленивое подключение файла**: Если `$cms["lang"][$module][$locale]` ещё не загружена, функция пытается подключить файл:
   ```
   .cms/lang/{locale}/{module}
   ```
   где `{locale}` — текущая локаль из `$cms["config"]["locale"]` (например, `ru_RU.UTF-8`), а `{module}` — имя модуля (например, `comments.mod.php`). Подключение выполняется через `include_once()`. Если файл не найден — создаётся пустой массив.
3. **Поиск перевода**: Если ключ `$string` найден в `$cms["lang"][$module][$locale]` — возвращается переведённая строка. Если не найден — возвращается оригинальная строка без ошибки.

**ВАЖНО**: Файлы переводов подключаются **автоматически** при первом вызове `__()`. Никакая предварительная регистрация или ручное подключение не требуется. Достаточно правильно назвать файл и разместить в нужной папке.

**ВАЖНО**: Не существуют переменные `$cms["lang_files"]`, `$cms["lang_modules"]` или аналоги. Единственный механизм загрузки переводов — функция `__()`. Не изобретайте собственные способы.

**Вывод переводов на JS**: При рендеринге шаблона админки (`admin.cms/html.php`) весь массив `$cms["lang"]` сериализуется в JSON и встраивается в HTML как `cms.lang`. JS-функция `_( str, module )` (определена в `admin_fn.js`) ищет перевод в `cms.lang[module][locale][str]`. Переводы для фронтенда передаются через отдельный ключ `frontend` (см. ниже).

#### Использование в PHP:
```php
echo __( "welcome_message", "my_module" ); 
```

#### Структура файлов перевода:
Файлы должны располагаться в `.cms/lang/{locale}/{module}.mod.php`.

**Пример файла `.cms/lang/ru_RU.UTF-8/my_module.mod.php`:**
```php
<?php
$cms["lang"]["my_module.mod.php"]["ru_RU.UTF-8"] = array(
    "my_module_name" => "Мой модуль",
    "my_module_description" => "Описание моего модуля",
    "welcome_message" => "Добро пожаловать в мой модуль!",
);
```

**ВАЖНО**: Ключ массива `$cms["lang"]` должен **точно совпадать** с именем модуля (то есть `{module}.mod.php`). Если ключ не совпадает с именем файла — перевод не будет найден.

#### Переводы для фронтенда (JS на стороне клиента)

Если модуль отправляет переводы в JavaScript для использования на странице сайта (не в админке), используйте вложенную структуру с ключом `frontend`:

```php
<?php
// Переводы для админки
$cms["lang"]["my_module.mod.php"]["ru_RU.UTF-8"] = array(
    "settings_saved" => "Настройки сохранены",
);

// Переводы для фронтенда (JS)
$cms["lang"]["my_module.mod.php"]["frontend"]["ru_RU.UTF-8"] = array(
    "send" => "Отправить",
    "error" => "Ошибка",
);
```

На сервере передайте переводы в JS через `json_encode`:
```php
$tr = json_encode( $cms["lang"]["my_module.mod.php"]["frontend"][ $cms["config"]["locale"] ] );
echo "<script>var translations = {$tr};</script>";
```

#### Переводы в JS-файлах модуля

Каждый JS-файл модуля, использующий переводы, **обязан** определить функцию `__()` в начале файла. Эта функция вызывает глобальную `_()` из `admin_fn.js` с именем модуля:

```javascript
// В начале файла .cms/js/my_module_admin.js
function __( str ) {
    return _( str, "my_module.mod.php" );
}

// Далее в коде используйте __():
var label = __( "save" );
var error = __( "error_message" );
```

**ВАЖНО**: Не используйте `translate()`, `i18n()`, `t()` или другие выдуманные функции. Единственный механизм — `__()` с вызовом `_()`.
