docs: plan UI/UX cleanup phase (nav, account, backup, connection info)
Fold user feedback into a new Phase 12 covering the panel's UX gaps: structural nav header on every page with active-state highlighting, an account settings page for admin login/password, a dedicated backup/migration page split from domain import, connection settings on the domain page, copy-to-clipboard for values meant to be pasted elsewhere, hiding the unused addresses field in wildcard mode, and moving the Reload button to the new /status landing page (Phase 13, renumbered from 12).
This commit is contained in:
+156
-6
@@ -27,7 +27,7 @@
|
|||||||
3. **CSRF — только `SameSite=Lax`, без токенов.** Достаточно для современных браузеров (все мутации — POST, все GET read-only), но не защищает при downgrade до старого браузера/особых прокси и не даёт защиты на уровне «per-request». **Вопрос:** считать `SameSite=Lax` достаточным для single-admin панели (моя рекомендация — да) или добавить double-submit CSRF-токен.
|
3. **CSRF — только `SameSite=Lax`, без токенов.** Достаточно для современных браузеров (все мутации — POST, все GET read-only), но не защищает при downgrade до старого браузера/особых прокси и не даёт защиты на уровне «per-request». **Вопрос:** считать `SameSite=Lax` достаточным для single-admin панели (моя рекомендация — да) или добавить double-submit CSRF-токен.
|
||||||
4. **Cookie без префикса `__Host-`.** Сейчас `selfpost_session` (`Secure`/`HttpOnly`/`SameSite=Lax`/`Path=/`). Префикс `__Host-` дал бы браузерный гарант «только HTTPS, только этот хост, без Domain». Мелочь, но бесплатная. **Вопрос:** переименовать (учесть dev-режим `PANEL_COOKIE_SECURE=false` — `__Host-` требует `Secure`, т.е. только когда secure включён).
|
4. **Cookie без префикса `__Host-`.** Сейчас `selfpost_session` (`Secure`/`HttpOnly`/`SameSite=Lax`/`Path=/`). Префикс `__Host-` дал бы браузерный гарант «только HTTPS, только этот хост, без Domain». Мелочь, но бесплатная. **Вопрос:** переименовать (учесть dev-режим `PANEL_COOKIE_SECURE=false` — `__Host-` требует `Secure`, т.е. только когда secure включён).
|
||||||
5. **Setup-ссылка печатается в stdout контейнера.** По ТЗ (7.6.1) — так и задумано, но если логи контейнера уезжают в агрегатор, токен там осядет на 10 минут. Файл `/data/setup-token` (0600) — альтернатива. Оставить как есть (по ТЗ), но **отметить в README**, что для сред с централизованными логами предпочтителен файл.
|
5. **Setup-ссылка печатается в stdout контейнера.** По ТЗ (7.6.1) — так и задумано, но если логи контейнера уезжают в агрегатор, токен там осядет на 10 минут. Файл `/data/setup-token` (0600) — альтернатива. Оставить как есть (по ТЗ), но **отметить в README**, что для сред с централизованными логами предпочтителен файл.
|
||||||
6. **Нет 2FA / смены пароля админа / нескольких админов из UI.** ТЗ этого не требует (один админ, secret-link). Смена пароля сейчас — только пересоздание состояния. **Вопрос:** нужен ли эндпоинт «сменить пароль администратора» в v1.x (разумное мелкое добавление) — 2FA и мульти-админ явно 2.x/вне объёма.
|
6. **Нет 2FA / смены пароля админа / нескольких админов из UI.** ТЗ этого не требует (один админ, secret-link). Смена пароля сейчас — только пересоздание состояния. **Решено:** добавить раздел настроек аккаунта (логин + пароль) — см. Фазу 12 ниже. 2FA и мульти-админ остаются явно 2.x/вне объёма.
|
||||||
|
|
||||||
### B. Надёжность и эксплуатация
|
### B. Надёжность и эксплуатация
|
||||||
|
|
||||||
@@ -46,11 +46,125 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 12 (v1.x) — Страница статуса сервиса + DNS-проверка доменов
|
## Фаза 12 (v1.x) — UI/UX: навигация, аккаунт, backup и параметры подключения
|
||||||
|
|
||||||
**Статус:** запланирована, не начата.
|
**Статус:** запланирована, не начата.
|
||||||
|
|
||||||
**Цель:** дать оператору один взгляд на «сервис жив и почта не будет зарубаться из-за DNS» — два независимых экрана: здоровье процесса (`/status`) и корректность DNS для каждого отправляющего домена (встроено в существующую страницу домена). Выходит за рамки ТЗ v1.0 (не было в разделе 7.2), согласовано с пользователем как отдельная фаза.
|
**Цель:** устранить конкретные недостатки панели, замеченные при использовании: (1) навигационная шапка не показывает, на какой странице находится админ; (2) нет способа сменить логин/пароль администратора после `/setup`, кроме пересоздания состояния; (3) полный бэкап и импорт домена свалены в один блок на дашборде, хотя это два разных по смыслу и риску действия; (4) страница домена не показывает сервер/порт/шифрование, нужные для настройки клиента отправки; (5) значения, которые нужно переносить во внешние сервисы (DKIM-запись, пароль приложения), нельзя скопировать одной кнопкой; (6) поле «Addresses» показывается даже в режиме, где оно не используется.
|
||||||
|
|
||||||
|
### A. Навигационная шапка обязательна на каждой странице панели
|
||||||
|
|
||||||
|
**Правило (закладывается на будущее, не только для текущих страниц):** навигационная шапка —
|
||||||
|
обязательный элемент **каждой** аутентифицированной страницы панели, без исключений. Чтобы это не
|
||||||
|
превращалось в чек-лист «не забыть добавить нав в очередной новый шаблон» (как уже случилось с
|
||||||
|
`domain_detail.html`/`domain_delete.html`, см. ниже, и как иначе случилось бы с будущими `/account`,
|
||||||
|
`/backup`, `/status` из этой же и следующей фазы) — нав должен рендериться **структурно**, из
|
||||||
|
[layout.html](../internal/web/templates/layout.html) (общей обёртки всех страниц), а не копипастой
|
||||||
|
в каждом content-шаблоне. Тогда гарантия «шапка есть везде» не зависит от того, вспомнил ли автор
|
||||||
|
конкретной страницы её вставить.
|
||||||
|
|
||||||
|
Сейчас же топбар (`Domains`/`Send log`/`Queue`/`Log`) продублирован вручную и по-разному в разных
|
||||||
|
content-шаблонах (каждый определяет `{{define "content"}}` независимо, `layout.html` их просто
|
||||||
|
оборачивает, см. [layout.html:81](../internal/web/templates/layout.html)):
|
||||||
|
- [dashboard.html](../internal/web/templates/dashboard.html), [queue.html](../internal/web/templates/queue.html), [sendlog.html](../internal/web/templates/sendlog.html), [logtail.html](../internal/web/templates/logtail.html) содержат нав-ссылки, но без выделения текущей страницы, и текущая страница обычно **не включает ссылку на саму себя** (например, `queue.html` не содержит пункта «Queue»), из-за чего создаётся впечатление, что кнопка «пропадает», а не выделяется;
|
||||||
|
- [domain_detail.html](../internal/web/templates/domain_detail.html) и [domain_delete.html](../internal/web/templates/domain_delete.html) — **самая посещаемая страница панели** (там живут DKIM-запись, приложения, лимиты) — вообще **не содержат нав-ссылок**: топбар там ограничен `{{.User}}`/«Sign out», и чтобы перейти на Send log/Queue/Log, нужно сначала вернуться на «← All domains». Именно это отсутствие и стало поводом сформулировать правило выше как общее, а не как точечный фикс двух файлов.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- Вынести `{{define "nav"}}` и вызвать его **из `layout.html`**, один раз, до `{{template "content" .}}` — не из отдельных content-шаблонов. Заодно туда же уходит и `{{.User}}`/«Sign out», которые сейчас тоже продублированы в топбаре каждого шаблона — так они тоже перестают зависеть от того, вспомнили их вставить или нет.
|
||||||
|
- Content-шаблоны сохраняют только то, что специфично для страницы (заголовок `<h1>`, специфичные действия вроде будущего «Reload» на `/status`) — без списка нав-ссылок и без `{{.User}}`/Sign out.
|
||||||
|
- Добавить в данные каждого шаблона поле `Active string` (`"domains"`/`"queue"`/`"sendlog"`/`"logtail"`, позже — `"status"`/`"account"`/`"backup"`) — простая правка в `handleDashboard`, `handleQueue`, `handleSendLog`, `handleLogTail`, `handleDomainDetail`, `handleDeleteConfirm`, а также во всех новых хендлерах, которые появятся в этой и следующей фазе. `layout.html` получает `.Active` наравне с остальными полями (`Title`, `User`), т.к. оборачивает все content-шаблоны одним и тем же кодом — ничего специально прокидывать через `content` не нужно.
|
||||||
|
- Активный пункт рендерится как `<span aria-current="page" class="active">`, а не `<a>` (не ссылка сама на себя), остальные — обычные ссылки.
|
||||||
|
- CSS: `.nav [aria-current]` — визуально выделенное состояние (цвет/подчёркивание/фон), уже в стиле остальной палитры `layout.html`.
|
||||||
|
|
||||||
|
### B. Раздел настроек аккаунта (логин, пароль)
|
||||||
|
|
||||||
|
Сейчас нет способа сменить логин или пароль администратора после `/setup` — только пересоздание состояния БД. `internal/store/admin.go` содержит только `CreateAdmin`/`GetAdmin`, без `Update*`.
|
||||||
|
|
||||||
|
- **Store:** `UpdateAdmin(username, passwordHash string) error` (один UPDATE по `id = 1`).
|
||||||
|
- **Web:** новый маршрут `GET/POST /account` в authed-группе ([web.go](../internal/web/web.go)); `handleAccount` показывает форму (текущий логин, поля «новый логин», «текущий пароль», «новый пароль», «подтверждение нового пароля»); `handlePostAccount` проверяет текущий пароль (`bcrypt.CompareHashAndPassword`, как в `handleLogin`) перед применением изменений, затем вызывает `UpdateAdmin`.
|
||||||
|
- Ссылка «Account» — в общей навигации (пункт A), рядом с `{{.User}}`/«Sign out».
|
||||||
|
- **Security:** та же lockout-защита, что у логина (rate-limit по неверному «текущему паролю», чтобы `/account` нельзя было использовать для брутфорса пароля обходным путём); при успешной смене пароля — инвалидировать все активные in-memory сессии, кроме текущей (заставить перелогиниться остальные при параллельном доступе — здесь один админ, так что риск минимален, но защищает от кражи старой cookie).
|
||||||
|
- **Шаблон:** `account.html` по образцу `login.html`/`setup.html` (`.card.narrow`).
|
||||||
|
|
||||||
|
### C. Backup/migration — отдельная страница, раздельные блоки
|
||||||
|
|
||||||
|
Сейчас на [dashboard.html](../internal/web/templates/dashboard.html) (строки 57–82) полный бэкап
|
||||||
|
(«Download full backup») и импорт домена («Import a domain») — **два разных по смыслу и риску
|
||||||
|
действия** — свалены в одну карточку «Backup & migration» посреди страницы со списком доменов.
|
||||||
|
Экспорт одного домена при этом уже (правильно) живёт отдельно, на самой странице домена
|
||||||
|
([domain_detail.html:181](../internal/web/templates/domain_detail.html)).
|
||||||
|
|
||||||
|
- Новый маршрут `GET /backup` (authed) с собственным пунктом в общей навигации (пункт A) — например «Backup».
|
||||||
|
- На дашборде вместо целой карточки остаётся одна ссылка/карточка-указатель на `/backup` (или пункт навигации целиком заменяет карточку — решить при реализации, что выглядит чище).
|
||||||
|
- На странице `/backup` — **две отдельные карточки**, не одна:
|
||||||
|
1. **Full backup** — тот же текст и форма (`POST /backup`), что сейчас, без изменений в логике.
|
||||||
|
2. **Import a domain** — та же форма (`POST /domains/import`), без изменений в логике.
|
||||||
|
- Домен-экспорт на `domain_detail.html` не переносится — он уже привязан к конкретному домену и корректно расположен рядом с его данными; трогать не нужно.
|
||||||
|
- Handler: новый `handleBackupPage` рендерит `backup.html`; существующие `handleBackup`/`handleImportDomain` не меняются (те же POST-маршруты).
|
||||||
|
|
||||||
|
**Готово, когда:** нав рендерится один раз из `layout.html` (не копипастой по content-шаблонам), поэтому присутствует на каждой странице панели, включая `domain_detail.html`/`domain_delete.html` (где нав-ссылок сейчас нет вовсе) и на всех будущих страницах этой и следующей фазы без отдельной правки каждой из них; текущий пункт визуально выделен и не дублируется как кликабельная ссылка на себя; `/account` позволяет сменить логин и/или пароль администратора с проверкой текущего пароля; после смены пароля старые сессии, кроме текущей, недействительны; `/backup` — отдельная страница с двумя визуально раздельными карточками (полный бэкап; импорт домена), дашборд больше не показывает эти формы напрямую; `gofmt`/`vet`/`test`/`docker build` зелёные.
|
||||||
|
|
||||||
|
**Риски:** смена логина не должна затронуть ничего, кроме таблицы `admin` (это учётная запись панели, не SASL-логины приложений — они хранятся отдельно в `applications`, см. Фазу 4); нужно убедиться, что rate-limit смены пароля не создаёт новый вектор lockout-DoS сверх уже описанного в п. A.1 открытых вопросов; вынос backup/import на отдельную страницу — чисто вёрстка, логика обработчиков `/backup` и `/domains/import` не меняется, так что риск регресса минимален.
|
||||||
|
|
||||||
|
### D. Параметры подключения клиента на странице домена
|
||||||
|
|
||||||
|
Сейчас [domain_detail.html](../internal/web/templates/domain_detail.html) показывает DKIM-запись и
|
||||||
|
таблицу приложений (логин/режим адресов), но нигде на странице нет данных, нужных, чтобы **настроить
|
||||||
|
почтовый клиент/скрипт для отправки** — сервер, порт, тип шифрования. Карточка «New application
|
||||||
|
password» (строки 17–27) показывает только логин/пароль конкретного приложения — этого недостаточно
|
||||||
|
без сервера/порта/шифрования рядом.
|
||||||
|
|
||||||
|
- Новая карточка **«Sending server settings»** на `domain_detail.html`, рядом с карточкой DKIM (до или после — решить при вёрстке), с фиксированными для всего инстанса значениями:
|
||||||
|
- **Server:** `{{.Hostname}}` (то же значение `SELFPOST_HOSTNAME`, которое уже используется для setup-ссылки и SASL realm — сервер уже хранит его в `Server.cfg.Hostname`, [web.go:25](../internal/web/web.go); прокинуть в данные шаблона `domain_detail`, аналогично тому, как это уже сделано для `dashboard`/`setup`).
|
||||||
|
- **Port 465 — SSL/TLS (implicit)** — всегда доступен (primary listener, spec 5).
|
||||||
|
- **Port 587 — STARTTLS (submission)** — показывать строку только если включён `SUBMISSION_ENABLE` (сейчас это чисто deploy-время env, [.env.example:10](../deploy/.env.example); нужно завести `Config.SubmissionEnabled bool` в [web.go](../internal/web/web.go), прокинуть из `os.Getenv("SUBMISSION_ENABLE")` в [cmd/panel/main.go](../cmd/panel/main.go) по аналогии с `Hostname`). Если submission выключен — строку не показывать вовсе, а не показывать «недоступно», чтобы не путать.
|
||||||
|
- **Username:** логин конкретного приложения — не общий для домена; сослаться на таблицу «Applications» ниже на той же странице, а не дублировать значение здесь (оно меняется per-application).
|
||||||
|
- **Password:** пояснение, что пароль отображается один раз при создании/регенерации приложения (уже описано в карточке `NewCred`) — здесь не показываем.
|
||||||
|
- Значения read-only (`.code`, как остальные DNS-блоки), без форм — это справочная информация, не настройка.
|
||||||
|
|
||||||
|
**Готово (доп. к критерию блока A/B/C):** страница домена показывает сервер/порт(ы)/шифрование, необходимые для настройки клиента отправки, без необходимости смотреть в документацию или `.env`; порт 587 отображается только когда submission действительно включён на этом инстансе.
|
||||||
|
|
||||||
|
**Риски:** `SubmissionEnabled` — deploy-time флаг (`docker-compose.yml`), а не что-то, что панель может проверить рантаймом (например, слушает ли порт 587 реально) — если оператор выставил `SUBMISSION_ENABLE=true` в `.env`, но не перезапустил compose с проброшенным портом 587, страница покажет строку, которая не работает. Отметить это как известное ограничение (как и остальные deploy/env-производные показатели), не решать в рамках этой фазы усложнением (например, реальной проверкой прослушивания порта — это ближе к `/status`, не к странице домена).
|
||||||
|
|
||||||
|
### E. Кнопка «Copy» на полях, которые переносятся во внешние сервисы
|
||||||
|
|
||||||
|
Значения из карточек `.code` сейчас нужно выделять мышью вручную — DKIM-запись (name/value),
|
||||||
|
логин/пароль нового приложения, будущие поля из пункта D (сервер) регулярно копируются в другой
|
||||||
|
интерфейс (DNS-панель регистратора, почтовый клиент, скрипт). Тривиально добавить копирование одной
|
||||||
|
кнопкой.
|
||||||
|
|
||||||
|
- Общий JS-хелпер в [layout.html](../internal/web/templates/layout.html) (без библиотек): `navigator.clipboard.writeText(text)`, вызываемый по клику маленькой кнопки/иконки рядом с каждым `.code`-элементом; кратковременная визуальная обратная связь (например, текст кнопки на 1–2 секунды меняется на «Copied»).
|
||||||
|
- Разметка: небольшая обёртка `.code-row` (код + кнопка `Copy`) вместо голого `.code`, значение бралось из `textContent` элемента (не из отдельного JS-массива — тогда не рассинхронизируется с тем, что реально показано).
|
||||||
|
- Применить везде, где сейчас есть `.code` для значений, которые логично куда-то переносить: DKIM `Record.Name`/`Record.Value` ([domain_detail.html:35](../internal/web/templates/domain_detail.html), [domain_detail.html:41](../internal/web/templates/domain_detail.html)), `NewCred.Login`/`NewCred.Password` ([domain_detail.html:23](../internal/web/templates/domain_detail.html), [domain_detail.html:25](../internal/web/templates/domain_detail.html)), поля карточки «Sending server settings» из пункта D. Таблица приложений (`td.code` с логином) — тоже кандидат, но менее приоритетно (короткое значение, легко выделить руками).
|
||||||
|
- **Секьюрити:** `navigator.clipboard.writeText` требует secure context (HTTPS или `localhost`) — в проде это всегда так (панель форсирует HTTPS), в dev-режиме с `PANEL_COOKIE_SECURE=false` по голому HTTP кнопка может тихо не сработать в некоторых браузерах. Не городить `document.execCommand('copy')`-fallback ради dev-режима — задокументировать как известное ограничение и/или обернуть в `try/catch`, чтобы отсутствие Clipboard API не ломало страницу, а просто оставляло текст доступным для ручного выделения.
|
||||||
|
|
||||||
|
**Готово:** у DKIM-записи, нового пароля приложения и параметров подключения (сервер) есть кнопка «Copy», по клику значение оказывается в буфере обмена и показывается краткое подтверждение.
|
||||||
|
|
||||||
|
**Риски:** нет побочных эффектов на бэкенд — чистая клиентская правка; единственный нюанс — secure-context ограничение Clipboard API в dev, описанное выше.
|
||||||
|
|
||||||
|
### F. Скрывать поле адресов в режиме «Any address»
|
||||||
|
|
||||||
|
Обе формы с выбором режима адресов — «Add an application» ([domain_detail.html:163–171](../internal/web/templates/domain_detail.html)) и «Edit mode» на существующем приложении ([domain_detail.html:72–79](../internal/web/templates/domain_detail.html)) — всегда показывают textarea «Addresses», даже когда выбран `Any address of the domain` (wildcard), где это поле не используется и просто игнорируется сервером. Из-за этого неочевидно, что поле относится только к режиму «List».
|
||||||
|
|
||||||
|
- Чистый клиентский JS (без изменений на сервере — валидация `mode`/`addresses` в `internal/app` не трогается, т.к. в wildcard-режиме бэкенд и так игнорирует содержимое textarea): на `change` у `<select name="mode">` показывать/прятать соответствующий `<label>`+`<textarea addresses>` через `hidden`/`display:none`, в зависимости от того, выбран ли `$.Wildcard` или `$.List`.
|
||||||
|
- Начальное состояние при загрузке страницы — тоже должно учитывать текущее значение `select` (важно для формы «Edit mode», где `select` уже может быть предзаполнен значением `List`, а не только для формы добавления, где по умолчанию `Wildcard`).
|
||||||
|
- Реализовать один небольшой переиспользуемый скрипт (или `<script>` внизу `domain_detail.html`), т.к. на странице несколько экземпляров этой пары select+textarea (форма добавления + одна на каждое существующее приложение в таблице).
|
||||||
|
|
||||||
|
**Готово:** при выборе «Any address of the domain» поле «Addresses» скрывается (и в форме добавления, и в «Edit mode» для существующих приложений); при выборе «Specific addresses (list)» — снова показывается с уже введённым значением, если было.
|
||||||
|
|
||||||
|
**Риски:** нет — чисто клиентское поведение, серверная валидация уже игнорирует `addresses` для wildcard-режима, так что скрытие поля ничего не меняет по сути обработки формы.
|
||||||
|
|
||||||
|
**Модель:** Sonnet (UI + рутинный CRUD, не риск-критичный тракт доставки).
|
||||||
|
|
||||||
|
**Зависимости:** не блокируется другими фазами; вводит общий `nav`-partial и `Active`-механизм, которым позже пользуется Фаза 13 (добавляет туда пункт «Status» и переносит «Domains» на `/domains`) — предпочтительно делать Фазу 12 первой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 13 (v1.x) — Страница статуса сервиса + DNS-проверка доменов
|
||||||
|
|
||||||
|
**Статус:** запланирована, не начата.
|
||||||
|
|
||||||
|
**Цель:** дать оператору один взгляд на «сервис жив и почта не будет зарубаться из-за DNS» — два независимых экрана: здоровье процесса (`/status`) и корректность DNS для каждого отправляющего домена (встроено в существующую страницу домена). Выходит за рамки ТЗ v1.0 (не было в разделе 7.2), согласовано с пользователем как отдельная фаза. **`/status` также становится стартовой страницей панели** (см. пункт C) — первое, что видит админ после логина, вместо списка доменов — и получает кнопку «Reload», перенесённую с дашборда с понятным пояснением назначения (см. пункт D).
|
||||||
|
|
||||||
### A. `/status` — здоровье сервера (только сервер, без доменов)
|
### A. `/status` — здоровье сервера (только сервер, без доменов)
|
||||||
|
|
||||||
@@ -74,6 +188,42 @@
|
|||||||
3. **DMARC** — TXT `_dmarc.<domain>`, наличие + политика (`none`/`quarantine`/`reject`).
|
3. **DMARC** — TXT `_dmarc.<domain>`, наличие + политика (`none`/`quarantine`/`reject`).
|
||||||
4. Статусы по каждому пункту: не найдено / найдено, но не покрывает наш сервис / корректно. Кэш на домен (TTL ~5–10 мин) + кнопка «Recheck» (без autopolling — DNS дороже, чем локальные проверки блока A).
|
4. Статусы по каждому пункту: не найдено / найдено, но не покрывает наш сервис / корректно. Кэш на домен (TTL ~5–10 мин) + кнопка «Recheck» (без autopolling — DNS дороже, чем локальные проверки блока A).
|
||||||
|
|
||||||
|
### C. `/status` — стартовая страница панели
|
||||||
|
|
||||||
|
Сейчас корневой маршрут `GET /{$}` (`handleDashboard`, [web.go:109](../internal/web/web.go)) отдаёт список
|
||||||
|
доменов — это же первая страница после логина. По решению пользователя стартовой страницей должна
|
||||||
|
быть страница статуса, а не список доменов.
|
||||||
|
|
||||||
|
- Список доменов (карточки «Add a sending domain» + «Domains» из [dashboard.html](../internal/web/templates/dashboard.html)) переезжает на новый маршрут `GET /domains` (тот же `handleDashboard`, просто перевешенный на другой путь; `POST /domains` для добавления домена остаётся как есть — коллизий с `GET /domains` нет, разные методы).
|
||||||
|
- Корневой `GET /{$}` начинает рендерить `/status` (страницу из блока A) — либо редиректом `/{$}` → `/status`, либо status-хендлер напрямую вешается и на `/`, и на `/status` (без редиректа, чуть дешевле). Редирект проще и не создаёт двух путей для одного контента — предпочтительный вариант.
|
||||||
|
- **Логин:** `handleLogin` после успешной аутентификации сейчас редиректит на `/` — поведение не меняется (просто `/` теперь означает статус, а не домены), правки в `handlers_auth.go` не требуется.
|
||||||
|
- **Навигация (Фаза 12.A):** пункт «Status» становится первым в общем `nav`-partial и получает `Active == "status"`; пункт «Domains» указывает на `/domains` вместо `/`. Это расширяет список `Active`-значений, заведённый в Фазе 12, а не меняет его архитектуру.
|
||||||
|
- Везде, где по коду сейчас зашит редирект/ссылка на `/` как «страница доменов» (например, `<a class="back" href="/">← Domains</a>` в `queue.html`, `sendlog.html`, `logtail.html`, `domain_detail.html`, `domain_delete.html`), ссылку нужно поменять на `/domains`.
|
||||||
|
|
||||||
|
**Готово (доп. к критерию блока A/B):** `GET /` открывает `/status`; список доменов доступен по `GET /domains`; все ссылки «← Domains» и пункт навигации «Domains» ведут на `/domains`; логин после успешной аутентификации попадает на страницу статуса.
|
||||||
|
|
||||||
|
**Риски:** нужно пройтись по всем захардкоженным `href="/"` в шаблонах (см. список выше) — пропущенная ссылка тихо ведёт на статус вместо доменов, а не ломается явно, поэтому стоит грепнуть `href="/"` после реализации и проверить каждое совпадение.
|
||||||
|
|
||||||
|
### D. Кнопка «Reload» — назначение непонятно, присутствует непоследовательно
|
||||||
|
|
||||||
|
`handleReload` ([handlers_domains.go:119](../internal/web/handlers_domains.go)) пересобирает конфигурацию OpenDKIM и Postfix-карту отправителей из БД и перечитывает оба демона (spec 7.2.12) — это **ручной drift-recovery** («на всякий случай пересобери конфиг с нуля»), а не элемент навигации. Сейчас кнопка:
|
||||||
|
- есть **только** на дашборде ([dashboard.html:9](../internal/web/templates/dashboard.html)), в топбаре вперемешку с навигационными ссылками — на `queue`/`sendlog`/`logtail`/`domain_detail` её нет;
|
||||||
|
- подписана просто «Reload» без единого слова, что она делает;
|
||||||
|
- после нажатия даёт флеш «Configuration reloaded.» — тоже без объяснения, что было пересобрано и зачем это могло понадобиться.
|
||||||
|
|
||||||
|
Итог: пользователю (да и админу, впервые видящему панель) неясно, что это за действие, когда его стоит нажимать, и почему оно недоступно с других страниц.
|
||||||
|
|
||||||
|
**Решение:** перенести Reload на `/status` (блок A) — тематически это ровно то же самое действие, что и остальной блок здоровья сервера (проверка/восстановление рабочего состояния демонов), и после Фазы 13 `/status` и так становится стартовой страницей, так что кнопка не теряется, а оказывается на самом заметном месте.
|
||||||
|
|
||||||
|
- Убрать кнопку/форму `POST /reload` из [dashboard.html](../internal/web/templates/dashboard.html) и топбара.
|
||||||
|
- Добавить на `/status` карточку «Configuration» (или секцию рядом с блоком A.1–A.4) с формой `POST /reload` и явным пояснением: *«Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use this if you edited files manually, restored a backup, or the config looks out of sync with the domain/application list below — it does not affect the mail queue or TLS.»*
|
||||||
|
- Редирект `handleReload` — сейчас `"/?reloaded=1"` ([handlers_domains.go:130](../internal/web/handlers_domains.go)); после переноса меняется на `"/status?reloaded=1"`, и флеш-сообщение обрабатывается на странице статуса (`dashboardFlash` для `reloaded` убирается — переносится в статус-хендлер, `deleted`/остальные флеши дашборда остаются на месте).
|
||||||
|
- Логика `handleReload` (сам `Resync` обоих компонентов) не меняется — это чисто перенос UI и текста, не изменение поведения.
|
||||||
|
|
||||||
|
**Готово (доп. к критерию блока A/B/C):** кнопка Reload есть только на `/status`, с текстом, объясняющим, что именно она пересобирает и когда это нужно; на дашборде/остальных страницах кнопки/флеша про reload больше нет.
|
||||||
|
|
||||||
|
**Риски:** нет — чисто перенос существующего, уже проверенного действия; главное — не потерять флеш-сообщение при переносе редиректа.
|
||||||
|
|
||||||
### Архитектура
|
### Архитектура
|
||||||
|
|
||||||
- Новый пакет `internal/dnscheck`: `ServerIP(hostname)` (A/AAAA lookup), `PTRCheck(hostname, ip)`, `DKIMCheck(domain, selector, expectedKey)`, `SPFCheck(domain, serverIP)`, `DMARCCheck(domain)` — все с таймаутом на DNS-запрос (~5с), через `net.DefaultResolver`/контекст.
|
- Новый пакет `internal/dnscheck`: `ServerIP(hostname)` (A/AAAA lookup), `PTRCheck(hostname, ip)`, `DKIMCheck(domain, selector, expectedKey)`, `SPFCheck(domain, serverIP)`, `DMARCCheck(domain)` — все с таймаутом на DNS-запрос (~5с), через `net.DefaultResolver`/контекст.
|
||||||
@@ -81,13 +231,13 @@
|
|||||||
- Веб: `GET /status` + `GET /status/fragment` (быстрый блок A, для polling); секция DNS-статуса на `domain_detail.html` + `POST /domains/{id}/dns-recheck` (форс, обход кэша).
|
- Веб: `GET /status` + `GET /status/fragment` (быстрый блок A, для polling); секция DNS-статуса на `domain_detail.html` + `POST /domains/{id}/dns-recheck` (форс, обход кэша).
|
||||||
- Безопасность: ничего нового по ТЗ 7.6 не добавляет — DNS/exec-вызовы только за авторизованным админом, без пользовательского ввода в exec (доменные имена уже провалидированы при добавлении домена), таймауты на все сетевые вызовы (защита от зависания страницы).
|
- Безопасность: ничего нового по ТЗ 7.6 не добавляет — DNS/exec-вызовы только за авторизованным админом, без пользовательского ввода в exec (доменные имена уже провалидированы при добавлении домена), таймауты на все сетевые вызовы (защита от зависания страницы).
|
||||||
|
|
||||||
**Готово, когда:** `/status` показывает живой статус всех пяти пунктов блока A и обновляется polling'ом; на странице домена отображается статус DKIM/SPF/DMARC с кнопкой Recheck; PTR-проверка корректно ловит и совпадение, и несовпадение (проверено на реальном домене `selfpost.mixfed.ru`, у которого PTR уже настроен, — см. [selfpost-prod-deployment.md]); `gofmt`/`vet`/`test`/`docker build` зелёные.
|
**Готово, когда:** `/status` показывает живой статус всех пяти пунктов блока A и обновляется polling'ом и является стартовой страницей (`GET /`); список доменов доступен на `GET /domains`; на странице домена отображается статус DKIM/SPF/DMARC с кнопкой Recheck; PTR-проверка корректно ловит и совпадение, и несовпадение (проверено на реальном домене `selfpost.mixfed.ru`, у которого PTR уже настроен, — см. [selfpost-prod-deployment.md]); кнопка «Reload» присутствует только на `/status`, с пояснением назначения; `gofmt`/`vet`/`test`/`docker build` зелёные.
|
||||||
|
|
||||||
**Риски:** DNS-резолверы могут быть медленными/недоступными — все проверки таймаутят и не блокируют остальной UI (страница рендерится с "неизвестно/timeout", а не висит). Ложные срабатывания SPF-эвристики (например, домен покрывает IP сервера через `include:` стороннего сервиса, который в свою очередь резолвится в IP сервера) — задокументировать как известное ограничение поверхностной проверки.
|
**Риски:** DNS-резолверы могут быть медленными/недоступными — все проверки таймаутят и не блокируют остальной UI (страница рендерится с "неизвестно/timeout", а не висит). Ложные срабатывания SPF-эвристики (например, домен покрывает IP сервера через `include:` стороннего сервиса, который в свою очередь резолвится в IP сервера) — задокументировать как известное ограничение поверхностной проверки. Плюс риск блока C — не пропустить захардкоженную ссылку на `/`.
|
||||||
|
|
||||||
**Модель:** Sonnet (UI + рутинные DNS-lookup, не риск-критичный тракт доставки/безопасности).
|
**Модель:** Sonnet (UI + рутинные DNS-lookup, не риск-критичный тракт доставки/безопасности).
|
||||||
|
|
||||||
**Зависимости:** не блокирует и не блокируется существующими фазами; использует уже реализованные `internal/postfix.Queue()`, DKIM-логику домена, паттерны HTMX-фрагментов из Фазы 7.
|
**Зависимости:** использует общий `nav`-partial и `Active`-механизм навигации, вводимые Фазой 12 — реализовывать после неё (или расширить тот же PR); использует уже реализованные `internal/postfix.Queue()`, DKIM-логику домена, паттерны HTMX-фрагментов из Фазы 7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user