47 lines
5.3 KiB
Markdown
47 lines
5.3 KiB
Markdown
# Ревью 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.
|