From 96b71aaa4ea8a0d648333425351ec2e79186a2a9 Mon Sep 17 00:00:00 2001 From: Crimson Date: Fri, 8 May 2026 23:17:34 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D1=8B=20fix24/fix24.1/fix24.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fix24.1.txt | 713 ---------------------------------------------------- fix24.2.txt | 243 ------------------ fix24.txt | 617 --------------------------------------------- 3 files changed, 1573 deletions(-) delete mode 100644 fix24.1.txt delete mode 100644 fix24.2.txt delete mode 100644 fix24.txt diff --git a/fix24.1.txt b/fix24.1.txt deleted file mode 100644 index 861e2bb..0000000 --- a/fix24.1.txt +++ /dev/null @@ -1,713 +0,0 @@ -Да, это логичное продолжение: `Account Info` в текущей архитектуре надо не “улучшать”, а удалить как концепцию. После разделения `admin_user` и `peer` эта страница теряет смысл: админ — это оператор панели, а не клиентский peer с квотой, Node URL и QR. Правильная замена — единый стартовый `Dashboard`, куда переезжают системный мониторинг, состояние Hysteria2, агрегированная статистика peer’ов, трафик и security notices. - -Ниже — дополнение к плану выше. - ---- - -## 9. Заменить `Account Info` + `System Monitor` на полноценный Dashboard - -Сейчас есть две слабые страницы: - -`views/info/account/index.vue` — фактически профиль текущего `account`, но там одновременно greeting, create time, quota/download/upload/expire time, subscription buttons, QR и security notifications. - -`views/monitor/system/index.vue` — набор карточек без истории: версия панели, CPU, RAM, disk, версия Hysteria2, статус, online users/devices. - -После разделения админов и peer’ов это должно стать так: - -```text -/dashboard - ├─ общая сводка панели - ├─ состояние Hysteria2 - ├─ общий трафик всех peer’ов - ├─ графики CPU/RAM/Disk - ├─ online peers/devices - ├─ top peers by traffic - ├─ security warnings - └─ быстрые действия оператора -``` - -`Account Info` удалить из меню. Профиль админа оставить в dropdown справа сверху: “Profile”, “Change password”, “Logout”. Peer-информация, Node URL и QR должны жить только в `Peer Detail / Peer Drawer`, а не в профиле администратора. - ---- - -## 10. Что именно не так в текущей реализации - -Текущий `Account Info` вызывает `getAccountApi({ id: accountStore.id })`. Пока админ и peer лежат в одной таблице, это случайно работает. После нормального разделения это станет архитектурной ошибкой: admin ID не должен использоваться как peer ID. - -Кнопки `Subscribe`, `Subscribe QR`, `Node URL`, `Node QR Code` на странице админа надо убрать полностью. Они допустимы только для конкретного peer’а. Сейчас из-за смешанной модели админ может выглядеть как клиент подключения. - -`System Monitor` сейчас делает два независимых запроса: `/monitor/monitorSystem` и `/monitor/monitorHysteria2`. Для дашборда это лучше заменить одним агрегирующим endpoint’ом, иначе UI будет собирать бизнес-смысл из разных API. - -`MonitorSystem()` каждый раз делает live-запросы к CPU/RAM/Disk. CPU-сэмпл через `cpu.Percent(time.Second, false)` блокирует примерно на секунду. Для интерактивного Dashboard это плохая модель: страницу могут открыть несколько админов, и каждый начнёт дергать системные метрики. - -`GetDiskPercent()` берёт первый partition из `disk.Partitions(true)`. Это ненадёжно: первым может оказаться не root/data partition. Для панели нужно мониторить `/`, data-dir панели и, если нужно, mountpoint с Hysteria2 config/logs. - -`MonitorHysteria2()` каждый раз выполняет `hysteria2 version`. Версию бинарника не надо получать на каждый render дашборда. Её можно кешировать и обновлять редко: при старте, после смены версии, по ручному refresh или раз в несколько минут. - ---- - -## 11. Новый backend-модуль: `dashboard` - -Добавить отдельный модуль, а не расширять `monitor.go`. - -```text -controller/dashboard.go -service/dashboard.go -service/metrics_collector.go -dao/dashboard.go -model/vo/dashboard.go -router/dashboard.go -``` - -Минимальный набор API: - -```text -GET /hui/dashboard/summary -GET /hui/dashboard/timeseries?range=1h&step=1m -GET /hui/dashboard/top-peers?range=24h&limit=10 -GET /hui/dashboard/security -``` - -Можно начать с одного endpoint’а: - -```text -GET /hui/dashboard -``` - -Но лучше сразу разделить summary и timeseries. Summary обновляется часто, графики можно обновлять реже. - -Целевой response для summary: - -```go -type DashboardSummaryVo struct { - CollectedAt int64 `json:"collectedAt"` - - System SystemSummaryVo `json:"system"` - Hysteria HysteriaSummaryVo `json:"hysteria"` - Peers PeerSummaryVo `json:"peers"` - Traffic TrafficSummaryVo `json:"traffic"` - - SecurityRisks []SecurityRiskVo `json:"securityRisks"` -} - -type SystemSummaryVo struct { - CpuPercent float64 `json:"cpuPercent"` - MemUsedBytes uint64 `json:"memUsedBytes"` - MemTotalBytes uint64 `json:"memTotalBytes"` - MemPercent float64 `json:"memPercent"` - DiskUsedBytes uint64 `json:"diskUsedBytes"` - DiskTotalBytes uint64 `json:"diskTotalBytes"` - DiskPercent float64 `json:"diskPercent"` - UptimeSeconds uint64 `json:"uptimeSeconds"` - Load1 float64 `json:"load1"` -} - -type HysteriaSummaryVo struct { - Version string `json:"version"` - Running bool `json:"running"` - ApiReachable bool `json:"apiReachable"` - LastApiError string `json:"lastApiError,omitempty"` -} - -type PeerSummaryVo struct { - Total int64 `json:"total"` - Enabled int64 `json:"enabled"` - Disabled int64 `json:"disabled"` - Expired int64 `json:"expired"` - OnlinePeers int64 `json:"onlinePeers"` - OnlineDevices int64 `json:"onlineDevices"` -} - -type TrafficSummaryVo struct { - DownloadBytes int64 `json:"downloadBytes"` - UploadBytes int64 `json:"uploadBytes"` - TotalBytes int64 `json:"totalBytes"` - TodayDownloadBytes int64 `json:"todayDownloadBytes"` - TodayUploadBytes int64 `json:"todayUploadBytes"` - SinceResetDownloadBytes int64 `json:"sinceResetDownloadBytes"` - SinceResetUploadBytes int64 `json:"sinceResetUploadBytes"` -} -``` - -Важно: после разделения таблиц все peer-агрегации идут только из `peer`, не из `admin_user`. - ---- - -## 12. Отдельный collector вместо live-сбора на каждый HTTP-запрос - -Dashboard не должен каждый раз сам опрашивать систему и Hysteria2 API. Нужен background collector. - -```text -MetricsCollector - ├─ каждые 5–10 секунд: CPU/RAM/load/uptime - ├─ каждые 30–60 секунд: disk - ├─ каждые HYSTERIA2_TRAFFIC_TIME секунд: Hysteria2 traffic - ├─ каждые 5–15 секунд: Hysteria2 online - └─ редко: Hysteria2 version -``` - -HTTP endpoint читает готовый snapshot из памяти и, если нужно, последние точки из SQLite. Это решает сразу три проблемы: быстрый UI, меньше нагрузки, стабильная история для графиков. - -Пример внутренней модели: - -```go -type MetricsSnapshot struct { - CollectedAt time.Time - System SystemMetrics - Hysteria HysteriaRuntimeMetrics - Online map[string]int64 -} -``` - -Для защиты от зависаний все внешние операции — с timeout и context. Hysteria2 API уже вызывается с timeout, это надо сохранить. - ---- - -## 13. Исправить учёт трафика перед графиками - -Это критично. - -В текущем `saveAccountTraffic()` берётся `/traffic?clear=1`, после чего `rx/tx` умножаются на `HYSTERIA2_TRAFFIC_TIME`. По документации Hysteria2 `/traffic` возвращает traffic statistics по клиентам, а параметр `clear=1` обнуляет статистику после возврата. То есть эти значения надо трактовать как дельту с прошлого clear, а не как rate, который нужно умножать на интервал. Иначе трафик будет завышаться. ([v2.hysteria.network][1]) - -Нормальный pipeline: - -```text -1. collector вызывает /traffic?clear=1 -2. получает rx/tx bytes по auth/client id -3. сохраняет immutable traffic_sample -4. атомарно инкрементит peer.download_bytes / peer.upload_bytes -5. обновляет aggregate таблицы для dashboard -``` - -Новая таблица: - -```sql -CREATE TABLE traffic_sample ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - peer_id INTEGER NOT NULL, - auth_id TEXT NOT NULL, - rx_bytes INTEGER NOT NULL DEFAULT 0, - tx_bytes INTEGER NOT NULL DEFAULT 0, - sampled_at INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_traffic_sample_time ON traffic_sample(sampled_at); -CREATE INDEX idx_traffic_sample_peer_time ON traffic_sample(peer_id, sampled_at); -``` - -Для быстрых графиков и top peers добавить агрегаты: - -```sql -CREATE TABLE traffic_aggregate_hourly ( - peer_id INTEGER NOT NULL, - hour_start INTEGER NOT NULL, - rx_bytes INTEGER NOT NULL DEFAULT 0, - tx_bytes INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY(peer_id, hour_start) -); - -CREATE TABLE traffic_aggregate_daily ( - peer_id INTEGER NOT NULL, - day_start INTEGER NOT NULL, - rx_bytes INTEGER NOT NULL DEFAULT 0, - tx_bytes INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY(peer_id, day_start) -); -``` - -Сброс трафика peer’а не должен удалять историю. Он должен сбрасывать только счётчики quota-period: - -```text -peer.download_bytes = 0 -peer.upload_bytes = 0 -peer.traffic_reset_at = now -``` - -Иначе после reset dashboard потеряет исторические графики. В UI надо явно различать: - -```text -Traffic since reset -Traffic today -Traffic last 24h -Traffic total tracked -``` - ---- - -## 14. Online users/devices: переименовать и использовать корректно - -Hysteria2 `/online` возвращает map client ID → количество client instances. В документации отдельно указано, что это именно количество клиентских экземпляров, то есть “devices”, а не количество активных proxy-соединений. ([v2.hysteria.network][1]) - -Поэтому в Dashboard надо писать: - -```text -Online peers -Online devices -``` - -Не “online users”, если в новой модели сущность называется `peer`. - -Расчёт: - -```text -onlinePeers = len(onlineMap) -onlineDevices = sum(onlineMap values) -``` - -После миграции ключом должен быть `peer.auth_id` или `peer.name`, но не admin username. Главное — не завязываться на таблицу админов. - ---- - -## 15. Security warnings перенести из уведомлений в Dashboard - -Сейчас `Account Info` показывает всплывающие `ElNotification`: default password и no HTTPS. Это раздражает, перекрывает интерфейс и не масштабируется. - -На Dashboard сделать постоянный блок сверху: - -```text -Security Center - ⚠ Default admin password / force password change - ⚠ Panel is served without HTTPS - ⚠ Hysteria2 Traffic Stats API is not protected - ⚠ Hysteria2 is stopped - ⚠ Public endpoint env is not configured -``` - -Для Traffic Stats API это особенно важно: Hysteria2 docs прямо рекомендуют задавать `secret`, иначе любой, кто имеет доступ к API listen address, сможет смотреть traffic stats и kick users. ([v2.hysteria.network][2]) - -Backend должен отдавать structured warnings: - -```go -type SecurityRiskVo struct { - Key string `json:"key"` // dashboard.security.noHttps - Severity string `json:"severity"` // info | warning | critical - ActionRoute string `json:"actionRoute,omitempty"` - Dismissible bool `json:"dismissible"` -} -``` - -Frontend переводит `key` через i18n. Никаких HTML-строк из backend. Сейчас `dangerouslyUseHTMLString` лучше убрать. - ---- - -## 16. Новый frontend layout Dashboard - -Создать: - -```text -src/views/dashboard/index.vue -src/api/dashboard/index.ts -src/api/dashboard/types.ts -src/views/dashboard/components/MetricCard.vue -src/views/dashboard/components/SecurityAlerts.vue -src/views/dashboard/components/SystemChart.vue -src/views/dashboard/components/TrafficChart.vue -src/views/dashboard/components/TopPeers.vue -src/views/dashboard/components/HysteriaStatus.vue -``` - -Структура страницы: - -```text -[Security alerts, если есть] - -[Hysteria2 status] [Online peers] [Online devices] [Traffic today] -[CPU] [Memory] [Disk] [Total peers] - -[Traffic chart: download/upload] -[System chart: CPU/RAM] -[Top peers by traffic] -[Recent peer activity / last connections] -``` - -Первый экран должен отвечать на вопросы оператора: - -```text -Сервис работает? -Есть ли пользователи онлайн? -Сколько трафика прошло? -Есть ли перегруз CPU/RAM/Disk? -Кто больше всего потребляет? -Есть ли security/config warnings? -``` - -То, что сейчас на `System Monitor`, можно сохранить как часть Dashboard, но карточки надо сделать полезнее: - -```text -CPU: 14.2%, load1 0.23 -Memory: 1.2 GB / 13.7 GB, 8.7% -Disk: 11.5 GB / 40 GB, 28.8% -Hysteria2: Running, v2.8.2 -``` - -Не только проценты. - ---- - -## 17. Графики: как сделать без костылей - -В проекте сейчас нет chart-библиотеки. Есть два нормальных варианта. - -Вариант A, практичный: добавить ECharts. Для админ-панелей это стандартное решение: line/area charts, tooltip, resize, нормальная работа с time-series. Компоненты графиков лучше lazy-load’ить, чтобы не раздувать initial bundle. - -Вариант B, минимальный: написать `Sparkline.vue` на SVG для CPU/RAM и traffic. Меньше зависимостей, но хуже tooltip, zoom, диапазоны и легенды. - -Для твоего кейса я бы выбрал ECharts, но не тащил бы его во все страницы. Только Dashboard chunk. - -Графики: - -```text -TrafficChart: - series: download, upload - ranges: 1h / 24h / 7d / 30d - source: traffic_aggregate_hourly/daily - -SystemChart: - series: cpuPercent, memPercent - ranges: 1h / 6h / 24h - source: metric_sample -``` - -Disk не нужно рисовать как частый line chart. Disk меняется медленно; достаточно карточки и, максимум, daily trend. - ---- - -## 18. Таблица системных метрик - -Добавить таблицу: - -```sql -CREATE TABLE metric_sample ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - sampled_at INTEGER NOT NULL, - - cpu_percent REAL NOT NULL DEFAULT 0, - load1 REAL NOT NULL DEFAULT 0, - - mem_used_bytes INTEGER NOT NULL DEFAULT 0, - mem_total_bytes INTEGER NOT NULL DEFAULT 0, - mem_percent REAL NOT NULL DEFAULT 0, - - disk_path TEXT NOT NULL DEFAULT '/', - disk_used_bytes INTEGER NOT NULL DEFAULT 0, - disk_total_bytes INTEGER NOT NULL DEFAULT 0, - disk_percent REAL NOT NULL DEFAULT 0, - - hysteria_running INTEGER NOT NULL DEFAULT 0, - online_peers INTEGER NOT NULL DEFAULT 0, - online_devices INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX idx_metric_sample_time ON metric_sample(sampled_at); -``` - -Retention: - -```text -raw metric_sample: 24–72 часа -minute aggregate: 7 дней -hour aggregate: 30–90 дней -daily aggregate: можно хранить дольше -``` - -SQLite нормально выдержит такие объёмы, если не писать каждую секунду и не хранить бесконечную raw-историю. - ---- - -## 19. Роутинг и меню - -Текущее: - -```text -/ -> /info/account -/info/account -/monitor/system -``` - -Целевое: - -```text -/ -> /dashboard -/dashboard -/peers -/hysteria -/config -/log/system -/log/hysteria -``` - -В `router/index.ts`: - -```ts -{ - path: "/", - component: Layout, - redirect: "/dashboard", - children: [...] -} -``` - -Новый route: - -```ts -{ - path: "/dashboard", - component: "Layout", - redirect: "/dashboard/index", - name: "Dashboard", - meta: { - title: "dashboard", - icon: "dashboard", - roles: ["admin"], - }, - children: [ - { - path: "index", - component: "dashboard/index", - name: "DashboardIndex", - meta: { - title: "dashboard", - icon: "dashboard", - roles: ["admin"], - }, - }, - ], -} -``` - -`Info` route удалить. `Monitor` route либо удалить, либо временно оставить redirect на `/dashboard/index` на один релиз. - ---- - -## 20. Что делать с бывшим `Account Info` - -Разложить по новым местам. - -Greeting и create time админа: - -```text -navbar dropdown / admin profile -``` - -Security warnings: - -```text -dashboard Security Center -``` - -Quota/download/upload/expire: - -```text -peer drawer -peer list compact cards -dashboard aggregate traffic -``` - -Subscribe/QR/Node URL: - -```text -peer detail только для конкретного peer -``` - -Default password warning: - -```text -dashboard + forced change-password flow -``` - -HTTPS warning: - -```text -dashboard security warning -``` - -Так исчезает главный conceptual bug: админ больше не выглядит как peer. - ---- - -## 21. Быстрые действия на Dashboard - -Добавить только operator actions, не peer actions: - -```text -Restart Hysteria2 -Open Hysteria config -Open peers -Open logs -Refresh metrics -``` - -Не добавлять на dashboard “Create peer” как главную кнопку, если страница перегружена. Лучше маленькая secondary action в блоке peers. - -Для dangerous actions — только confirm dialog: - -```text -Restart Hysteria2 -Reset all traffic -``` - -`Reset all traffic` я бы вообще не держал на Dashboard. Это административная операция, ей место в settings или peer management. - ---- - -## 22. Backend queries для агрегатов peer’ов - -После split-модели нужны DAO-методы: - -```go -CountPeersByStatus(now int64) (PeerSummary, error) -SumPeerTrafficSinceReset() (TrafficSummary, error) -TopPeersByTraffic(from, to int64, limit int) ([]TopPeerVo, error) -ListRecentPeerActivity(limit int) ([]PeerActivityVo, error) -``` - -SQL-логика: - -```sql -SELECT - COUNT(*) AS total, - SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) AS enabled, - SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) AS disabled, - SUM(CASE WHEN expires_at > 0 AND expires_at < ? THEN 1 ELSE 0 END) AS expired -FROM peer; -``` - -Traffic current period: - -```sql -SELECT - COALESCE(SUM(download_bytes), 0) AS download, - COALESCE(SUM(upload_bytes), 0) AS upload -FROM peer -WHERE disabled = 0; -``` - -Top peers из history: - -```sql -SELECT - peer_id, - SUM(rx_bytes) AS download, - SUM(tx_bytes) AS upload -FROM traffic_sample -WHERE sampled_at BETWEEN ? AND ? -GROUP BY peer_id -ORDER BY download + upload DESC -LIMIT ?; -``` - ---- - -## 23. Polling на frontend - -Первый production-safe вариант без WebSocket/SSE: - -```text -summary: каждые 5 секунд -timeseries: каждые 30 секунд -top peers: каждые 60 секунд -``` - -Использовать `useIntervalFn` из `@vueuse/core`, он уже есть в dependencies. - -Обязательно: - -```text -pause polling on unmount -pause polling when tab hidden -show stale badge if collectedAt older than N seconds -manual refresh button -``` - -Если позже захочешь realtime без polling — добавить SSE: - -```text -GET /hui/dashboard/events -``` - -Но я бы не начинал с SSE. Для такой панели polling проще, надёжнее и легче тестируется. - ---- - -## 24. Состояния ошибок - -Dashboard должен показывать не просто `-`, а причину. - -Примеры: - -```text -Hysteria2 stopped -Traffic API unreachable -Traffic API unauthorized -No traffic samples yet -Metrics collector stale -Disk path unavailable -``` - -Backend response: - -```go -type DataHealthVo struct { - Status string `json:"status"` // ok | stale | error - MessageKey string `json:"messageKey,omitempty"` - LastSuccessAt int64 `json:"lastSuccessAt,omitempty"` -} -``` - -Например: - -```json -{ - "hysteria": { - "running": true, - "apiReachable": false, - "lastApiError": "dashboard.error.trafficApiUnauthorized" - } -} -``` - -Frontend переводит через i18n. - ---- - -## 25. Acceptance criteria для Dashboard - -Готовность можно проверять так: - -```text -/ открывает /dashboard, а не /info/account. -В меню нет Account Info. -Admin profile не содержит quota, Node URL, Subscribe, QR. -Dashboard показывает Hysteria2 status, version, online peers/devices. -Dashboard показывает общий download/upload всех peer’ов. -Dashboard показывает traffic today и traffic since reset. -Dashboard показывает CPU/RAM/Disk не только процентом, но и used/total. -CPU/RAM графики строятся из history, а не из разовых live-запросов. -Traffic график не ломается после reset traffic. -Traffic accounting не умножает /traffic?clear=1 на polling interval. -Security warnings отображаются внутри Dashboard, не всплывающими HTML notification. -Если Hysteria2 API недоступен, Dashboard показывает degraded state, а не пустые карточки. -На 1366px первый экран читается без горизонтального скролла. -``` - ---- - -## 26. Приоритет внедрения - -Я бы добавил этот блок в общий roadmap так: - -```text -A. Security hotfix текущей account/peer модели. -B. Split admin_user / peer. -C. Исправить traffic accounting: убрать ошибочное умножение, добавить traffic_sample. -D. Создать Dashboard backend summary endpoint. -E. Перенести /info/account и /monitor/system в /dashboard. -F. Добавить графики и history retention. -G. Удалить legacy routes /info/account и /monitor/system. -``` - -Главное: не делать красивый Dashboard поверх старой таблицы `account`. Сначала развести admin и peer, затем строить дашборд на правильной доменной модели. Иначе ты просто перенесёшь старую путаницу в более красивый интерфейс. - -[1]: https://v2.hysteria.network/docs/advanced/Traffic-Stats-API/?utm_source=chatgpt.com "Traffic Stats API" -[2]: https://v2.hysteria.network/docs/advanced/Full-Server-Config/?utm_source=chatgpt.com "Full Server Config" diff --git a/fix24.2.txt b/fix24.2.txt deleted file mode 100644 index 9cc2aef..0000000 --- a/fix24.2.txt +++ /dev/null @@ -1,243 +0,0 @@ -Да, я бы убирал подписки полностью. В текущем состоянии это не недоделанная мелочь, а мёртвый функциональный слой, который создаёт ложное ожидание у оператора и путает UX. - -По текущему коду видно следующее: ordinary Node URL / Node QR строятся отдельно и полезны; subscription-кнопки в UI есть, но backend endpoint `Hysteria2SubscribeUrl` фактически возвращает ошибку `subscription delivery is out of scope in HY2XS baseline`. При этом в service ещё лежит старая логика генерации подписок для Clash/Shadowrocket/v2rayN, но она не подключена нормальным публичным delivery route. То есть это уже рудимент: фронт показывает кнопки, backend говорит “не поддерживается”, а пользователи всё равно подключаются через обычный URI/QR. - -Я бы добавил в общий план отдельный пункт. - ---- - -## 27. Удалить subscription layer как неподдерживаемый рудимент - -### Решение - -Оставить только: - -```text -Node URL -Node QR Code -Copy URI -Rotate Secret -``` - -Удалить: - -```text -Subscribe -Subscribe QR Code -Subscription URL -Clash subscription extension -Shadowrocket/V2rayN subscription delivery -/hysteria2/hysteria2SubscribeUrl -``` - -То есть один peer — один обычный Hysteria2 URI/QR. Этого достаточно для текущей панели, особенно если цель — упрощённая, зрелая 3x-ui-like админка, а не subscription gateway. - ---- - -## 28. Почему лучше удалить, а не чинить сейчас - -Подписка — это отдельный продуктовый слой, а не просто “ещё один QR”. - -Чтобы она была production-grade, нужны: - -```text -публичный endpoint доставки подписки; -форматы под разные клиенты; -нормальная авторизация или signed token; -TTL / revoke / rotate; -rate limit; -логирование выдачи; -обработка client type; -совместимость Clash/Shadowrocket/v2rayN; -безопасное сокрытие peer secret; -корректные HTTP headers; -контроль доступа после disable/expire peer; -тесты на каждый формат клиента. -``` - -Сейчас этого нет. Более того, подписочный URL по старой логике строится вокруг `conPass`, то есть секрет peer’а становится частью URL. Это хуже обычного QR, потому что URL может попасть в browser history, reverse-proxy logs, access logs, Referer, скриншоты и т.д. - -Обычный Node QR тоже содержит секрет, но он показывается авторизованному админу в панели для конкретного peer’а. Подписка же предполагает публичную доставку конфигурации по ссылке. Это другой threat model. - -Поэтому чинить подписки сейчас — не “доделать кнопку”, а открывать отдельный блок безопасности и совместимости. Для текущего scope это лишнее. - ---- - -## 29. Что удалить на backend - -Удалить или пометить deprecated с последующим удалением: - -```go -controller.Hysteria2SubscribeUrl -controller.Hysteria2Subscribe -service.Hysteria2SubscribeUrl -service.Hysteria2Subscribe -dto.Hysteria2SubscribeUrlDto -vo.Hysteria2SubscribeVo -``` - -Из router убрать: - -```go -hysteria2.GET("/hysteria2SubscribeUrl", controller.Hysteria2SubscribeUrl) -``` - -Если будешь делать новую REST-модель после разделения `admin_user` / `peer`, оставить только: - -```text -GET /hui/peers/:id/client-uri -GET /hui/peers/:id/client-qr -``` - -Или один endpoint: - -```text -GET /hui/peers/:id/client-config -``` - -Response: - -```json -{ - "uri": "hysteria2://...", - "qrCode": "base64..." -} -``` - -Важно: этот endpoint должен работать только под admin JWT. Никакой публичной выдачи по `conPass`. - ---- - -## 30. Что удалить на frontend - -Из `src/views/account/list/index.vue` убрать: - -```text -Subscribe -``` - -Из будущего peer drawer убрать: - -```text -Subscribe -Subscribe QR -``` - -Из бывшего `src/views/info/account/index.vue` это всё всё равно исчезнет вместе с самой страницей `Account Info`. - -Из `src/api/hysteria2/index.ts` убрать: - -```ts -hysteria2SubscribeUrlApi -``` - -Из `src/api/hysteria2/types.ts` убрать: - -```ts -Hysteria2SubscribeUrlDto -Hysteria2SubscribeVo -``` - -Из i18n убрать или оставить deprecated на один релиз: - -```text -common.subscribe -common.subscribeQrCode -config.clashExtension -``` - -Если `CLASH_EXTENSION` используется только для подписки, убрать его из UI настроек. В базе можно не удалять сразу, чтобы не ломать существующие SQLite-файлы. Лучше сделать миграцию так: - -```text -релиз N: поле скрыто, backend не использует; -релиз N+1: config key удаляется или игнорируется окончательно. -``` - ---- - -## 31. Что оставить вместо подписки - -В peer list / peer drawer оставить понятные действия: - -```text -Copy URI -Show QR -Rotate Secret -Edit -Reset Traffic -Kick -Disable -Delete -``` - -В detail drawer можно сделать блок: - -```text -Connection - URI: hysteria2://... - [Copy] - [Show QR] - [Rotate secret] -``` - -И короткий warning: - -```text -Rotating secret invalidates existing client configs. -``` - -Этого достаточно. Оператор создаёт peer, копирует URI или QR, отдаёт клиенту. Без псевдоподписок. - ---- - -## 32. Если когда-нибудь возвращать подписки - -Возвращать их стоит только как отдельную feature, не как восстановление старого кода. - -Нормальная модель: - -```text -subscription_token - id - peer_id - token_digest - expires_at - revoked_at - created_at - last_used_at -``` - -Публичный endpoint: - -```text -GET /sub/:token -``` - -Token должен быть random, revocable, не равен peer secret. В URL нельзя класть `conPass`. - -Формат: - -```text -GET /sub/:token?target=clash -GET /sub/:token?target=shadowrocket -GET /sub/:token?target=v2rayn -``` - -Но я бы сейчас этого не делал. Это увеличит scope, а ценность низкая, если обычные URI/QR уже закрывают основной сценарий. - ---- - -## 33. Acceptance criteria для удаления подписок - -```text -В UI нигде нет Subscribe / Subscribe QR. -В peer list нет действий, ведущих к subscription endpoint. -В API клиента нет hysteria2SubscribeUrlApi. -Backend router не регистрирует /hysteria2/hysteria2SubscribeUrl. -Service-код генерации подписок удалён или явно deprecated и недоступен. -Обычный Node URL и Node QR продолжают работать. -Админ не получает Node URL/QR для себя, только для peer’ов. -В настройках нет Clash subscription extension, если подписки отключены. -``` - -Итог: да, подписку лучше удалить. Не скрыть кнопку CSS’ом, не оставить endpoint с `Fail`, а именно вырезать из публичного UI/API. Это упростит продукт, уберёт ложные кнопки и снизит риск утечки peer secret через URL. diff --git a/fix24.txt b/fix24.txt deleted file mode 100644 index 545834d..0000000 --- a/fix24.txt +++ /dev/null @@ -1,617 +0,0 @@ -Посмотрел архив `HY2XS_flamy-main.zip`. Проблема не косметическая: сейчас в проекте смешаны две разные доменные сущности — администратор панели и peer Hysteria2. Это видно прямо по схеме `account`: в одной таблице лежат `username/pass` для входа в админку, `con_pass` для подключения, `quota/download/upload/device_no`, `role`, `deleted`. Логин панели фильтрует `role = 'admin'`, а Hysteria2 auth ищет только по `con_pass` и не отсекает `role = 'admin'`. Поэтому админ технически остаётся peer’ом. - -Ниже план нормального продакшен-исправления без «подмазать CSS и добавить if role != admin». - ---- - -## 1. Развести админов панели и peer’ов Hysteria2 - -### Текущее состояние - -Сейчас: - -`account.pass` — пароль входа в панель. -`account.con_pass` — пароль подключения Hysteria2. -`account.role` — попытка различать `admin/user`. -`account.quota/download/upload/device_no/expire_time` — peer-поля. -`login_at` — поле админа. -`con_at` — поле peer’а. - -Это плохая модель. Роль не должна превращать одну таблицу в две разные сущности. - -### Целевая модель - -Сделать минимум две таблицы. - -`admin_user`: - -```sql -id -username -password_hash -status -force_password_change -last_login_at -password_changed_at -token_version -created_at -updated_at -``` - -`peer`: - -```sql -id -name -remark -auth_id -secret_digest -secret_ciphertext -- если нужно показывать URL/QR после создания -quota_bytes -download_bytes -upload_bytes -expires_at -max_devices -disabled -banned_until -last_connection_at -created_at -updated_at -``` - -`admin_user` не должен иметь `quota`, `con_pass`, `device_no`, `download`, `upload`. - -`peer` не должен иметь пароль входа в панель и `role`. - -### Как хранить peer secret - -Текущий `con_pass` хранится как plain text. Для продакшена лучше уйти от этого. - -Вариант нормальный: - -`auth_id` — публичный идентификатор peer’а, например короткий random/base32. -`raw_secret` — генерируется при создании или ротации. -`secret_digest = HMAC-SHA256(raw_secret, HY2XS_PEER_SECRET_KEY)` — используется для auth lookup. -`secret_ciphertext` — опционально, если UI должен уметь повторно показать Node URL/QR. Шифровать ключом из data-dir/env, не хранить просто строкой в SQLite. - -Если не хочется вводить шифрование сейчас, допустим компромисс: хранить `secret_plain` временно, но уже в таблице `peer`, не рядом с admin password. Потом отдельной миграцией заменить на digest/encrypted secret. - -### Миграция - -Сделать версионированные миграции, а не держать огромную строку SQL внутри `dao/sqlite.go`. - -Сейчас есть два источника схемы: `apps/docs/sql/h_ui_db.sql` и inline `sqlInitStr` в `apps/dao/sqlite.go`. Они уже расходятся: в `sqlite.go` добавлен `force_password_change`, в SQL-доке его нет; `remark` объявлен как `INTEGER DEFAULT ''`, хотя в Go это `string`. Это надо убрать. - -Нужен один механизм: - -```sql -schema_migrations(version, applied_at) -``` - -Миграции: - -`001_initial_legacy_snapshot.sql` — текущая схема, только для reference. -`002_admin_peer_split.sql` — создаёт `admin_user`, `peer`. -`003_migrate_legacy_accounts.sql` — переносит данные. -`004_drop_or_archive_legacy_account.sql` — не сразу удалять, а переименовать в `legacy_account_backup`. - -Правила переноса: - -`role = 'admin'` → `admin_user`. Переносить `username`, `pass`, `force_password_change`, `login_at`. Не переносить `con_pass`. - -`role != 'admin'` → `peer`. Переносить `username` как `name`, `remark`, лимиты, трафик, `expire_time`, `kick_util_time`, `con_at`. - -После миграции admin credentials больше не могут пройти Hysteria2 auth даже теоретически. - ---- - -## 2. Переписать backend-слой по доменам - -### Что заменить - -Сейчас всё сидит в `account.go`: DAO, service, controller, DTO, VO. Надо разделить: - -```text -dao/admin_user.go -dao/peer.go - -service/auth.go -service/admin_user.go -service/peer.go -service/hysteria2_auth.go - -controller/auth.go -controller/admin_user.go -controller/peer.go -controller/hysteria2.go -``` - -### API - -Оставить `/hui/auth/login`, но он должен работать только с `admin_user`. - -Добавить: - -```text -GET /hui/admin/me -POST /hui/admin/change-password - -GET /hui/peers -POST /hui/peers -GET /hui/peers/:id -PATCH /hui/peers/:id -DELETE /hui/peers/:id - -POST /hui/peers/:id/reset-traffic -POST /hui/peers/:id/kick -POST /hui/peers/:id/release-kick -POST /hui/peers/:id/rotate-secret -GET /hui/peers/:id/client-url -GET /hui/peers/:id/qr -``` - -Старые `/account/*` можно оставить только как compatibility layer на один релиз, но UI уже должен ходить в `/peers/*`. - -### Hysteria2 auth - -Сейчас `Hysteria2Auth()` делает: - -```go -dao.GetAccount("con_pass = ? and deleted = 0 ...") -``` - -Нужно заменить на peer-auth: - -```go -peer, err := peerRepo.FindBySecretDigest(digest) -``` - -И проверять только peer-поля: - -```text -disabled = false -now < expires_at -quota_bytes < 0 OR quota_bytes > download_bytes + upload_bytes -now > banned_until -online_devices < max_devices -``` - -Admin-таблица здесь вообще не импортируется. - -### Сессии и JWT - -JWT сейчас содержит `AccountBo` с `Roles`. Оставить можно, но лучше переименовать в `AdminClaims`. - -Добавить `token_version` в `admin_user`. Тогда смена пароля, reset или принудительная инвалидизация токенов делается увеличением `token_version`. - -После смены пароля обязательно сбрасывать `force_password_change = 0`. Сейчас флаг возвращается из логина, но нормального dedicated flow для смены admin password не видно. - -### Reset command - -`apps/cmd/reset.go` сейчас меняет row `id=1` в `account`, печатает ещё и `Connection Password`. После разделения: - -```text -hy2xs-admin reset-admin -``` - -Должен менять только `admin_user`. - -Никакого connection password для админа печатать нельзя. - ---- - -## 3. UI: переименовать “Account Manage” в peer management - -В интерфейсе сейчас название “Account” вводит в заблуждение. Там смешаны профиль админа и peer’ы. Нужна терминология: - -```text -Профиль / Admin Profile -Пиры / Peers -Управление пирами / Peer Management -``` - -В форме создания peer убрать поле “Пароль входа”. Для peer нужен только generated/rotatable connection secret. - -Нормальная форма создания peer: - -```text -Комментарий -Имя / label -Квота -Срок действия -Лимит устройств -Статус -[Создать] -``` - -После создания показать одноразовый блок: - -```text -Connection URI -QR -Copy -Сохраните сейчас: после закрытия секрет может быть скрыт -``` - -Если оставляете encrypted secret, можно показывать URL и позже. - ---- - -## 4. Починить русскую локаль и “плывущие” иконки правильно - -### Причина - -Это не проблема русского языка как такового. Русские строки длиннее, а sidebar сейчас завязан на дефолтный layout Element Plus плюс ручные margin’ы: - -```scss -.svg-icon { margin-right: 16px; } -.hideSidebar .el-sub-menu__title { padding: 0 !important; } -.hideSidebar .svg-icon { margin-left: 20px; } -``` - -Из-за этого при длинных заголовках, collapse/open state и sub-menu иконки начинают жить отдельно от текста. - -### Исправление - -В `SidebarItem.vue` обернуть title в отдельный span: - -```vue - - {{ translateRouteTitleI18n(...) }} - -``` - -Для `el-menu-item` и `el-sub-menu__title` задать один стабильный layout: - -```scss -.sidebar-container { - .el-menu-item, - .el-sub-menu__title { - display: flex; - align-items: center; - gap: 12px; - height: 48px; - line-height: normal; - padding: 0 16px !important; - } - - .svg-icon { - flex: 0 0 18px; - width: 18px; - height: 18px; - margin-right: 0; - } - - .menu-title { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .el-sub-menu__icon-arrow { - margin-left: auto; - } -} -``` - -Убрать ручные `margin-left: 20px` для collapsed state. Для collapsed меню текст скрывать штатно, а не через плавающие отступы. - -Ширину sidebar лучше поднять с `210px` до `240px` или `248px`. Но это вторично. Основной фикс — стабильная flex/grid-разметка. - -Для длинных пунктов включить tooltip с полным названием при hover. Не переносить текст на вторую строку внутри sidebar. - ---- - -## 5. Перевести уведомления и ошибки без хардкода - -Сейчас часть UI переведена через `$t`, но много строк осталось захардкоженными: - -```text -"Required" -"Username format is incorrect" -"Are you sure to reset traffic?" -"Are you sure to delete..." -"Warning" -"A week later" -"file format not supported" -``` - -Плюс backend возвращает английские строки: - -```text -wrong password -system error -permission denied -username already exists -admin cannot be deleted -``` - -### Frontend - -Добавить namespace: - -```ts -validation: { - required, - usernameInvalid, - passwordInvalid, - peerSecretInvalid, - integer, - number, - fileFormat, - fileTooLarge, -} - -confirm: { - warning, - deletePeer, - resetTraffic, - restartPanel, -} - -timeShortcut: { - hourLater, - dayLater, - weekLater, - monthLater, - yearLater, -} -``` - -Все validation rules сделать через `computed`, чтобы при смене языка сообщения обновлялись: - -```ts -const dataFormRules = computed(() => ({ - username: [ - { required: true, message: t("validation.required"), trigger: ["blur"] }, - ], -})) -``` - -Подключить `ElConfigProvider` в `App.vue` и прокидывать locale Element Plus: - -```vue - - - -``` - -Иначе встроенные компоненты Element Plus, datepicker, pagination и popconfirm будут жить своей локалью. - -### Backend - -Не возвращать UI-текст как источник истины. Возвращать стабильный error code/message key: - -```json -{ - "code": 40010, - "type": "no", - "message": "peer.usernameAlreadyExists", - "params": { "username": "testuser" } -} -``` - -Frontend переводит `message` как i18n key. Если key неизвестен — fallback на `common.systemError`. - -Для machine endpoint `/hysteria2/auth` локализация не нужна, там протокольный ответ `{ ok: true/false, id }`. - ---- - -## 6. Сжать таблицу peer’ов до production-вида - -Сейчас таблица перегружена. В `account/list/index.vue` одновременно выводятся: - -```text -ID -Remark -Username -Role -Quota -Download -Upload -Online status -Online devices -Device limit -Offline remaining time -Expire time -Last login time -Last connection time -Create time -Status -Operate -``` - -Для peer list это слишком много. На широком экране оно всё равно не будет хорошо читаться. - -### Основной список - -Оставить в таблице только: - -```text -Peer -Status -Traffic -Devices -Expires -Last connection -Actions -``` - -Где: - -`Peer` — имя + remark + ID мелким текстом. -`Status` — enabled/disabled + online/offline. -`Traffic` — progress bar: used / quota, а upload/download спрятать в details. -`Devices` — online / max. -`Expires` — дата + “expired soon/expired” tag. -`Last connection` — дата или `-`. -`Actions` — 1–2 основные кнопки и dropdown. - -### Details drawer / overview dialog - -По клику “Обзор” открыть drawer: - -```text -Peer overview -- ID -- Name -- Remark -- Created at -- Updated at -- Download -- Upload -- Quota -- Expires at -- Last connection -- Kick until -- Device limit -- Current online devices -- Node URL -- QR -``` - -Это лучше, чем прятать половину в троеточие без структуры. - -### Actions - -В таблице оставить: - -```text -Copy URL -Edit -⋯ -``` - -В dropdown: - -```text -Show QR -Reset traffic -Kick -Release kick -Rotate secret -Disable / Enable -Delete -``` - -`Subscribe` сейчас в baseline отключён, значит в UI его лучше не показывать, пока delivery layer вне scope. Иначе оператор видит кнопку, которая всегда ведёт к ошибке. - -### Поведение на малых экранах - -На desktop можно оставить `el-table`. - -На tablet/mobile лучше отдельный card layout, а не пытаться ужать таблицу. Например: - -```text -Peer card -name / status -traffic progress -devices -expires -actions -``` - ---- - -## 7. Конкретный порядок работ - -### Этап A. Быстрый security hotfix - -Это временный фикс, не финальная архитектура. - -1. В `Hysteria2Auth` добавить фильтр `role = 'user'`. -2. В `PageAccount` по умолчанию показывать только `role = 'user'`. -3. В `SaveAccount` явно ставить `role = 'user'`. -4. Запретить выдачу Node URL/QR для `role = 'admin'`. -5. В `reset.go` убрать вывод connection password. - -Это закрывает самый опасный баг до большой миграции. - -### Этап B. Нормальная доменная миграция - -1. Добавить `schema_migrations`. -2. Создать `admin_user`. -3. Создать `peer`. -4. Перенести legacy data. -5. Переписать DAO/service/controller. -6. Оставить legacy `account` только как backup. -7. Добавить `reset-admin`. -8. Убрать `role` из peer flow. - -### Этап C. UI/i18n - -1. Переименовать раздел `Account Manage` → `Peer Manage`. -2. Убрать peer login password из формы. -3. Сделать i18n keys для всех validation/confirm/toast строк. -4. Подключить Element Plus locale provider. -5. Убрать backend English strings из UI-отображения. -6. Добавить проверку отсутствующих i18n keys в CI. - -### Этап D. Layout/sidebar - -1. Переписать sidebar CSS на flex/grid. -2. Убрать ручные margin hacks. -3. Добавить `.menu-title` с ellipsis. -4. Поднять sidebar width до 240–248px. -5. Проверить RU/EN в expanded/collapsed состояниях. - -### Этап E. Peer table redesign - -1. Сделать компактные колонки. -2. Вынести details в drawer. -3. Перенести второстепенные действия в dropdown. -4. Скрыть subscription actions, если delivery layer отключён. -5. Добавить responsive card layout. - ---- - -## 8. Тесты и критерии готовности - -Backend tests: - -```text -admin_user может войти в панель -admin_user не может пройти Hysteria2 auth -peer не может войти в панель -peer может пройти Hysteria2 auth -expired peer rejected -disabled peer rejected -quota-exceeded peer rejected -device-limit peer rejected -legacy migration переносит admin и peers корректно -``` - -Frontend checks: - -```text -pnpm lint:eslint -pnpm build:prod -vue-tsc --noEmit -``` - -E2E/screenshot: - -```text -RU sidebar expanded -RU sidebar collapsed -EN sidebar expanded -EN sidebar collapsed -Peer table 1366px -Peer table 1920px -Peer card/mobile layout -``` - -Acceptance criteria: - -```text -Админ панели не отображается в списке peer’ов. -У админа нет Node URL, QR и connection password. -Peer не имеет password для входа в UI. -Все toast/confirm/validation сообщения переводятся. -Русская локаль не ломает sidebar. -Peer list не требует горизонтального скролла на 1366px. -Второстепенные peer-поля доступны через “Обзор”. -``` - -Главная мысль: не чинить это через `role` и CSS-отступы. Правильный продакшен-фикс — разделить admin identity и peer identity на уровне схемы, сервисов и UI. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.