Задача 036: меню пользователя в шапке

Логин в шапке — триггер выпадающего списка «Сменить пароль» / «Выйти» в общем стиле списков
(popMenu, row-pop) вместо отдельных кнопок; шрифт и место прежней кнопки «Выйти».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Opus 5.5 committed 2026-09-27 16:14:58 +03:00
1 parent 8384c2c311
commit c852f47f09
5 files changed
+71 -2

No files matched your search

+2
View File
@@ -141,6 +141,7 @@ docs/changes/ планы и итоги доработок docs/reviews/
## Интерфейс
- Экраны: «Обзор», «Префиксы» (дерево по VRF), «Адреса» подсети, «Организации», «Операторы», «Устройства», «Журнал», «Пользователи» (только `superadmin`).
- В «Пользователях» видна дата и IP последнего входа (изменение 035); «—», если пользователь ещё не входил.
- Логин в шапке раскрывает меню «Сменить пароль» / «Выйти» (изменение 036).
- Переключатель организации — только у `superadmin`; `admin`/`viewer` работают в своей организации.
- Строка реестра кликабельна целиком. Действия над строкой — в меню «⋯».
- Групповые операции через чекбоксы (кроме «Журнала»): удаление, смена типа устройств, статус префиксов и адресов, доступ пользователей.
@@ -191,6 +192,7 @@ docker compose -p ipam_control_006 up -d --build && venv/bin/python -m pytest -q
| 033 | Исправление находок ревью 032 | [план](docs/changes/033-review-fixes-032/PLAN.md) · [итог](docs/changes/033-review-fixes-032/SUMMARY.md) |
| 034 | Публикация через Caddy: rxipam.rxmsk.ru | [план](docs/changes/034-caddy-publication/PLAN.md) · [итог](docs/changes/034-caddy-publication/SUMMARY.md) |
| 035 | Последний вход пользователя: дата и IP | [план](docs/changes/035-user-last-login/PLAN.md) · [итог](docs/changes/035-user-last-login/SUMMARY.md) |
| 036 | Меню пользователя в шапке: логин с выпадающим списком | [план](docs/changes/036-user-menu/PLAN.md) · [итог](docs/changes/036-user-menu/SUMMARY.md) |
## Отчёты ревью
- [Ревью кодовой базы](docs/reviews/2026-09-26-codebase-review.md) (находки → изменения 011–023)
+42
View File
@@ -0,0 +1,42 @@
# Меню пользователя в шапке: логин с выпадающим списком (изменение 036)
## Context
Сейчас справа в шапке (`web/app.js::shell`) три элемента: серый логин (`span.user`) и две кнопки `btn ghost` — «Сменить пароль» и «Выйти».
Их нужно свернуть в один элемент. В шапке остаётся только логин; по нажатию раскрывается список «Сменить пароль» / «Выйти»
в том же стиле, что все выпадающие списки сайта (`popMenu` + `.pop`). Логин стоит на месте нынешней кнопки «Выйти»,
шрифт тот же, что у неё: `.btn.ghost` — `500 14px var(--sans)`, цвет `--muted`.
## Изменения — `web/app.js`
- `shell()`: вместо `span.user` и двух `btn(...)` — один триггер по образцу `bulkMenu`/`filterBtn`:
```
<span class="rel user-menu"><button type="button" class="btn ghost" data-action="menu" data-menu="user" title="<роль>">
<логин>${I.chevD(14)}</button>${popMenu("user", [{label:"Сменить пароль", value:"pw"}, {label:"Выйти", value:"logout"}], "row-pop")}</span>
```
- `btn ghost` даёт шрифт и цвет нынешней «Выйти»; шеврон — как у остальных триггеров списков.
- `row-pop` (`top:40px; right:0`) выравнивает список по правому краю, чтобы он не уходил за край окна.
- Роль остаётся во всплывающей подсказке (`title`), как сейчас у `span.user`.
- `actions.pick`: ветка `id === "user"` — снять открытый `.pop` (как для `rowActions`), затем `value === "logout"` → `actions.logout()`,
иначе → `passwordDialog()`. Существующие действия `logout` и `pw-change` не меняются.
- Открытие и закрытие по клику вне списка, навигация стрелками — уже есть в общих обработчиках `menu`/`.pop` (≈стр. 1036, 1059), новых не нужно.
- Комментарии — «изменение 036».
## Изменения — `web/styles.css`
- Удалить неиспользуемое правило `.appbar .user`.
- При необходимости `.user-menu .pop{min-width:180px}`, если `row-pop` уже задаёт ширину — не добавлять.
## Документация (правила проекта)
- `docs/changes/036-user-menu/PLAN.md` — этот план; по завершении — `SUMMARY.md`.
- `README.md`, «Интерфейс»: логин в шапке раскрывает меню «Сменить пароль» / «Выйти»; строка 036 в истории.
## Исполнение
По принятой схеме: правки делает агент на Sonnet (только `node --check`, без стенда); ревью, пересборка стенда, проверки и SUMMARY — моя часть.
Тесты не нужны: API не меняется, UI автотестами не покрыт.
## Проверка
- `node --check web/app.js`; пересборка `docker compose -p ipam_control_006 up -d --build app`; стенд отдаёт новый `app.js`.
- В браузере на `https://rxipam.rxmsk.ru` (или `:8088`):
- в шапке справа только логин с шевроном, шрифт как у прежней «Выйти»;
- клик раскрывает список в стиле остальных, выровненный вправо;
- «Сменить пароль» открывает диалог, «Выйти» — экран входа;
- клик вне списка и Esc закрывают его, стрелки перемещают фокус.
- Визуальную проверку выполняет пользователь (либо через `claude-in-chrome`, если он подключён).
+19
View File
@@ -0,0 +1,19 @@
# Итог: меню пользователя в шапке (изменение 036)
План: `PLAN.md`. Справа в шапке вместо логина и кнопок «Сменить пароль» / «Выйти» — один элемент: логин с шевроном,
по нажатию раскрывается список «Сменить пароль» / «Выйти» в общем стиле выпадающих списков.
## Что сделано
| Файл | Изменение |
|---|---|
| `web/app.js` | `shell()`: триггер `btn ghost` с логином (шрифт прежней «Выйти») и ролью в `title`; список через общий `popMenu("user", …, "row-pop")`, выровнен вправо. `actions.pick`: ветка `user` закрывает список и вызывает `actions.logout()` или `passwordDialog()` |
| `web/styles.css` | Удалено неиспользуемое `.appbar .user` |
| `README.md` | «Интерфейс» — меню логина; строка 036 в истории |
## Проверки
- `node --check web/app.js` — чисто. Стенд пересобран: контейнер healthy, новый `app.js` отдаётся напрямую (`:8088`) и через Caddy (`rxipam.rxmsk.ru`).
- Ручная проверка в браузере (пользователь): вид шапки, раскрытие списка, «Сменить пароль», «Выйти», закрытие — подтверждено.
## Замечание по стенду
`docker compose -p ipam_control_006 up -d --build app` пересобрал образ, но не пересоздал контейнер — понадобился `--force-recreate`.
После пересборки проверять, что стенд отдаёт новый код, а не только статус healthy.
+7 -1
View File
@@ -132,9 +132,11 @@ const header = (title, sub, actions = "") => `<div class="head"><div><h1>${esc(t
const NAV = [["overview", "Обзор"], ["prefixes", "Префиксы"], ["orgs", "Организации"], ["isps", "Операторы"], ["devices", "Устройства"], ["journal", "Журнал"], ["users", "Пользователи", "superadmin"]]; // изменение 032: "superadmin"
function shell(active, content) {
const nav = NAV.filter(([, , role]) => role !== "superadmin" || isSuperadmin()); // изменение 032: раздел «Пользователи» виден только суперадминистратору
// изменение 036: логин в шапке — триггер выпадающего меню («Сменить пароль» / «Выйти») вместо отдельных кнопок
const userMenu = `<span class="rel user-menu"><button type="button" class="btn ghost" data-action="menu" data-menu="user" title="${esc(ROLE_RU[user?.role] ?? "")}">${esc(user?.username ?? "")}${I.chevD(14)}</button>${popMenu("user", [{ label: "Сменить пароль", value: "pw" }, { label: "Выйти", value: "logout" }], "row-pop")}</span>`;
return `<div class="appbar"><div class="brand"><span class="brand-logo">${I.globe()}</span>ipam_manager</div>
<nav class="nav">${nav.map(([k, l]) => `<a href="#/${k}" class="${k === active ? "active" : ""}">${l}</a>`).join("")}</nav>
<div class="grow"></div><span class="user" title="${esc(ROLE_RU[user?.role] ?? "")}">${esc(user?.username ?? "")}</span>${btn("Сменить пароль", "pw-change", { cls: "ghost" })}${btn("Выйти", "logout", { cls: "ghost" })}</div>
<div class="grow"></div>${userMenu}</div>
<div class="page">${content}</div>`;
}
@@ -924,6 +926,10 @@ const actions = {
else if (id === "jactor") { S.jactor = value; S.limit = 100; }
else if (id === "jentity") { S.jentity = value; S.limit = 100; }
else if (id === "bulk-status" || id === "bulk-type") return bulkPick(id, value);
else if (id === "user") { // изменение 036: меню логина в шапке
document.querySelectorAll(".pop").forEach((el) => el.remove());
return value === "logout" ? actions.logout() : passwordDialog();
}
else {
const key = Object.keys(rowActions).find((k) => id.startsWith(k + "-"));
const [act, rid] = value.split(":");
+1 -1
View File
@@ -14,7 +14,7 @@ button{font-family:var(--sans)}
.nav a{display:flex;align-items:center;height:100%;padding:0 14px;font:500 14px/1 var(--sans);color:var(--muted);border-bottom:2px solid transparent}
.nav a.active{color:var(--text);border-bottom-color:var(--primary)}
.grow{flex:1}
.appbar .user{color:var(--muted);font:400 14px/1 var(--sans)}
/* изменение 036: .appbar .user удалено — логин заменён на триггер меню (.user-menu .btn.ghost) */
/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:36px;margin:0;border-radius:8px;font:500 14px/1 var(--sans);white-space:nowrap;cursor:pointer;padding:0 14px;background:#fff;color:var(--text);border:1px solid var(--line-strong)}