Commit Graph

40 Commits

Author SHA1 Message Date
kami 9ffefd9e8c maven: gofmt handlers_test.go (fix stale alignment)
The Task-7 verification commit (d52f60c) added the CalendarEvents mock
method but left fakeCore's struct block misaligned, so `gofmt -l` still
flagged this file despite the "all gates green" claim. Realign it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:49:03 +04:00
kami cda614d1bb maven: fix нью-йорк transliteration in weather location map 2026-07-06 11:01:49 +04:00
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00
kami 880715fad4 maven: long-term memory vector-store interface (task 7)
- New internal/memory/ package: Store interface, InMemoryStore (cosine sim)
- Tests: insert→search roundtrip, topK truncation, empty store, cosine edges
- Wire into voice: memStore on reactiveHandler, insert note embedding after WriteNote
- Memory Insert is best-effort, log-and-continue on error

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:18:50 +04:00
kami e030466cac maven: weather module skeleton with open-meteo provider (task 5)
- New internal/weather/ package: Provider interface, Weather struct, StubProvider
- OpenMeteoProvider with geocoding + current weather (keyless, free API)
- Config: WeatherConfig in VoiceConfig (provider, default_location)
- Wire in voice.go as weatherProvider on reactiveHandler
- Handle weather queries in IntentQuery (before notes RAG)
- Helper: isWeatherQuery / extractWeatherLocation
- Tests: mocked HTTP round-trip for OpenMeteo, stub ErrNotConfigured, config tests
- No real network calls in any test

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:15:54 +04:00
kami 428af3f3c6 maven: general-knowledge phraser routing (task 4)
- Stub.PhraseQuery: empty notes → "не знаю." (was: "no notes")
- LLMPhraser.PhraseQuery: empty notes → general knowledge prompt to LLM
- Voice handler: on notes RAG failure, try phraser before giving up
- New router.KnowledgePrompt() pure function with test

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:11:54 +04:00
kami cf066bde97 maven: calendar event querying (task 3)
Store: CalendarEvents(ctx, from, to) — filters caldav facts by key date prefix.
IPC: full wiring through interface, server dispatch, and client.
Router: ParseCalendarDate (сегодня/завтра), CalendarEventFormatter (RU reply).
Voice: calendar detection before notes RAG in IntentQuery handler.
Tests: store integration test, date parser tests, formatter tests.

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:09:47 +04:00
kami b778f0bbec maven: embedder config validation + docs (task 1)
- Tests: all-three-set → ok; one-missing → error; nil → ok
- Log message on HashEmbedder fallback in voice.go
- Document floor mode in START.md

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:02:32 +04:00
kami 311c5cb1cd mavweb: ui design system refresh — shared nav, cards, component classes
- Rewrote ui.css with design tokens, card/btn/badge/dot components
- All pages wrapped in <main class=page> with max-width container
- Replaced inline <style> blocks with ui.css classes
- passkey page now uses shared nav.site template
- PWA voice page unified under shared nav.site (no more separate tab nav)
- Inline lang toggle moved from nav to voice page body
2026-07-06 00:41:16 +04:00
kami 2506327b62 progress: document voice-assistant gaps; theme the destructive checkbox
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
2026-07-05 18:47:46 +04:00
kami d7c0cf89d3 mavweb: unify UI — shared ui.css + nav partial across all pages
One theme (the PWA's dark palette) for dash/history/trace/notifications/
tools/passkey via static/ui.css; shared nav template with active-page
highlight; tables wrapped in .scroll so they pan on phones; PWA nav no
longer clips the RU/EN toggle; dash 'updated' timestamp fixed (selector
matched nothing). AGENTS.md documents the local preview/screenshot recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
2026-07-05 18:36:31 +04:00
kami 8d823000d1 fix: code-review findings on overnight-jul5
- digest queue no longer dropped on failed dispatch (retry next tick)
- collapsed reminders marked fired/rescheduled only after digest delivers
- /tools step-up gate skipped when WebAuthn is not configured (was 403 forever)
- passkey credential store rolls back memory on persist failure
- auth_test fake updated for TickTrace (branch build break)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
2026-07-05 17:50:21 +04:00
kami 5c34fb14f9 mavweb: add notifications history page at /notifications
RecentNudges IPC method, store adapter, dispatch, and client proxy.
Web UI at /notifications showing recent nudge history with color-coded
outcomes, nav links from /dash and /history.
2026-07-05 13:20:24 +04:00
kami 85013f7b8b mavweb: add rule trace page at /trace
New template showing the most recent tick's rule evaluation results
with color-coded table, expandable gate detail panels, nav links
to dash and history. Uses the TickTrace IPC method from #15.
2026-07-05 13:17:47 +04:00
kami 2689db1248 loop: add rule trace/explanation engine
ExplainTick and ExplainGate produce a full TickTrace for every tick,
recording per-rule: predicate result, gate result, first gate blocker,
gate detail (snooze/cooldown/presence etc.), and win/loss info.

IPC layer: new MethodTickTrace, DTOs (TickTrace, RuleTrace, GateDetail),
dispatch, client proxy, and CoreAPI interface method.

Daemon: tickLoop caches the latest trace; daemonAPI wraps storeAPI
with a TickTrace override that returns the cached trace.

Tests: 15 new tests for ExplainGate (all blockers, bypass conditions,
ordering) and ExplainTick (nothing fires, one fires, tiebreak,
winner/lost_to recording, gate-blocked recording).
2026-07-05 13:14:58 +04:00
kami 354990fed0 nudge: add digest/batching mode for care nudges
When enabled, eligible nudges (sev ≤ ceiling) are queued in memory
instead of sent immediately. Every 'window' duration (or when
'max_items' reached), the queue is flushed as a single digest
notification with concatenated bodies.

Config:
  digest:
    enabled: true           # default false
    window: 30m             # flush window (default 30m)
    max_items: 5            # flush at this count (default 5)
    severity_ceiling: 2     # max sev batched (default 2; sev3+ bypass)

Changes:
- config.go: add DigestConfig struct with defaults
- tick.go: QueuedNudge type, digest queue/flush in TickLoop,
  shouldQueue/maybeFlush/flushDigest helpers
- main.go: pass cfg.Digest to newTickLoop
- tick_test.go: 6 new tests covering queue, flush, bypass, dedup
2026-07-05 13:07:28 +04:00
kami 5afff001c3 mavweb: add in-process auth gate for POST /tools
Add a local PasskeySession that handleTools checks before processing
any POST action (enable/disable). If the session hasn't been asserted
within the 5-minute TTL, return 403 Forbidden.

Changes:
- webauthn/session.go: add IsStepUp() convenience method (nil-safe)
- webauthn.go: PasskeyHandle holds a *PasskeySession; AssertFinish
  calls session.Assert() after IPC step-up
- main.go: create stepUpSession, pass to handleTools and
  newPasskeyHandle; handleTools returns 403 if !session.IsStepUp()
- handlers_test.go: update TestEnableTool_NoInProcessAuthGate to
  expect 403; add TestEnableTool_WithAuthGate_RequiresStepUp for
  the happy path with asserted session; update all 10 call sites
2026-07-05 11:55:44 +04:00
kami c225ba37b2 pwa: add bilingual lang toggle to cheatsheet
Split cheatsheet into separate RU/EN <dd> elements with CSS class
toggle. Toggle buttons in nav bar (RU/EN). Language also controlled
by ?lang=ru|en query parameter (default: ru).

The rest of the PWA stays English; voice responses remain Russian.
2026-07-05 11:50:57 +04:00
kami 00a3bba9cb pwa: add app icon SVG and update manifest
Create a minimal SVG icon (512x512, dark rounded square with blue 'M')
and update manifest.json to reference it with purpose 'any'.

This fixes the blank tile on mobile add-to-home-screen and PWA install
prompts.
2026-07-05 11:49:44 +04:00
kami 1eca17f37b reminders: add recurring reminder support
Add cron expression support for recurring reminders using robfig/cron/v3.

Changes:
- Migration #2: ALTER TABLE reminders ADD COLUMN cron TEXT + next_fire_ts INTEGER
- Reminder struct: add Cron and NextFireTs fields
- scanReminder helper extracts full row including nullable cron
- CreateReminder: accept optional cron param, store next_fire_ts = fire_ts
- DueReminders: query on next_fire_ts instead of fire_ts
- RescheduleReminder: new method — parse cron, compute next fire, update
  next_fire_ts or mark fired if no more valid times
- Dispatcher: call RescheduleReminder for cron reminders, MarkReminder for
  one-shots (preserving existing behavior for ID=0 digest skip)
- ReminderCompleter interface: add RescheduleReminder method
- storeAPI adapter: forward RescheduleReminder
- All callers updated: CreateReminder signature includes cron param
- Tests: TestRecurringReminder (store), TestDispatchRecurringReminderReschedules
- Existing tests updated for new signature
2026-07-05 11:46:12 +04:00
kami 6b80fd0c0f tools: add scope column for capability model
Add a 'scope' TEXT column (default 'homelab') to the tools table so tools
can be namespaced by scope (e.g. "homelab:restart", "datacenter:reboot").
Backward-compat: bare name defaults to "homelab" scope.

Changes:
- Migration #1: ALTER TABLE tools ADD COLUMN scope
- store.Tool: add Scope field, update all SQL and scanTool()
- ipc.Tool DTO and request types: add Scope field
- CoreAPI interface: pass scope in ProposeTool/EnableTool
- storeAPI adapters: forward scope
- cmd/mavend/voice: pass scope (empty → homelab)
- cmd/mavweb/tools: show scope column in UI tables, hidden fields
- All tests updated for scope field
- Migration test made dynamic (startVer = len(migrations))
2026-07-05 11:40:15 +04:00
kami a80b919780 mavttsd: add comprehensive test suite (8 tests)
Covers:
- StubHandler.Synthesize: PCM16kMono format, 6400-byte output,
  different text → different waveform, empty text → audio
- resample22050To16000: empty input, approximate output length
- defaultSocket: XDG_RUNTIME_DIR resolution
- Integration: full synthesize round-trip via Unix socket
2026-07-05 11:32:28 +04:00
kami 185f4f578e mavweb: extend test coverage to credentials, signal, dash, history, revert handlers
- New credentials_test.go: 4 test functions covering credentialStore
  (new, save, lookup, update, persistence across restarts).
- Extended handlers_test.go: fakeCore now supports WriteFact, Presence,
  RecentFacts/Nudges/Notes, RevertFact. Added 7 new test functions:
  TestNoCache, TestHandleSignal (5 subtestcases covering method guard,
  nil core, unknown key, known key, write error), TestHandleDash (3),
  TestHandleHistory (3), TestHandleRevert (6), and ListToolsError 502.
- Fixed history.html: Go html/template requires conditional class
  rendered as separate <tr> branches, not inline attribute.
2026-07-05 02:31:05 +04:00
kami 6daa96b66e mavcaldav: add comprehensive test suite (14 cases)
Covers:
- iCal parsing: filters today's events, excludes past/future/all-day
- VEVENT parsing: TZID, UTC, and all-day (nil) events
- DTSTART/DTEND parsing: UTC, local (treated as UTC), all-day, invalid
- safeKey sanitization: spaces→dashes, strip special chars
- writeIfChanged: no-prev writes, same-value skips, diff-value writes,
  read-error and write-error propagation
- pollOnce end-to-end: httptest.Server + fakeCore verifies calendar_busy
  + calendar_event facts written with correct keys/values
2026-07-05 02:24:28 +04:00
kami a02e10fd11 ipc+mavweb: add revert/undo endpoint to void latest fact for a key
- New store.VoidLatestFact() method finds latest non-voided fact for
  a key and writes a void-marker row pointing at it (transactional).
- New IPC method MethodRevertFact with CoreAPI.RevertFact interface,
  storeAPI adapter, server dispatch, and client proxy.
- New HTTP endpoint POST /api/revert?key=<key> in mavweb.
- History page adds a 'revert' button per non-voided fact row with
  JS confirmation and optimistic UI (marks row voided on success).
- All existing store, IPC, and mavweb tests pass.
2026-07-05 02:18:39 +04:00
kami f8ba396fec mavweb: add /history page for command history
- New /history route displays recent facts in a dedicated page with
  voided-row styling (line-through + opacity + badge).
- Navigation link added to /dash page header.
- Handler calls core.RecentFacts(ctx, 200) and renders historyTmpl.
2026-07-05 02:13:13 +04:00
kami 44807b612c webauthn: persist credentials to JSON file instead of in-memory map
- New credentialStore type in credentials.go loads/saves
  map[id]localCred to a JSON file. Thread-safe with sync.RWMutex,
  writes to disk on every mutation.
- PasskeyHandle replaces sync.RWMutex+map with *credentialStore.
  Inline save/lookip/update closures delegate to store methods.
- newPasskeyHandle now takes a storePath parameter and returns an
  error; callers updated.
- New -passkey-file flag (default ./passkeys.json) configures the
  credential store path in main.go.
- Tests use os.CreateTemp in t.TempDir() so each test gets an
  isolated, auto-cleaned store file.
2026-07-05 02:09:56 +04:00
kami 7683a9b32c ipc: promote startup socket-wait to a shared DialWait; use in all modules
The cold-start crash-loop wasn't mavweb-specific — mavpoll and mavcaldav also
ipc.Dial + exit on failure, so they crash-looped until core booted too. Moved
the retry into ipc.DialWait (capped backoff, bounded) and switched mavweb,
mavpoll, mavcaldav to it. mavweb's local dialCoreWithRetry is gone.

Test: server appears after DialWait starts → it waits and connects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:17:10 +04:00
kami a38e733514 fix: local timezone for replies + quiet-hours; guard presence "ago" overflow
Timezone: the container ran in UTC, so mavend answered clock/date queries
(voice.go replySystem) and evaluated quiet-hours (gather.go) in UTC. Fixed at
the root — process TZ — rather than per-call: TZ=Europe/Samara in compose +
tzdata in the image (debian-slim strips it, without which Go ignores TZ and
stays UTC). One knob fixes replies and quiet-hours for every daemon; change the
zone in compose.

Overflow: the dash "ago" helper ran time.Since on a zero timestamp (no presence
yet / fresh db), saturating to ~292y and rendering "2562047h47m…". Guard zero →
"never".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:13:13 +04:00
kami 1a3ef572d1 mavweb: wait for core socket at startup instead of crash-looping
mavweb log.Fatal'd if mavend's socket wasn't up yet, so under compose it
crash-looped (relying on restart:unless-stopped) until core finished booting
its models. depends_on only orders container start, not socket readiness.
dialCoreWithRetry polls with capped backoff up to 60s; still fatal past the
deadline. Mid-life core restarts remain covered by ipc.Client's redial-on-drop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:08:56 +04:00
kami 88fb4912c4 mavweb: add a command cheatsheet to the voice page
Temporary reference while testing utterances: a collapsible <details> panel
(native, no JS) listing the six router intents (act/reminder/fact/note/query/
system) with real example phrasings pulled from models/seeds, RU-first since
the voice lang is ru.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:05:16 +04:00
kami 2740c2f685 mavsttd: silence gate — don't feed non-speech to whisper
Whisper hallucinates subtitle-credit boilerplate ("Редактор субтитров …") on
silence/room-noise, which then got stored as tap:voice facts. Gate before the
model: drop clips shorter than -min-ms (default 300) or below -silence-rms
(default 0.01 normalized RMS). Both are flags — the mic floor is hardware
specific. Returns empty transcript (same as whisper's no-segments path), so
nothing downstream changes.

gateReason is pure and unit-tested (silence/short/quiet → dropped, loud+long →
passes). ponytail: energy gate, not a real VAD; upgrade to WebRTC VAD or
whisper no_speech_prob if too blunt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:02:44 +04:00
kami de2058c851 mavweb: handler tests + gate DisableTool at step-up
- handlers_test.go: first tests for cmd/mavweb (feature-ranking #2). Covers
  the /tools enable/disable surface (arg parsing, error mapping, html escaping)
  and the webauthn handler contracts (method guards, malformed input). 14 cases.
  Verified the enable path is genuinely gated: an un-asserted call fails at the
  mavend IPC boundary (Requirement(EnableTool)=AuthStepUp), so mavweb stays a
  trust-nothing pass-through and core mediates.

- policy.go: DisableTool now also requires AuthStepUp. It mutates the same tool
  allowlist as EnableTool and is a lever to silence a security-relevant tool;
  gating allowlist mutation uniformly beats a split rule. ProposeTool stays
  maven-callable (no passkey). Corrects the stale api.go comment that claimed
  all three gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:22:10 +04:00
kami 047a813278 store: at-rest encryption + schema-migration runner
Two spine infra items (feature-ranking #1, part of the migration prereq):

- migrations.go: PRAGMA user_version runner, empty (no-op) migration slice,
  one tx per step, fail-closed. Mechanism in place before any real schema
  change needs it.
- crypt.go: file-level at-rest encryption. On-disk file is always AES-256-GCM
  ciphertext; decrypted to a tmpfs working copy modernc sqlite operates on;
  re-encrypted atomically on Close, plaintext wiped, key zeroed. Pure stdlib,
  CGO stays off. Fails closed on wrong key/tamper, never falls back to
  plaintext. Key is a 32-byte seam (config db_key_b64/db_key_env today; the
  passkey-derived L3 cold-start key plugs into the same seam later).

Chosen over cgo SQLCipher (would force libsqlcipher + CGO across the project)
and over the ncruces page-level VFS (swaps the driver project-wide); noted as
the upgrade path in a ponytail: comment. Threat model is disk-at-rest only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:22:09 +04:00
kami 6239eca243 items 5-7: passkey step-up, tools enable/disable, note RAG — end to end
Completes the three in-flight open items and fixes the away-fallthrough bug.

Item 7 — passkey step-up (WebAuthn):
- internal/webauthn: ES256/P-256 register + assert with real ecdsa signature
  verification, minimal CBOR/COSE decode, PasskeySession (L2→L3 on assert,
  decays after TTL). Drop the RS256 offer we can't verify (register-ok/
  assert-fail trap). Verify rpIdHash + UP/UV flags in FinishAssertion — UV is
  the step-up gesture. Round-trip test with negative cases (tampered sig,
  missing UV, wrong origin).
- cmd/mavweb: /auth/passkey enroll+assert page (the only surface that can do
  a WebAuthn gesture) + the four begin/finish endpoints. Without this the
  daemon's PasskeySession swap leaves /tools enable permanently blocked.
- daemon wires PasskeySession as the auth Session + srv.StepUp; policy gates
  MethodAssertStepUp at AuthRead.

Item 5 — tools page: DisableTool through store/ipc/client/wire; /tools grows a
disable action and a link to the passkey page. Lifecycle test.

Item 6 — note RAG: PhraseQuery on the phraser (LLM-composed answer over top-k
notes, raw-notes fallback); IntentQuery routes through it. Stub returns a
deterministic summary.

Item 2 — away-fallthrough: on ErrVoiceNoSession the dispatcher now reroutes
through the AWAY table (sev3→ntfy, sev4→telegram-repeat-til-ack, sev≤2→drop)
instead of silently dropping / mis-routing to the present-list remainder.
Covers DispatchNudge + DispatchReminder. 4 tests.

Also: re-add ProposeTool to CoreAPI (dropped in a comment rewrite), fix
missing imports + a duplicate block left mid-edit, drop dead AssertStepUpFunc,
gitignore /mavcaldav.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:41:13 +04:00
kami 36233058dd quiet-hours: configurable time-window schedule
Adds QuietHours config section (start/end as HH:MM local time). The loop's
gatherer checks the window each tick: if now falls within [start, end), the
State.QuietHours flag is set to true regardless of the config fact (which
the voice toggle writes independently). Both sources activate quiet —
schedule AND toggle.

Handles midnight-crossing windows (23:00-08:00). The gate already reads
State.QuietHours for care nudge suppression — no gate change needed.
2026-07-03 13:21:08 +02:00
kami f94758966f cmd/mavcaldav: new CalDAV poller module
Polls Radicale for today's events, writes calendar_busy and calendar_event
facts through CoreAPI. Only writes on value change (same append-only
discipline as mavpoll).

Usage: mavcaldav -socket <core> -url <radicale> -user <u> -pass <p>

Flags: -interval (default 5m), -timeout (default 10s).
Fires immediately on start, then on interval.

iCal parser supports UTC and local DTSTART/DTEND, skips all-day events.
2026-07-03 13:19:47 +02:00
kami 359ae81d1f piper: pipe cleanup on early return, write error; misc error hygiene
piper_handler: close stdin/stdout pipes on Start() failure and on
WriteString error instead of leaking fds. Propagate WriteString error.

worker/client: log SetDeadline errors instead of discarding them.

voice/session: pushAudio marshals params inline and returns the marshal
error instead of swallowing it via mustParams (removed).

tool/matcher: log ListTools errors instead of silently returning an
empty allowlist that refuses every act.

config: applyDefaults now sets RouterThreshold and ToolTimeout defaults
so consumers self-contained defaults are belt-and-suspenders.
2026-07-03 12:11:24 +02:00
kami b77f209686 voice: pre-route quiet-hours toggle, whisper ctx cancellation, stale reply fix
- resolveQuietToggle runs in HandlePushToTalk before the router so
  'тихий режим' works regardless of classifier confidence.
- whisper_full() runs in a goroutine with ctx.Done() select so the
  handler returns promptly on timeout/shutdown.
- StubReplier.IntentQuery no longer claims query is unimplemented.
2026-07-03 10:56:44 +02:00
kami e00cb07658 fix zombie leak, add quiet-hours toggle, improve query reply, configurable router threshold, JS dashboard 2026-07-03 00:42:35 +02:00