# 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.