Files
pwa-homelab-mon/REVIEW.md
T
alexey.bagno fed9ad594c
Deploy / deploy (push) Successful in 9s
Added MOCK=1 mode for local dev
2026-08-31 18:45:53 +05:00

5.3 KiB
Raw Blame History

Ревью SPA/PWA-части целиком (App.tsx, docsStore, св, карточки, деплой-сборка). Не безопасность — архитектура и баги.

Критично: крах всего приложения на одном битом доке

docsStore.get() делает JSON.parse(raw) без try/catch. Одна повреждённая запись в localStorage (частичная запись при переполнении квоты, ручная зачистка, битая сериализация) — и localDocs() кидает исключение прямо в рендере (useState(localDocs())) → белый экран всего дашборда, не только вкладки Docs. Лечится одним try/catch в get(), возвращающим null. Это единственное место, где SPA может умереть целиком.

Свежесть данных невидима

.catch(() => {}) в поллинге: офлайн → UI молча показывает последние значения. Для панели за VPN это главный UX-дефект: tailscale отвалился — ты минутами смотришь на CPU 12%, который был час назад. Ошибки мониторов попадают в значение (error: ... в тексте), а вот пропажа связи — нет. Ленивый фикс: lastOkAt timestamp рядом с content, если Date.now() - lastOkAt > 10s — один бейдж «offline» в шапке.

Гонка ответов при медленном VPN

setInterval(refresh, 3000) без коалесинга и отмены: при таймаутах/лагах два запроса в полёте, ответ на старый приходит позже нового — setContent откатывает данные назад. На 3s-такте и tailscale-латентности реально. Один guard (reqSeq или AbortController) — три строки. Сюда же: connect fetch без таймаута, на мёртвом туннеле запросы копятся.

Дублирование транспорт-клиента ×4

createClient(HomelabService, createConnectTransport({ baseUrl: "" })) скопирован в App, ServiceCard, ScriptCard и docsStore. Один api.ts с экспортом client — и правка транспорта (позже auth через caddy) в одном месте, а не четырёх. Это ровно то место, которое выстрелит, когда будешь прикручивать caddy-auth.

Docs-синк: три дыры в модели

  • Призраки: удалённый на сервере док живёт в localStorage вечно — pull удалять не умеет. Список только растёт.
  • Вечный ретрай: dirty-док, который сервер отклоняет, пушится каждые 5s бесконечно, без счётчика и без сигнала юзеру.
  • Черновики уезжают на сервер: каждое нажатие клавиши → localStorage → через ≤5s запушено. Второе устройство видит полуфразы; если на нём нет правок — оно перезапишет твой док, когда ты его допишешь (last-write-wins уже задокументирован, но «push недописанного» делает конфликты чаще). Дешёвый фикс: пушить только доки, к которым не было правок N секунд (debounce).

Service worker: лишняя ручная работа

CACHE = "homelab-shell-v4" с ручным бампом версии — мёртвый груз: кэшируются только content-hashed /assets/, а activate и так удаляет старые кэши. Имя можно не менять никогда. Обновление PWA при этом работает (network-first index + skipWaiting) — тут всё нормально, включая iOS-пункт про stale index.

Мелочи

  • Роутинга нет: вкладка — useState. Рефреш/OSError выкидывает на Monitor, глубокой ссылки нет. Осознанный YAGNI — ок, но одна строка location.hash вернула бы back-button на iOS, который в standalone-PWA иначе мёртв (свайп назад закрывает приложение).
  • String(e).replace(/^.*real error: /, "") — парсинг ошибок по строке. У connect-ошибок есть .message; регексп отвалится при первом изменении текста на сервере.
  • error && String(e) в ScriptCard без setError("") при успехе... — есть, при успехе сбрасывается. Ок.
  • Search в сервисах без debounce — при 30 карточках не нужно, не трогать.
  • Модалки (info/output) без focus-trap и Escape — для личной панели терпимо, одна строка onKeyDown на document закрыла бы Escape.