Динамические названия кнопок #9

Open
opened 2026-09-25 12:56:08 +00:00 by forgejo-admin · 7 comments

Нужно предусмотреть переименование кнопок в конфигурации фронта. То есть я хочу иметь возможность задать другие имена кнопкам. Если значения не заданы, то должны использоваться текущие по умолчанию. Здесь нужно подумать, как это лучше сделать. Через конфигурацию бекенда и чтобы он возвращал названия кнопок, либо это сделать конфигурацией фронта. Не знаю, предложи решение с плюсами и минусами каждого.

Нужно предусмотреть переименование кнопок в конфигурации фронта. То есть я хочу иметь возможность задать другие имена кнопкам. Если значения не заданы, то должны использоваться текущие по умолчанию. Здесь нужно подумать, как это лучше сделать. Через конфигурацию бекенда и чтобы он возвращал названия кнопок, либо это сделать конфигурацией фронта. Не знаю, предложи решение с плюсами и минусами каждого.
Collaborator

Беру задачу, ветка feature/9_dynamic_button_labels. Изучаю код и готовлю предложение по вариантам реализации (конфиг бекенда vs конфиг фронта) с плюсами/минусами — опубликую здесь на выбор.

Беру задачу, ветка `feature/9_dynamic_button_labels`. Изучаю код и готовлю предложение по вариантам реализации (конфиг бекенда vs конфиг фронта) с плюсами/минусами — опубликую здесь на выбор.
Collaborator

Разобрался, как сейчас устроено, и подготовил варианты.

Как сейчас. Кнопки жёстко зашиты во фронте (web/src/utils/buttons.ts): 5 кнопок (POWER, MOVE_OUT, MOVE_IN, ACK, PU), у каждой mask (протокол), name (внутренний идентификатор, на нём маска/логика), label (отображаемое имя) и key (хоткей 1–5). Конфигурация системы — единый .env рядом с бинарником бекенда (LINK_PATH, HTTP_ADDR, API_TOKEN, ...). Бекенд раздаёт собранный фронт и API GET/PUT /api/buttons.

Переименовывать нужно именно label; mask/name/key трогать нельзя — на них завязаны протокол и логика. Оба варианта ниже это учитывают; если переопределения не заданы — используются текущие дефолтные имена.


Вариант A — конфиг бекенда (рекомендую).
В .env добавляются ключи вида BUTTON_LABEL_POWER=Питание, BUTTON_LABEL_MOVE_OUT=Выезд и т.д. Бекенд отдаёт их фронту новым эндпоинтом GET /api/config в виде переопределений: {"labels":{"POWER":"Питание", ...}}. Фронт при загрузке мержит их поверх встроенной таблицы кнопок; чего нет или пусто — берётся дефолт.

Плюсы:

  • единая точка конфигурации: всё уже настраивается через .env у бинарника, новые ключи ложатся туда же естественно;
  • смена имён без пересборки фронта: один и тот же dist работает с любой конфигурацией (правка .env + рестарт бекенда);
  • минимальная связанность: бекенд не дублирует таблицу кнопок (не знает про маски/хоткеи), а отдаёт только пары «стабильный name → подпись» — риск расхождения фронт/бекенд минимален.

Минусы:

  • чуть больше работы: новый эндпоинт в Go + логика загрузки/мержа во фронте;
  • до первого ответа /api/config фронт показывает дефолтные подписи (доли секунды при загрузке; можно закрыть заглушкой до завершения загрузки).

Вариант B — конфиг фронта.

  • B1: build-time переменные Vite (VITE_BTN_POWER_LABEL=...) — имена зашиваются в бандл при сборке.
  • B2: статический config.json рядом с index.html, фронт читает его при старте.

Плюсы:

  • B1 — самый простой механизм, бекенд не меняется вовсе;
  • B2 — тоже без изменений бекенда, правится без пересборки.

Минусы:

  • B1: любое переименование = пересборка фронта; под каждую инсталляцию нужен свой билд, бинарник и dist приходится держать синхронно;
  • B2: появляется второй конфиг-файл в другом месте (внутри web/dist), который затирается при каждой пересборке/обновлении фронта — конфигурация разносится по двум точкам, при деплое легко ошибиться;
  • оба ломают сложившуюся схему «один .env рядом с бинарником».

Рекомендация: вариант A. Сохраняет единую точку конфигурации, не требует пересборки при переименовании и почти не связывает бекенд с фронтом. Если подтверждаете вариант A (или выбираете другой) — реализую в ветке feature/9_dynamic_button_labels: ключи .env + GET /api/config + мерж во фронте с фолбэком на дефолты, тесты на обе стороны (Go-хендлер и мерж во фронте).

Разобрался, как сейчас устроено, и подготовил варианты. **Как сейчас.** Кнопки жёстко зашиты во фронте (`web/src/utils/buttons.ts`): 5 кнопок (POWER, MOVE_OUT, MOVE_IN, ACK, PU), у каждой `mask` (протокол), `name` (внутренний идентификатор, на нём маска/логика), `label` (отображаемое имя) и `key` (хоткей 1–5). Конфигурация системы — единый `.env` рядом с бинарником бекенда (`LINK_PATH`, `HTTP_ADDR`, `API_TOKEN`, ...). Бекенд раздаёт собранный фронт и API `GET/PUT /api/buttons`. Переименовывать нужно именно `label`; `mask`/`name`/`key` трогать нельзя — на них завязаны протокол и логика. Оба варианта ниже это учитывают; если переопределения не заданы — используются текущие дефолтные имена. --- **Вариант A — конфиг бекенда (рекомендую).** В `.env` добавляются ключи вида `BUTTON_LABEL_POWER=Питание`, `BUTTON_LABEL_MOVE_OUT=Выезд` и т.д. Бекенд отдаёт их фронту новым эндпоинтом `GET /api/config` в виде переопределений: `{"labels":{"POWER":"Питание", ...}}`. Фронт при загрузке мержит их поверх встроенной таблицы кнопок; чего нет или пусто — берётся дефолт. Плюсы: - единая точка конфигурации: всё уже настраивается через `.env` у бинарника, новые ключи ложатся туда же естественно; - смена имён **без пересборки фронта**: один и тот же `dist` работает с любой конфигурацией (правка `.env` + рестарт бекенда); - минимальная связанность: бекенд не дублирует таблицу кнопок (не знает про маски/хоткеи), а отдаёт только пары «стабильный name → подпись» — риск расхождения фронт/бекенд минимален. Минусы: - чуть больше работы: новый эндпоинт в Go + логика загрузки/мержа во фронте; - до первого ответа `/api/config` фронт показывает дефолтные подписи (доли секунды при загрузке; можно закрыть заглушкой до завершения загрузки). --- **Вариант B — конфиг фронта.** - B1: build-time переменные Vite (`VITE_BTN_POWER_LABEL=...`) — имена зашиваются в бандл при сборке. - B2: статический `config.json` рядом с `index.html`, фронт читает его при старте. Плюсы: - B1 — самый простой механизм, бекенд не меняется вовсе; - B2 — тоже без изменений бекенда, правится без пересборки. Минусы: - B1: любое переименование = пересборка фронта; под каждую инсталляцию нужен свой билд, бинарник и `dist` приходится держать синхронно; - B2: появляется второй конфиг-файл в другом месте (внутри `web/dist`), который **затирается при каждой пересборке/обновлении фронта** — конфигурация разносится по двум точкам, при деплое легко ошибиться; - оба ломают сложившуюся схему «один `.env` рядом с бинарником». --- **Рекомендация: вариант A.** Сохраняет единую точку конфигурации, не требует пересборки при переименовании и почти не связывает бекенд с фронтом. Если подтверждаете вариант A (или выбираете другой) — реализую в ветке `feature/9_dynamic_button_labels`: ключи `.env` + `GET /api/config` + мерж во фронте с фолбэком на дефолты, тесты на обе стороны (Go-хендлер и мерж во фронте).
Author
Owner

Хорошо, давай сделаем вариант A. Как можно сделать так, чтобы например пользователь с фронта мог изменить названия кнопок и чтобы они навсегда остались, не важно перезагрузился фронт, бекенд или полностью устройство? Пока не думай о фронте (это реализуем позже), как сделать лучше всего на бекенде?

Хорошо, давай сделаем вариант A. Как можно сделать так, чтобы например пользователь с фронта мог изменить названия кнопок и чтобы они навсегда остались, не важно перезагрузился фронт, бекенд или полностью устройство? Пока не думай о фронте (это реализуем позже), как сделать лучше всего на бекенде?
Collaborator

Ответ на вопрос про хранение: названия кнопок — это пользовательские данные, а не конфигурация развёртывания. Поэтому предлагаю хранить их не в самом .env, а в отдельном JSON-файле рядом с бинарником; .env остаётся точкой админских значений по умолчанию.

Предлагаемая схема (рекомендую):

  1. Новый ключ .env: UI_CONFIG_FILE (по умолчанию — ui_config.json рядом с исполняемым файлом).
  2. Файл хранит переопределения в JSON. Запись — атомарная, tmp+rename (тот же приём, что уже применяется для BUTTON_FILE), плюс fsync перед rename — чтобы изменения переживали даже выключение питания:
{ "labels": { "POWER": "Питание", "MOVE_OUT": "Выезд" } }
  1. Приоритет источников (от низкого к высокому): встроенные дефолты фронта < BUTTON_LABEL_* из .env (админские) < ui_config.json (пользовательские). Пустая строка = явный сброс к дефолту (ключ не учитывается).
  2. API (авторизация — тот же bearer-токен, что у /api/buttons):
    • GET /api/config — отдаёт эффективные переопределения (env, слитый с файлом);
    • PUT /api/config/labels — принимает {"labels": {...}}, валидирует (только известные имена кнопок POWER/MOVE_OUT/MOVE_IN/ACK/PU, трим, ограничение длины), атомарно пишет файл и обновляет копию в памяти.
  3. При старте бекенд читает файл один раз; битый/нечитаемый JSON не роняет процесс — пишем событие в лог и откатываемся к env-дефолтам.

Почему это решает задачу «навсегда»: файл лежит на диске рядом с бинарником — перезагрузка фронта не важна (он просто заново делает GET), рестарт бекенда не важен (файл читается при старте), полное выключение устройства не важно (запись атомарная, с fsync).

Почему не перезаписывать сам .env из программы:

  • при программной записи (godotenv.Marshal) теряются комментарии и форматирование, которые админ мог вести руками;
  • значения из .env читаются один раз при старте — изменения из UI требовали бы либо рестарта, либо второй копии «истины» в памяти, разъезжающейся с файлом;
  • выше риск конфликтов: админ правит .env руками, программа одновременно — из HTTP-хендлера.

Рассмотренные альтернативы:

  • SQLite/bolt — избыточно для пяти строк: новая зависимость, усложнённый бэкап;
  • хранить только в .env без файла — не позволяет менять имена из UI без перезаписи конфига программой (см. выше).

Бонус: тот же ui_config.json дальше примет настройки из задачи #10 (тема, раскладка кнопок, флаг «Журнал») — получится единая точка пользовательских настроек с одним API /api/config.

Если подтверждаете схему — реализую в ветке feature/9_dynamic_button_labels: UI_CONFIG_FILE, загрузка/сохранение с атомарной записью, GET /api/config + PUT /api/config/labels, тесты (валидация, приоритеты источников, переживание рестарта).

**Ответ на вопрос про хранение: названия кнопок — это пользовательские данные, а не конфигурация развёртывания.** Поэтому предлагаю хранить их не в самом `.env`, а в отдельном JSON-файле рядом с бинарником; `.env` остаётся точкой админских значений по умолчанию. **Предлагаемая схема (рекомендую):** 1. Новый ключ `.env`: `UI_CONFIG_FILE` (по умолчанию — `ui_config.json` рядом с исполняемым файлом). 2. Файл хранит переопределения в JSON. Запись — атомарная, tmp+rename (тот же приём, что уже применяется для `BUTTON_FILE`), плюс fsync перед rename — чтобы изменения переживали даже выключение питания: ```json { "labels": { "POWER": "Питание", "MOVE_OUT": "Выезд" } } ``` 3. Приоритет источников (от низкого к высокому): встроенные дефолты фронта < `BUTTON_LABEL_*` из `.env` (админские) < `ui_config.json` (пользовательские). Пустая строка = явный сброс к дефолту (ключ не учитывается). 4. API (авторизация — тот же bearer-токен, что у `/api/buttons`): - `GET /api/config` — отдаёт эффективные переопределения (env, слитый с файлом); - `PUT /api/config/labels` — принимает `{"labels": {...}}`, валидирует (только известные имена кнопок POWER/MOVE_OUT/MOVE_IN/ACK/PU, трим, ограничение длины), атомарно пишет файл и обновляет копию в памяти. 5. При старте бекенд читает файл один раз; битый/нечитаемый JSON не роняет процесс — пишем событие в лог и откатываемся к env-дефолтам. **Почему это решает задачу «навсегда»:** файл лежит на диске рядом с бинарником — перезагрузка фронта не важна (он просто заново делает GET), рестарт бекенда не важен (файл читается при старте), полное выключение устройства не важно (запись атомарная, с fsync). **Почему не перезаписывать сам `.env` из программы:** - при программной записи (godotenv.Marshal) теряются комментарии и форматирование, которые админ мог вести руками; - значения из `.env` читаются один раз при старте — изменения из UI требовали бы либо рестарта, либо второй копии «истины» в памяти, разъезжающейся с файлом; - выше риск конфликтов: админ правит `.env` руками, программа одновременно — из HTTP-хендлера. **Рассмотренные альтернативы:** - SQLite/bolt — избыточно для пяти строк: новая зависимость, усложнённый бэкап; - хранить только в `.env` без файла — не позволяет менять имена из UI без перезаписи конфига программой (см. выше). **Бонус:** тот же `ui_config.json` дальше примет настройки из задачи #10 (тема, раскладка кнопок, флаг «Журнал») — получится единая точка пользовательских настроек с одним API `/api/config`. Если подтверждаете схему — реализую в ветке `feature/9_dynamic_button_labels`: `UI_CONFIG_FILE`, загрузка/сохранение с атомарной записью, `GET /api/config` + `PUT /api/config/labels`, тесты (валидация, приоритеты источников, переживание рестарта).
Author
Owner

ui_config.json - делаем. Только обязательно валидируй данные, по размеру, по формату, по безопасности и так далее. Обнови main я туда влил изменения по настройкам

ui_config.json - делаем. Только обязательно валидируй данные, по размеру, по формату, по безопасности и так далее. Обнови main я туда влил изменения по настройкам
Collaborator

Принято: схема с ui_config.json подтверждена, валидации (формат, размеры, безопасность) — обязательно. Обновляю main с вашими изменениями по настройкам и продолжаю работу в ветке feature/9_dynamic_button_labels.

Принято: схема с `ui_config.json` подтверждена, валидации (формат, размеры, безопасность) — обязательно. Обновляю main с вашими изменениями по настройкам и продолжаю работу в ветке `feature/9_dynamic_button_labels`.
Collaborator

Готово, PR #12 на ревью, ветка feature/9_dynamic_button_labels.

Реализовано по подтверждённой схеме: UI_CONFIG_FILE (ui_config.json рядом с бинарником, атомарная запись tmp+rename+fsync), BUTTON_LABEL_* в .env как админские дефолты, GET /api/config + PUT /api/config/labels (bearer-авторизация), фронт применяет переопределения при загрузке. Валидация: только известные имена кнопок, лимиты длины/количества/размера, белый список символов, битый файл не роняет процесс. Подписи — переопределяются, маски/имена/хоткеи — нет. Все тесты зелёные (Go + web), схема проверена живым прогоном: сохранение переживает рестарт процесса.

Готово, PR #12 на ревью, ветка `feature/9_dynamic_button_labels`. Реализовано по подтверждённой схеме: `UI_CONFIG_FILE` (`ui_config.json` рядом с бинарником, атомарная запись tmp+rename+fsync), `BUTTON_LABEL_*` в `.env` как админские дефолты, `GET /api/config` + `PUT /api/config/labels` (bearer-авторизация), фронт применяет переопределения при загрузке. Валидация: только известные имена кнопок, лимиты длины/количества/размера, белый список символов, битый файл не роняет процесс. Подписи — переопределяются, маски/имена/хоткеи — нет. Все тесты зелёные (Go + web), схема проверена живым прогоном: сохранение переживает рестарт процесса.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
forgejo-admin/keba-hhs#9
No description provided.