# Smart Home Tablet — Handoff Документ для следующей сессии. Где что лежит, как деплоить, что сделано. --- ## Что это Дашборд для планшета на стене: `https://tablet.digital-home.site`. Next.js 14 App Router, TypeScript, standalone Docker build. Контейнер живёт на сервере `cosmo@192.168.31.60`, образ собирается self-hosted Gitea Actions runner'ом и пушится через `docker stop/rm/run` (без compose для этого сервиса). --- ## Где файлы ### Локально (рабочая копия) ``` /tmp/tablet-work/smart-home-tablet/ ``` Это git-clone из `https://git.digital-home.site/daniil/smart-home-tablet`. **Ремоут уже настроен с токеном в URL** — `git push origin main` работает без пароля. ### На сервере (192.168.31.60) ``` /opt/digital-home/smart-home-tablet/ # код (что задеплоено) /opt/digital-home/smart-home-tablet-data/ # JSON-стораджи (volume → /data в контейнере) ├─ tablet-notes.json ├─ tablet-timers.json └─ tablet-countdowns.json /opt/digital-home/tablet.env # секреты (HA_TOKEN, ELEVENLABS_*, VOICE_API_KEY, и т.д.) ``` ### Workflow деплоя ``` /opt/digital-home/smart-home-tablet/.gitea/workflows/deploy.yml ``` Триггер: push в `main`. Делает `git pull → docker build → stop/rm/run`. Контейнер: `tablet-yfh53kixpwkjlo4zibglx4n2` на порту 3006, traefik роутит `tablet.digital-home.site` → 3000 внутри. --- ## Как деплоить ```bash cd /tmp/tablet-work/smart-home-tablet # правки... NODE_OPTIONS="--max-old-space-size=4096" npm run build # tsc на дефолтном heap падает OOM git add -A && git commit -m "..." && git push origin main ``` Дальше Gitea Actions сам пересоберёт. Как проверить, что задеплоилось: ```bash ssh -i ~/.ssh/id_ed25519 cosmo@192.168.31.60 \ "docker ps --filter name=tablet --format '{{.CreatedAt}}'" curl -sk -o /dev/null -w "%{http_code} %{time_total}s\n" https://tablet.digital-home.site/ ``` Свежий `CreatedAt` = новый билд поднялся. --- ## SSH ```bash ssh -i ~/.ssh/id_ed25519 cosmo@192.168.31.60 ``` Ключ `~/.ssh/id_ed25519` авторизован. **Не** `daniil@192.168.31.103` — туда с этой машины ключ не положен. --- ## Голосовой ассистент Tablet — **только UI**. Слой, отвечающий за wake-word/STT/LLM, живёт **снаружи** (на 192.168.31.103, плюс ElevenLabs через прокси `192.168.31.103:8888`). Поток событий: ``` Внешний Python-агент ──POST──▶ /api/voice/event (auth: VOICE_API_KEY) │ ▼ voiceBus (in-memory EventEmitter) │ ▼ Браузер ◀──SSE──── /api/voice/stream │ ▼ ``` ### Файлы голосового стека (в этом репо) ``` app/api/voice/ ├─ event/route.ts # POST: принимает {event, agent, text} от Python-агента ├─ stream/route.ts # GET (SSE): отдаёт события в браузер ├─ tts/route.ts # POST: проксирует в ElevenLabs (cosmo / lusya голоса) ├─ timer/route.ts # голос-операции с таймерами └─ tools/ # tool-calls от LLM (открыть таб, поставить таймер, ...) lib/voice-bus.ts # EventEmitter, шарится между event и stream lib/voice-tools.ts # описание tools для LLM components/VoiceOverlay.tsx # Siri-style орб, рендерит wake/listening/command/response/error ``` Состояния (см. `VoiceOverlay.tsx`): `idle | wake | listening | command | response | error`. Два агента (`cosmo` / `lusya`) — каждому своя цветовая пара (фиолетовая / розовая) и свой ElevenLabs voice ID. Голоса в `tablet.env`: `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE`. Агент сам шлёт `idle` когда закончил; в overlay есть safety-таймер 60с на случай падения агента. **Где сам Python-агент** — на `192.168.31.103` (рабочая машина пользователя). Этот репо его не содержит и не деплоит. --- ## Структура UI ``` app/ layout.tsx, page.tsx # Home: погода + комнаты + транспорт + ноуты + календарь globals.css # дизайн-токены: --surface-*, --data-*, --space-*, утилиты .num/.grain/... api/ auth/ ha/ weather/ transport/ notes/ calendar/ tasks/ savings/ countdowns/ voice/ components/ TopBar.tsx # часы, погода-чип, HA-статус (44px hit-zone), сенсор-чип RoomTabs.tsx # переключатель Home/Зал/Кухня/Спальня DeviceCard.tsx # лампы/тв/кондей/пуриф — accent по типу из DEVICE_ACCENT TransportWidget.tsx # трамваи, цвета через --data-good/info/danger WeatherAnimation.tsx # SVG-анимация по condition NotesTab.tsx # стикеры (drag-to-trash, --data-* цвета) CalendarTab.tsx # 3 модалки, у всех data-swipe-ignore TimerHomeWidget.tsx, TimerModal.tsx, TimerWidget.tsx Sidebar.tsx VoiceOverlay.tsx # см. секцию выше FocusCard.tsx # ⚠ написан, но НЕ используется на Home (см. ниже) CountdownCard.tsx # ⚠ написан, но НЕ используется на Home ``` ### Свайп между табами В `app/page.tsx` — handler на `
` через `onPointerDown/Up`, ref `swipeStart`. Условия срабатывания: `|dx|>|dy|*1.6 && |dx|>90 && (dt<600 || |dx|>160)`. Bail-out если `e.target.closest('[data-swipe-ignore]')` — этот атрибут стоит на всех fixed-overlay (TimerModal, VoiceOverlay, модалки CalendarTab, weather day modal, draggable note). --- ## Что было сделано в прошлой сессии 1. **CSS-рефакторинг** — `app/globals.css`: добавлены семантические токены `--data-cool/info/good/warm/hot/danger/rose/violet/mood` + их `-bg` варианты через `color-mix(in srgb, ...)` для тёмной и `.light` темы. `--space-1..6`, `--touch-min: 44px`, `--touch-comfy: 56px`. Утилиты: `.num`, `.num-display` (tabular-nums slashed-zero), `.grain` (SVG-noise через `::after`), `.hit-zone`, `.eyebrow`, `.focus-card`, `.divider`. 2. **Tap-targets подняты до 44px** — TopBar (HA-dot обёрнут в hit-zone), NotesTab (плюс/удалить), CalendarTab (close/nav/list), TimerHomeWidget. 3. **Иконки/цвета** через токены вместо хардкода — TransportWidget (route.color = var(--data-*)), DeviceCard (DEVICE_ACCENT по типу), TopBar-сенсоры. 4. **Свайп между табами** — добавлен в `app/page.tsx`, c bail-out по `[data-swipe-ignore]`. 5. **Forecast day buttons / WeatherDayModal** — увеличены тапы и шрифты. 6. **API `/api/countdowns`** — GET/POST/PUT/DELETE, бэкенд `/data/tablet-countdowns.json`. Дефолтная запись: Токио 2026-10-15. 7. **FocusCard.tsx + CountdownCard.tsx** написаны, но **с Home убраны** по фидбеку: пользователю не понравилось, что виджет погоды стал меньше трамваев и пропали `feelsLike` / `humidity` / `WeatherAnimation`. Сейчас на Home — старый weather-hero с display-цифрой 76px, фоновой `WeatherAnimation size=180` (opacity 0.14), inline `WeatherAnimation size=60`, и тремя `.eyebrow + .num` блоками снизу (Ощущается/Влажность/Ветер). ### Что НЕ делать без явной просьбы - **Не возвращать FocusCard на Home** и **не показывать CountdownCard на Home** — пользователь явно отверг. - **Не делать виджеты Habits и Pulse** — пользователь сказал "пока что их не добавляем". ### Что осталось как orphan-файлы (есть, но не подключено) - `components/FocusCard.tsx` — context-engine с приоритетами bill-due / event-upcoming / tram-imminent / morning-outfit / countdown / night / quiet. Можно использовать в будущем, но Home не место. - `components/CountdownCard.tsx` — ротация записей из `/api/countdowns` каждые 8с. --- ## Известные грабли - **`tsc --noEmit` ловит OOM** на дефолтном heap — всегда `NODE_OPTIONS="--max-old-space-size=4096"`. - **`lucide-react`**: нет экспорта `Tram`, есть `TramFront`. Грепать `node_modules/lucide-react/dist/lucide-react.d.ts` если сомневаешься. - **Git push с локальной машины**: ремоут уже c токеном, не сбрасывать (`git remote -v` покажет `https://@git.digital-home.site/...`). Если потерялся — взять с сервера: `ssh cosmo@192.168.31.60 'cat /opt/digital-home/smart-home-tablet/.git/config'`. - **Volume `/data`** в контейнере — это `/opt/digital-home/smart-home-tablet-data/` на хосте. JSON-сторадж (notes/timers/countdowns) переживает редеплой только потому, что лежит там. --- ## Последние коммиты сессии - `e328055` — большой CSS / tap-target / swipe / FocusCard / CountdownCard / countdowns API - `a97dd11` — revert Home: вернул weather-hero с анимацией и деталями, убрал CountdownCard с главной Оба задеплоены, прод 200 OK.