Commit Graph

103 Commits

Author SHA1 Message Date
Cosmo
2143ccadab fix: allow spotify OAuth routes without auth cookie
All checks were successful
Deploy / deploy (push) Successful in 1m35s
2026-05-01 11:13:04 +00:00
Cosmo
c43bad1fc3 fix: use docker rm -f to avoid stop/rm conflict in CI
All checks were successful
Deploy / deploy (push) Successful in 4s
2026-05-01 11:10:56 +00:00
Cosmo
9bea298687 feat: Spotify integration (OAuth + voice tools)
Some checks failed
Deploy / deploy (push) Failing after 1m40s
2026-05-01 11:02:11 +00:00
Cosmo
ae7edbe6ed fix: smart-home tool syntax error in else branch
All checks were successful
Deploy / deploy (push) Successful in 1m52s
2026-05-01 07:00:01 +00:00
Cosmo
89d8140014 feat: smart-home voice tools (get_state + control_air_purifier)
Some checks failed
Deploy / deploy (push) Failing after 1m18s
2026-05-01 06:57:43 +00:00
Cosmo
7b5f76576f refactor: tool plugin registry - each tool in separate file
All checks were successful
Deploy / deploy (push) Successful in 1m25s
2026-04-30 20:58:11 +00:00
Cosmo
4ba1aa43d5 feat: switch voice chat from Anthropic to Groq (llama-3.3-70b) + proxy support
All checks were successful
Deploy / deploy (push) Successful in 1m26s
2026-04-30 20:48:02 +00:00
Cosmo
04b7d1f104 feat: switch from Anthropic to Groq API (llama-3.3-70b-versatile)
All checks were successful
Deploy / deploy (push) Successful in 2m47s
- route.ts: replace @anthropic-ai/sdk with groq-sdk, rewrite chat loop
- voice-tool-schemas.ts: convert from Anthropic format to OpenAI/Groq function tools
- voice-history.ts: extend HistoryMessage type to include tool role, simplify cache stubs

No prompt caching (Groq does not support it), tool calling preserved.
2026-04-30 20:43:30 +00:00
Cosmo
96fa78bd5c fix(calendar): GOOGLE_SA_JSON_B64 поддержка (env-file friendly)
All checks were successful
Deploy / deploy (push) Successful in 2m7s
docker --env-file не поддерживает многострочные значения и не парсит
кавычки. Сырой JSON service-account ломается на newline'ах в private_key
поле → docker пытается парсить '-----END PRIVATE KEY-----' как имя
переменной и валится с 'contains whitespaces'.

Решение: base64 GOOGLE_SA_JSON_B64 (одна строка ASCII, никаких кавычек).
Старая GOOGLE_SA_JSON оставлена как fallback. Третий fallback на файл —
для локальной разработки.
2026-04-27 12:59:17 +00:00
Cosmo
be377e377f chore: ignore google-sa.json в git и docker context
Some checks failed
Deploy / deploy (push) Failing after 1m35s
2026-04-27 12:52:47 +00:00
Cosmo
d17e1232f1 security: убираем google-sa.json из репо и Docker image
Some checks failed
Deploy / deploy (push) Has been cancelled
Service Account ключ больше не распространяется через git/image —
читается из env GOOGLE_SA_JSON, которая выставлена в /opt/digital-home/
tablet.env на сервере.

- google-sa.json удалён из git tracking (git rm --cached)
- Добавлен в .gitignore + .dockerignore
- На сервере после git pull файл будет удалён с диска;
  /api/calendar читает credentials из env (env-first fallback уже был
  в коде). Это критично если bundle/image куда-то утечёт — ключ Google
  больше не лежит внутри.
2026-04-27 12:52:36 +00:00
Cosmo
05b300d472 chore(voice): security, cleanup, resilience
All checks were successful
Deploy / deploy (push) Successful in 1m47s
Безопасность:
- Rate-limit на /api/voice/chat (20/мин per cookie/IP, env VOICE_RATE_LIMIT).
  Защищает от случайных циклов и утечки PIN.
- Усечение user prompt'а до 4000 символов в /api/voice/chat.
- Tool-loop защита от циклов: если LLM дважды просит тот же tool с теми же
  args — прерываем (раньше мог уйти в бесконечный цикл при tool error'ах).

Чистка кода:
- lib/debug.ts — vlog/vwarn/verror гейтят браузерные логи за
  NEXT_PUBLIC_VOICE_DEBUG=1 (или localStorage 'voice-debug=1').
  Серверные console.log оставлены — полезны в Docker logs.
- lib/audio-wav.ts — вынесена дублированная floatToWav из VoiceController.
- Удалены orphan компоненты FocusCard.tsx и CountdownCard.tsx
  (не подключены, отвергнуты по UX-фидбеку).

Resilience:
- WakeWordDetector: drop-on-busy в onChunk — на медленных устройствах
  (Android, бюджетный CPU) backlog inference больше не копится.
- voice-history fallback на /tmp/voice-history если /data не примонтирован
  (локальная разработка / нестандартная конфигурация).
2026-04-27 12:44:18 +00:00
Cosmo
3211d62198 fix(voice): COOP/COEP=credentialless → SharedArrayBuffer на iOS
All checks were successful
Deploy / deploy (push) Successful in 1m52s
VAD на iPad валился с «no available backend found / RangeError».
onnxruntime-web 1.24 ships только threaded WASM сборки, которые
требуют SharedArrayBuffer. iOS Safari его не даёт без cross-origin
isolation — отсюда и ошибка.

Добавляем заголовки:
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: credentialless

credentialless (а не require-corp) выбрали чтобы не ломать внешние
ресурсы без CORP-заголовка (Google Fonts, wttr.in). Поддержка:
Chrome 96+, Firefox 110+, Safari 16.4+.
2026-04-27 11:30:08 +00:00
Cosmo
6083597065 fix(voice): TTS играет через AudioContext (фикс для iPad Safari)
All checks were successful
Deploy / deploy (push) Successful in 1m45s
iOS Safari блокирует <audio>.play() даже после silent-WAV unlock —
каждый new Audio() считается новым элементом без gesture.

Решение: при тапе кнопки в VoiceController создаём общий
AudioContext (под user-gesture) и пробуждаем его. VoiceOverlay
теперь играет TTS через этот ctx (decodeAudioData + BufferSource).
HTMLAudioElement остаётся fallback'ом если ctx недоступен.

decodeAudioData в Safari исторически callback, в Chrome — Promise:
используем оба варианта.
2026-04-27 11:17:41 +00:00
Cosmo
6c3992bb4e fix(voice): audio unlock на тапе → TTS играет на Android
All checks were successful
Deploy / deploy (push) Successful in 2m1s
Android Chrome требует user-gesture для <audio>.play(). Wake-word
триггерит TTS «сам», без тапа, поэтому play() тихо отвергался.

При тапе на кнопку микрофона теперь проигрываем 1мс silent WAV →
браузер помечает страницу как разрешённую для autoplay в текущей
сессии. Дальше TTS-ответы Cosmo/Lusya играют без проблем.

В VoiceOverlay логируем причину если play() всё ещё отвергнут.
2026-04-27 11:12:27 +00:00
Cosmo
f74b10ba38 fix(voice): откатить VAD preload — мешал wake-word ловить «Космо»
All checks were successful
Deploy / deploy (push) Successful in 1m51s
Параллельный getUserMedia от MicVAD конфликтует со stream'ом wake-word —
видимо Chrome применяет AGC/NS по-разному и wake получает «глухое» аудио.
Score упал с 0.988 до 0.093 — wake перестал срабатывать.

Возвращаемся: VAD создаётся ПОСЛЕ первого wake (~1-2с пауза),
но cancel/onSpeechEnd теперь только pause (не destroy), так что
повторные wake мгновенные.
2026-04-27 11:03:09 +00:00
Cosmo
7e3c5072bb fix(voice): preload VAD один раз — мгновенная реакция после «Космо»
All checks were successful
Deploy / deploy (push) Successful in 1m36s
В логах было видно: между wake-trigger и реальным VAD recording
проходило 1-2с (Loading VAD... → finished loading → started micVAD).
Каждый cancel дополнительно destroy'ил VAD, и следующий wake снова
ждал инициализацию.

Теперь:
- VAD создаётся один раз в paused-режиме сразу после wake.start()
  (в фоне, не блокирует UI).
- На каждый wake → vad.start() мгновенно.
- onSpeechEnd → vad.pause() (был implicit pause; явно ставим).
- voice-cancel → vad.pause(), а не destroy. Wake продолжает слушать.
- destroy только при полном выключении ассистента.
2026-04-27 10:55:18 +00:00
Cosmo
fddca5de66 fix(wake-word): cosmo.onnx ждёт окно из 25 embedding'ов, не 16
All checks were successful
Deploy / deploy (push) Successful in 1m56s
Из ORT-ошибки:
  onnx::Flatten_0 index 1 Got: 16 Expected: 25.

Стоковые openWakeWord модели тренируются на окне 16, наша cosmo.onnx —
на 25. Меняем EMB_WINDOW. Окно для embedding-буфера тоже подвинули,
чтобы хватало для classifier'а.
2026-04-27 10:42:59 +00:00
Cosmo
9583c84e27 feat(voice): кнопка X в overlay закрывает прослушивание
All checks were successful
Deploy / deploy (push) Successful in 2m13s
В overlay появляется крестик в правом верхнем углу. Тап = эмитит
voice-cancel → VoiceController прерывает активный VAD-захват и сам
overlay закрывается. Wake-word, если был активен, продолжает слушать
в фоне.
2026-04-27 10:25:21 +00:00
Cosmo
0ea9fad144 debug(voice): mount + tap логи в консоль
All checks were successful
Deploy / deploy (push) Successful in 2m14s
2026-04-27 10:16:17 +00:00
Cosmo
d7accb5602 ci: install docker-cli-buildx so BuildKit cache mounts work
All checks were successful
Deploy / deploy (push) Successful in 2m57s
2026-04-27 10:01:04 +00:00
Cosmo
463043b43b build: BuildKit cache mounts → CI 3-5× быстрее
Some checks failed
Deploy / deploy (push) Failing after 2s
Текущая сборка ~7 минут — npm ci качает все 183 пакета с нуля + next
build без incremental cache на каждом push.

- Dockerfile: BuildKit cache mount на /root/.npm (npm install) и
  .next/cache (Next.js incremental TS/swc/webpack cache).
- workflow: DOCKER_BUILDKIT=1 чтобы cache mounts работали.
- .dockerignore: убираем node_modules/.next/.git/docs из контекста —
  меньше копирования + чище инвалидация слоёв.
- Поднимаем NODE_OPTIONS heap до 4G в build-стадии (TS падал с OOM).

Первый билд после этого коммита заполнит cache — второй и дальше
будут значительно быстрее (deps install ~5-10с вместо 60-90с,
next build тоже ускорится за счёт incremental).
2026-04-27 09:59:21 +00:00
Cosmo
71124ce565 debug(voice): verbose logging для wake-word pipeline
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-04-27 09:57:56 +00:00
Cosmo
522d36d1a2 feat(voice): wake-word «Космо» в браузере (Шаг 3)
All checks were successful
Deploy / deploy (push) Successful in 6m33s
openWakeWord pipeline на onnxruntime-web прямо на планшете. Цепочка:
mic (16kHz, AudioWorklet) → melspectrogram.onnx → embedding_model.onnx
(sliding 76-frame window, stride 8) → cosmo.onnx → score 0..1.

Триггер при score≥0.5 → запускается тот же VAD-flow что и push-to-talk.

- public/wake/ — cosmo.onnx (custom-trained на голос Даниила) +
  melspectrogram.onnx + embedding_model.onnx (~2.9MB вместе).
- lib/wake-word.ts — WakeWordDetector class. ort грузится через
  <script src=/vad/ort.wasm.min.js> на клиенте — обход проблемы next-swc
  с парсингом import.meta.url в onnxruntime-web .mjs билдах.
- VoiceController: тап = активация (нужен для AudioContext user-gesture),
  далее непрерывное слушание wake-word; на детект → MicVAD флоу.
  Долгий тап = выкл. Ручной тап остаётся как fallback.

После деплоя Python-агент на .103 не нужен — можно архивировать
home-voice-assistant. На .103 остаётся только ElevenLabs прокси :8888.
2026-04-27 09:43:53 +00:00
Cosmo
96bd846a08 fix(voice): ship non-asyncify ort-wasm + force single-thread
All checks were successful
Deploy / deploy (push) Successful in 3m46s
В public/vad/ были только asyncify-варианты, а onnxruntime-web по дефолту
просит ort-wasm-simd-threaded.{mjs,wasm} → 404 → MicVAD init falls.

- Положили ort-wasm-simd-threaded.{mjs,wasm} рядом.
- ortConfig forces numThreads=1, чтобы не требовать SharedArrayBuffer
  (нет COOP/COEP headers и не хотим их вешать на весь сайт).
- Раздельный getUserMedia probe перед VAD init, чтобы отличить отказ
  по микрофону от ошибки VAD/wasm в UI-сообщении.
2026-04-27 09:01:52 +00:00
Cosmo
93bf34f216 feat(voice): push-to-talk button — браузерный mic+VAD pipeline
All checks were successful
Deploy / deploy (push) Successful in 6m53s
Шаг 2 миграции: убираем зависимость от Python-агента для базового
голосового сценария. Тап на круглую кнопку-микрофон в правом нижнем
углу → MicVAD (Silero v5) ловит речь → автостоп по тишине → /api/voice/stt
→ /api/voice/chat → ответ через SSE и TTS как раньше.

- components/VoiceController.tsx — push-to-talk UI + MicVAD orchestration
- VoiceOverlay теперь слушает window CustomEvent('voice-local'), чтобы
  орб моргал ещё до round-trip на сервер (wake/listening мгновенно).
- public/vad/ — silero v5/legacy onnx + ort wasm + audio worklet,
  раздаются через baseAssetPath: '/vad/' (не зависит от внешнего CDN,
  важно если планшет без интернета или с RU-блоком).

Что осталось от home-voice-assistant: только wake-word. После Шага 3
(onnxruntime-web + перенос openwakeword .onnx) Python-агент уйдёт целиком.
2026-04-27 08:48:22 +00:00
Cosmo
eeac2eefb3 feat(voice): server-side LLM/STT — porting Python satellite into tablet
All checks were successful
Deploy / deploy (push) Successful in 5m44s
Шаг 1 миграции голосового стека из home-voice-assistant в сам tablet:

- /api/voice/chat — Claude Haiku 4.5 с tool-loop (max 4 раунда), prompt
  caching на system + старой истории, история в /data/voice-history/.
  Эмитит command/response/error в voice-bus → орб моргает как раньше.
- /api/voice/stt — Groq whisper-large-v3-turbo, multipart или raw audio.
- lib/voice-text.ts — порт clean_for_speech (без pymorphy3, время в
  именительном падеже) и strip_fillers + RESET_PATTERNS.
- lib/voice-executors.ts — tool executors через loopback fetch на
  существующие /api/voice/tools/* и /api/voice/timer.
- Поддержка ANTHROPIC_PROXY/GROQ_PROXY (fallback на HTTPS_PROXY).

После деплоя нужны GROQ_API_KEY и ANTHROPIC_API_KEY в tablet.env.
Шаги 2 (push-to-talk в браузере) и 3 (wake-word) — отдельно.
2026-04-27 08:24:19 +00:00
Cosmo
a97dd11f25 revert(home): restore weather hero with anim + details, drop Countdown from home
All checks were successful
Deploy / deploy (push) Successful in 3m24s
Focus/Countdown не лежат по high-density. На Home вернул старый
weather-hero: WeatherAnimation (фон + иконка), feelsLike/humidity/wind,
76px display-цифра (чуть крупнее прежних 64). FocusCard и CountdownCard
файлы оставляем для будущего, но на главной не подключаем. Убрал
сопутствующие state/fetch (tramNext, countdowns, nextEvent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:35:01 +00:00
Cosmo
e328055851 feat(design): FocusCard hero, CountdownCard, data-* palette, swipe, touch-targets
All checks were successful
Deploy / deploy (push) Successful in 3m8s
Big design pass across Home + tokens + components.

— globals.css: new data-* palette (cool/warm/hot/good/info/rose/violet/mood)
  with theme-aware variants, .grain overlay utility, .num-display
  typography helper, .hit-zone 44px wrapper, .eyebrow label, .focus-card
  base, focus-visible outline-offset 3px, space/touch scale vars.
— FocusCard.tsx: context engine — пять состояний (morning-outfit,
  tram-imminent, event-upcoming, countdown, bill-due, night, quiet).
  Auto-rotates by hour + live data. 96px display numbers, accent-mixed
  surfaces, grain overlay.
— CountdownCard.tsx + /api/countdowns: rotating 8s list, persistent
  /data/tablet-countdowns.json, full CRUD. Default seeded with Токио.
— HomeTab: replaced plain Weather hero with FocusCard, added Row 4
  with CountdownCard. Pulls trams + countdowns for the Focus context.
— Swipe between tabs: pointer-level detection on <main>, data-swipe-ignore
  bails out inside modals + note swipe-to-delete + voice overlay.
— Touch-target sweep: TopBar HA dot → 44px hit-zone, sensor chip 44px
  min-height, forecast day buttons 92px min, DeviceCard toggle 60x36,
  CalendarTab prev/next/close/list all 44x44, NotesTab buttons 44x44,
  TimerHomeWidget + 44x44, WeatherDayModal chevrons 48x48, close 48.
— Hardcoded hex → data-* tokens: TopBar sensors, TransportWidget routes
  (via color-mix), DeviceCard full rewrite (per-kind accent, glass
  removed in favor of color-mix surfaces + proper mock-state treatment),
  NotesTab palette refreshed to match dark theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:24:23 +00:00
Cosmo
f78daffd5b feat(timers): tap-to-open modal with big touch targets + create via UI
All checks were successful
Deploy / deploy (push) Successful in 3m23s
Tablet touch targets were cramped (30px +/- buttons). Swap to modal UX:

- New TimerModal component handles both 'control existing' and
  'create new'. 84px countdown, 56px min button height, 3×2 adjust
  grid (-5m, -1m, -10s, +10s, +1m, +5m), big destructive cancel/stop.
  Create view has 7 duration presets (1..60 min), label input with
  shortcut chips (Чайник, Паста, Яйца…), gradient 'Запустить' CTA.
- TimerHomeWidget cards are now full tap targets — open control modal
  on press. + button in header opens create modal. Inline buttons
  removed. Subtle hint text 'тап чтобы изменить'.
- Ticking countdown inside modal stays fresh via shared timers state:
  while modal is open we look up the current timer in active[] by id
  and re-pass to modal on every tick. When timer disappears
  (cancelled / expired >30s), modal auto-closes.
2026-04-23 16:34:51 +00:00
Cosmo
56844a539d feat(voice/events): full CRUD — POST/PUT/DELETE with owner routing
All checks were successful
Deploy / deploy (push) Successful in 2m57s
Голосовой ассистент теперь может создавать, изменять и удалять события
в календарях Даниила и Светы.

- POST  /api/voice/tools/events — create (title, date, start_time, end_time,
  all_day, owner). Маппит owner (daniil/sveta) в calendar_id и проксирует
  в /api/calendar POST.
- PUT   — update (event_id, owner, fields). Передаёт только изменённые
  поля + нужный calendarId.
- DELETE ?event_id=X&owner=Y — удаление.
- GET   — теперь возвращает id события и owner (daniil/sveta), чтобы
  скрипт мог их передать в update/delete.
- range=month поддержан с year/month query params.

Все три метода под bearer auth (VOICE_API_KEY), как остальные voice tools.
Loopback к /api/calendar идёт через internalHeaders() x-voice-internal.
2026-04-23 14:34:32 +00:00
Cosmo
fa583cd279 fix(timer): dismiss actually cancels on server + shorter retention
All checks were successful
Deploy / deploy (push) Successful in 3m10s
Bug: после перезагрузки страницы оверлей «Таймер прозвенел» открывался
снова и снова. Две причины:

- dismissTimer в TimerWidget удалял таймер только из локального
  useState, но /data/tablet-timers.json оставался нетронутым. После
  reload таймер возвращался в список и firedRef (которая пустая после
  reload) снова триггерила alarm.
- lib/timers.ts держал просроченные таймеры 30 минут, давая им шанс
  повторно сработать при каждом reload в этом окне.

Фикс:
- dismissTimer теперь POST /api/voice/timer {action:cancel, id} через
  cookie auth (endpoint с прошлого коммита принимает и cookie, и bearer).
- Retention в listActive снижена до 30 секунд — этого хватает чтобы
  клиент увидел свежий звонок; старше = самоудаление.
- TimerWidget клиентский фильтр тоже 30 секунд.
2026-04-23 13:58:53 +00:00
Cosmo
e2b2a5d82f fix(voice): dismiss overlay after TTS ends, show listening state for followups
All checks were successful
Deploy / deploy (push) Successful in 3m11s
Two fixes:

1) Overlay was hiding mid-TTS because dismiss timer used
   text.length * 80ms — ElevenLabs speaks slower, so the audio got
   cut off. Now scheduleDismiss is only called from playTTS's
   onEnded callback (plus 4s lingering after audio finishes).

2) After response, the Python script silently re-entered record()
   for follow-ups but the overlay disappeared, so the user had to
   re-wake every turn. Added a new 'listening' event — Python
   emits it just before each followup record(), tablet shows the
   orb pulsing at medium intensity with 'жду' status and the last
   response text preserved below.

Safety: any state now arms a 60s auto-close in case Python dies
and never emits idle.
2026-04-23 13:55:25 +00:00
Cosmo
0c677df558 feat(voice): hero TimerHomeWidget + timer cancel/adjust by label
All checks were successful
Deploy / deploy (push) Successful in 3m25s
UI:
- Replace Notes column on Home bento with TimerHomeWidget. Shows all
  active timers as stacked cards with big 30px countdowns, per-timer
  +1/-1 minute buttons and cancel. Colors: indigo default, amber in
  last 10s, red when expired. Empty state suggests voice command.
- Existing chip TimerWidget (bottom-right) kept for ambient view on
  other tabs — redundant on Home, but harmless.

API:
- /api/voice/timer accepts cookie OR bearer (browser widget cancel
  works with user's auth_token cookie; Python script uses bearer).
- New action 'adjust' — shifts endsAt by delta_seconds. Clamps so
  endsAt never goes into the past.
- Cancel now supports {label} in addition to {id} (fuzzy substring
  match, most-recently-started wins). Emits timer_cancel with id+label
  so clients can refresh.
- findByLabel / adjustTimer helpers in lib/timers.ts.
2026-04-23 13:51:25 +00:00
Cosmo
7fb05181e6 fix(voice/tools): use x-voice-internal header for loopback fetches
All checks were successful
Deploy / deploy (push) Successful in 3m10s
Tool endpoints (events, notes, transport, weather) call other /api/*
routes via loopback (http://localhost:3000). Those routes are
middleware-protected — cookie-less loopbacks were getting 401, which
surfaced to the voice agent as get_today_events → tool_http_502.

Add internal header bypass: middleware lets the request through when
x-voice-internal matches VOICE_API_KEY. Only our own tool endpoints
use this header, from inside the same container, so the blast radius
is limited to loopback traffic.

- middleware.ts: check x-voice-internal before cookie
- lib/voice-tools.ts: internalHeaders() helper
- app/api/voice/tools/{weather,transport,events,notes}: use it
2026-04-23 13:41:57 +00:00
Cosmo
e96e7a1342 feat(voice): tool endpoints, timer widget, clean Siri-style overlay
All checks were successful
Deploy / deploy (push) Successful in 3m18s
Adds the infrastructure for Claude tool use + visual timer.

Tablet API surface (all bearer-authed with VOICE_API_KEY, middleware bypassed):
- /api/voice/tools/weather    — current + short forecast via Open-Meteo
- /api/voice/tools/transport  — tram arrivals by direction / route filter
- /api/voice/tools/events     — Google Calendar today/week
- /api/voice/tools/notes      — notes + shopping lists
- /api/voice/timer            — start (with seconds+label), cancel; GET list (cookie ok)
  Active timers persisted at /data/tablet-timers.json

UI:
- VoiceOverlay stripped to minimal Siri look: no agent emoji/name, just the
  pulsing orb (3-layer radial gradient, independent breath animations),
  subtle status label on wake only, transcription/response text centered.
  Agents distinguished by orb color (Cosmo indigo/violet, Люся pink).
- TimerWidget: bottom-right chip stack with countdown, progress bar, turns
  amber in last 10s. On expiry, fires fullscreen alarm overlay with beep
  (WebAudio osc) + Остановить button.

Other:
- lib/timers.ts — persistent timer store in /data
- lib/voice-tools.ts — shared bearer-auth helper
- middleware — bypass list now covers /api/voice/tools/* and /api/voice/timer
2026-04-23 13:33:31 +00:00
Cosmo
c29da75c19 feat(voice/tts): route ElevenLabs through HTTP proxy for non-RU egress
All checks were successful
Deploy / deploy (push) Successful in 4m3s
ElevenLabs Cloudflare returns 302 to a region-restricted help page
when requested from a Russian IP. Tablet host (.60) is in RU, so the
Stage 2 call was failing with 502 upstream.

Fix: use https-proxy-agent when ELEVENLABS_PROXY (or generic HTTPS_PROXY
/ HTTP_PROXY) env var is set. Tinyproxy on .103 (non-RU egress host)
acts as the tunnel.

- package.json: add https-proxy-agent ^7.0.6
- app/api/voice/tts: switch from global fetch to node:https with
  explicit Agent (either direct or HttpsProxyAgent). Still streams
  MP3 back via Readable.toWeb so Next.js Response pipes it to the
  browser as audio arrives.

Operational: set ELEVENLABS_PROXY=http://192.168.31.103:8888 in
tablet.env after bringing tinyproxy up on .103.
2026-04-23 13:00:55 +00:00
Cosmo
a780fc7bd5 feat(voice): play TTS through tablet speakers via ElevenLabs proxy
All checks were successful
Deploy / deploy (push) Successful in 2m58s
Stage 2 of voice integration — centralizes TTS on the tablet so the
Python satellite no longer needs ElevenLabs credentials or mpv.

- app/api/voice/tts — POST {text, agent}, proxies to ElevenLabs
  streaming endpoint with flash_v2_5 default, returns audio/mpeg.
  Per-agent voice id via COSMO_TTS_VOICE / LUSYA_TTS_VOICE env.
- VoiceOverlay — on response/error events fetches TTS and plays via
  HTMLAudioElement; on wake event stops playback (barge-in). Dismiss
  timer extended by text length so long responses do not cut off.
- Autoplay caveat: browser may block first playback until user taps
  anywhere on the page (FKB: enable Force Autoplay to bypass).
2026-04-23 12:52:26 +00:00
Cosmo
51c3d6016a feat(voice): SSE bridge + Siri-blob overlay for wake-word script
All checks were successful
Deploy / deploy (push) Successful in 3m12s
Adds the tablet side of voice assistant integration. External Python
script (openWakeWord + Groq STT + OpenClaw) will POST state transitions
to /api/voice/event with a bearer token, and the tablet shows a
fullscreen overlay with Siri-style animated blob + current agent +
recognized text / response text.

- lib/voice-bus.ts — in-process EventEmitter singleton, preserved
  across hot reloads via globalThis
- app/api/voice/event — POST, bearer-auth via VOICE_API_KEY env,
  validates event kind, broadcasts on voiceBus
- app/api/voice/stream — GET, SSE endpoint, per-connection listener
  with 15s keep-alive ping and abort-signal cleanup
- components/VoiceOverlay — full-screen overlay, 3-layer pulsing
  Siri blob, per-agent palette (cosmo indigo/violet, lusya pink/rose),
  auto-dismiss timeouts (wake=20s safety, response=6s, error=4s),
  auto-reconnect on SSE drop
- middleware bypasses /api/voice/event so the script does not need
  a user auth cookie
- VoiceOverlay mounted in HomePageInner outside tab routing so it
  appears on every view
2026-04-23 12:36:26 +00:00
Cosmo
9fec9bca99 fix(calendar): all-day end.date must be next day, clear opposite field
All checks were successful
Deploy / deploy (push) Successful in 2m50s
Google Calendar API rejects all-day events where start.date == end.date
(end is exclusive). POST/PUT were sending the same date for both,
producing Invalid start time when toggling Весь день on edit.

- Added nextDayISO helper (UTC-safe +1d arithmetic)
- all-day: start = { date }, end = { date: nextDay, dateTime: null }
- timed: also explicitly nulls start.date/end.date so patching a
  timed-only event over a previously all-day one doesnt leave stale
  date fields that also trigger Invalid start time
2026-04-23 09:29:21 +00:00
Cosmo
dc5c9b3673 fix(home): remove black flash between tab switches
All checks were successful
Deploy / deploy (push) Successful in 2m40s
- AnimatePresence mode=wait keeps the DOM empty while the outgoing tab
  finishes its exit transition (200ms) before mounting the incoming
  tab. On touch devices this shows as a brief black frame — reported
  as экран остается черным.
- Switch to mode=sync: outgoing fades out while incoming fades in,
  no gap. initial=false suppresses the enter animation on first render.
- Drop the y: 12 → -8 slide (cheap jank on hydration), keep just
  opacity. Duration 200ms → 150ms for snappier feel.
2026-04-23 09:22:55 +00:00
Cosmo
8d32e7ebb0 feat: forecast swipe nav, note swipe-to-delete, night-shift tint
All checks were successful
Deploy / deploy (push) Successful in 2m45s
- WeatherDayModal now accepts the full forecast array and an onChange
  callback; supports horizontal drag (framer-motion) plus prev/next
  chevrons and a dot-indicator. Drag > 60px switches day; style uses
  semantic tokens (shadow-xl, surface-1).
- NotesTab list items wrap each note in a motion.button with drag=x,
  constrained to -80px. Below it a gradient+trash reveal layer. Drag
  past 60px opens the existing confirmDelete modal.
- HomePageInner adds a night-shift overlay (fixed, mixBlendMode multiply,
  rgba(255,120,40,0.12)) active 22:00-06:00, auto-checked each minute,
  fades in/out over 800ms. No user toggle yet — fully automatic.
2026-04-23 09:17:22 +00:00
Cosmo
0908ad93de fix(home): prevent bento grid overflow on narrow viewports
All checks were successful
Deploy / deploy (push) Successful in 2m40s
CSS grid items default min-width to min-content; the tram widget 3-col
subgrid plus its баbadges and long затем text forced its cell wider
than 1.1fr, collapsing the outer layout on tablet. Fixes:

- outer bento row → gridTemplateColumns: minmax(0, 1fr) minmax(0, 1.1fr)
- events+notes row same treatment
- TransportWidget inner subgrids: 58px → 52px badge, 1fr → minmax(0, 1fr)
- Cell: minWidth: 0, overflow: hidden, затем text trimmed with ellipsis
  and short м suffix (5м instead of 5 мин)
- big number 32→28px, badge 22→20px to fit in denser columns
2026-04-23 08:46:00 +00:00
Cosmo
e967924f1f ui(topbar): move greeting from Home body to TopBar center
All checks were successful
Deploy / deploy (push) Successful in 2m43s
The big Доброе утро line on Home ate one row for marginal value.
Moved it into TopBar as a center column via 3-col grid (1fr auto 1fr),
so it appears on all tabs and leaves the Home body denser.
2026-04-23 08:38:41 +00:00
Cosmo
09185c2a2a fix(home): drop greeting date; fix touch scroll; tokenize TopBar icons
Some checks failed
Deploy / deploy (push) Has been cancelled
- Removes the date label from the greeting row (user request — тoo dense
  on tablet and redundant with TopBar clock).
- Scrolling: drops overflowY: auto on the inner Events/Notes cards and
  removes flex: 1 / minHeight: 0 from their grid. On iPad-class touch
  nested scroll containers fought the root scroll; now only the root
  scrolls, which the browser handles natively.
- TopBar: replaces hardcoded rgba(255,255,255,X) on sensor icons, chip
  background, chip border and header bottom-border with semantic tokens
  (--text-tertiary, --surface-2, --border-subtle, --hairline) so the
  thermometer/humidity/wind icons are visible in light theme.
2026-04-23 08:36:56 +00:00
Cosmo
121bf30ab1 redesign: bento home + semantic tokens + solid cards
All checks were successful
Deploy / deploy (push) Successful in 2m43s
- introduces semantic CSS tokens (--surface-1/2/3, --border-subtle/strong,
  --hairline, --shadow-sm/md/lg/xl) with distinct dark and light values;
  fixes broken light theme caused by hardcoded rgba(255,255,255,X)
- drops glassmorphism on cards — solid var(--surface-1) with 1px border
  and layered shadows; glass kept only for aurora page background
- introduces .card/.card-raised/.card-hero utility classes
- Home page restructured into a bento grid:
  * greeting row with inline day/date
  * hero weather (64px number, large icon, ощущается/влажность/ветер)
    next to the tram widget (1fr 1.1fr)
  * forecast as a single hairline-separated band (no per-day cards)
  * events+notes in a 2-column grid; events card combines today and
    tomorrow with a divider; notes card styled via surface tokens
- TransportWidget repainted to use tokens, larger numbers (32px for the
  next arrival), imminent highlight uses color-mix against surface-2
2026-04-23 08:30:03 +00:00
Cosmo
9ad758174d style(home): drop weather-hint block; recolor trams 23 green, 27 blue, 39 red
All checks were successful
Deploy / deploy (push) Successful in 2m47s
Weather hint (оденьтесь потеплее / не забудьте зонт) was pushing the
home screen past one viewport on the tablet — removed the block and its
helper fn. New tram color palette per user preference.
2026-04-23 08:21:09 +00:00
Cosmo
43dff776f5 fix(transport): use node:https instead of undici (module not exported)
All checks were successful
Deploy / deploy (push) Successful in 2m47s
Next.js could not resolve undici as a top-level import even though it
ships internally. Drop that path and call the ORGP endpoint via the
built-in node:https with a per-request Agent(rejectUnauthorized: false).
Adds runtime = nodejs on the route so Node APIs are guaranteed.
2026-04-23 08:13:52 +00:00
Cosmo
c25e15e697 fix(transport): accept ORGP self-signed cert via undici Agent
Some checks failed
Deploy / deploy (push) Failing after 1m20s
ORGP SPb uses a TLS chain Node rejects by default (curl works with -k
but Node fetch doesnt). Use an undici Agent with rejectUnauthorized
false for this one hop. Also drop the conflicting next.revalidate: 0
option — cache: no-store already covers it.
2026-04-23 08:11:16 +00:00
Cosmo
95352356b7 refactor(transport): group by route with two direction columns
Some checks failed
Deploy / deploy (push) Has been cancelled
Restructures the tram widget: instead of one card per stop (showing all
routes at that stop), now one row per route (23, 27, 39) with two
columns — → Лента (в центр) and → Дыбенко (от центра). Each cell shows
the next arrival prominently plus the following 1-2 pickups inline.
2026-04-23 08:09:44 +00:00