# Ревью 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.