| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
review(web): close Moonshine and chat findings Verified: Bun typecheck, Bun smoke and Vitest tests (164 passed), and lint (warnings only). | 1 个月前 | |
fix(web): keep streamed replies open through tool call pauses | 4 天前 | |
feat(listen): capture-position clock for transcripts, audio and live speaker ID (#18995) * feat(listen): pure audio-timeline v2 primitives One capture coordinate: a per-socket integer sample cursor over decoded PCM16 mono, with arrival-time anchors at the first accepted frame and at measured inter-arrival hiatuses (2 s jitter guard, monotonic-confirmed so a wall-clock step never mints an hour of audio). Provider timestamps translate only through SendMap spans the provider actually accepted; out-of-range or evicted ranges fail closed. segment_wall_window is the one wall-time conversion; covered_window/coverage_outcome never guess coverage from timestamps alone. AUDIO_TIMELINE_V2 flag helper is pure config/, read at the call boundary, default off. * feat(listen): capture sample timeline through receiver, STT epochs and pusher audio runs Eligible sessions (flag on, single-channel, server STT) get a CaptureTimeline; every decoded frame occupies an exact sample range that is carried into the ring buffer (span ledger), the STT buffer (contiguous start sample) and the pusher audio runs. Each selected provider socket gets its own epoch translator created at callback-creation time (legacy callbacks and the managed chain both rebuild per epoch); the VAD gate tracks pre-roll/source spans and GatedSTTSocket/LiveLegSocket record only sends the provider accepted. Mapped segments resolve their owning conversation from the capture span; straddling segments are dropped. listen_pusher_session buffers AudioRun(conversation_id, start_wall, bytes) captured at acceptance, sends one 101 per contiguous same-conversation run with the retained projected start (opcode 103 ahead of boundaries), and performs the opt-in audio_timeline=2 capability handshake: ack required before any v2 audio and on every reconnect; capability loss mid-recording withholds audio as a coverage gap instead of terminating or silently downgrading. * feat(listen): pin conversation first-audio origin with fenced v2 marker started_at is set once from the first accepted audio frame associated with the conversation (not conversation creation, not the first segment), so pre-roll can leave the first segment at a positive offset and delayed callbacks cannot shift an origin already emitted to clients. update_conversation_segments pins the typed audio_timeline marker and the projected origin in one transaction, only while the row has no marker; once pinned, stale started_at writes are ignored. The conversation controller tracks per-conversation origins: fresh v2 generations pin at their first accepted frame (receiver), resumed conversations reuse the persisted started_at and are never marked v2. TranscriptProcessor processes epoch-translated segments per owning conversation from the capture span: the current generation gets the full live path with offsets projected against the pinned origin; a late batch may still write its still-open owner, and a terminal owner is fenced out and counted, never replayed into a newer recording. Photo-only drains keep ordinary wall lifecycle times. * feat(pusher): v2 continuity, span-carrying chunks and coverage-true storage Opted-in connections (audio_timeline=2 query param) get an explicit pusher->listen acknowledgment (opcode 202, fixed version) before any v2 audio is accepted. v2 audio frames are checked against the last accepted run's end with 1 ms tolerance: positive discontinuities flush the partial private-cloud chunk and close the pending batch for upload (never concatenating across a gap or inside a 60s batch), exact replays are ignored, partial overlaps are trimmed after verifying the retained bytes, and conflicting bytes for an accepted range fail closed. v2 chunk queues carry their authoritative PCM sample count; uploads stamp span metadata on the blob and refuse to overwrite a colliding filename with differing bytes. Listing rehydrates spans; grouping splits at actual uncovered ends (or overlaps) rather than start-to-start differences; AudioFile exposes validated chunk_spans whose layout joins the artifact fingerprint; span-aware merging trims overlaps and silence-fills only real gaps. Legacy listings behave exactly as before. * feat(listen): coverage-gated audio consumers Clips, speaker-tag prompt selection and speaker-sample extraction use the central helpers: for v2-marked conversations the actual selected window must be covered by validated chunk_spans (clip endpoint rechecks through the same conversation_clip_pcm); uncovered, pending-upload, unsupported and no-audio windows are unavailable rather than a clip of the wrong audio, with bounded coverage counters at each check. Legacy conversations keep the timestamp-based best-effort behavior unchanged. v2 playback parts are contiguous by construction (grouping splits at every real gap), so one span per part keeps the released clients' strict-seek arithmetic. * docs(listen): audio timeline v2 wire/storage contract; dev-on prod-off flag listen_pusher_pipeline.mdx owns the overlap env/wire contract and now documents the v2 capture coordinate, epoch translation, the audio_timeline=2 query-parameter handshake with the opcode-202 acknowledgment, discontinuity/overlap reconciliation, span-carrying blobs and the coverage-gated consumers. AUDIO_TIMELINE_V2 is true in the dev backend-listen and pusher values and explicitly false in prod; no dev backend Cloud Run env surface exists (backend-listen and pusher deploy through the GKE charts). * test(listen): hermetic receiver->pusher->storage->clip v2 regression Drives the actual receiver frame/epoch logic, ListenPusherSession run buffering, the pusher websocket handler (ack, continuity, batch split, span uploads), storage upload/listing/grouping/merge, the fenced conversation persistence and the clip helper against in-memory doubles. The same burst scenario runs with the flag off — which IS the legacy calculation — and asserts the acceptance criterion fails there: the chunk timestamp (last arrival minus the whole buffered run) lands ~10 s away from the segment position (first audio + provider time), coverage is unsupported and no correct clip exists. With v2 on: origin pinned to the first accepted frame, an hour-long wall step mints no audio and no anchor, a reconnect rebuilds the provider epoch with provider time restarted, the rollover conversation pins its own origin, out-of-range provider segments reject, and clips return the exact phrase bytes. Capability loss withholds buffered audio and never downgrades. Also: ownership ranges are bounded by capture time (120 s) not a tiny frame count, batch uploads carry the span start, and the v2 branch in process_loop reads the timeline defensively for stub hosts. * feat(probe): opt-in audio-timeline alignment scenario for dev --alignment-scenario paces two >8 s speech windows in real time with delivered silence and one actual bounded inter-arrival gap, holds the socket open through finalization, and checks the v2 marker, span coverage of the transcript windows, /v1/sync/audio urls, and the clip endpoint with verify_and_transcribe_sample as an auxiliary ASR check only (deterministic PCM identity stays the hermetic gate). Enrollment in private-cloud sync is verified/attempted first; NOT_RUN — never PASS — when the dev identity cannot enable it. Receipts carry no transcript, audio, token, or endpoint data; all reads stay inside the five-minute token TTL. Not executed here: dev-only, run by the coordinator. * test(pusher): v2 wire matrix — ack, discontinuity flush, replay, overlap Old listen (no query parameter) sees no ack and keeps legacy concatenation; an opted-in connection is acknowledged with opcode 202 before anything else and an unsupported version is refused. A positive discontinuity uploads the partial run separately (span-carrying, never concatenated across the gap or inside a pending batch), an exact replay is ignored, a conflicting overlap fails closed, a partial overlap is trimmed to continue the run exactly, and a conversation switch flushes and rebinds. The switch test caught a real bug: the extracted flush helper skipped the buffer reset, so a switched conversation's audio appended to the previous conversation's leftover bytes under its old start; the reset is restored. * refactor(storage): share v2 span helpers from the pure timeline module Blob span metadata helpers and coverage-aware chunk grouping move to utils/audio_timeline.py so storage.py and database/conversations.py keep only their persistence-specific code (and their line-count growth shrinks to what the feature genuinely adds there). * feat(listen): capture-clock speaker ID for every server-STT session Live speaker identification now locates audio on the capture sample clock for every single-channel server-STT listen session, not only AUDIO_TIMELINE_V2 admissions. After a mid-session STT failover the replacement provider's timestamps restart at zero; the legacy first_audio + raw_start window then lands minutes before the 60s ring buffer and matching returns silently, so the owner loses live recognition for the rest of the recording even though the voiceprint is correct. The sample cursor, per-provider-epoch translators (fresh on every failover rebuild), the capture-positioned ring buffer and the speaker-ID window are internal to one listen socket: they run always. Only the v2 persistence stays flag-gated via state.capture_timeline_v2 (pinned once per recording): projected segment times, started_at pin/marker and the pusher opcode-101 projection. With the flag off the translator runs in project_times=False mode - provider-native times pass through untouched, owner fencing is skipped, unmapped segments stay delivered (origin/main persisted them) - so persisted fields, WebSocket segment fields and the pusher wire remain byte-identical to the flag-off baseline; only the speaker-ID window moves. Multi-channel and custom-STT keep no capture clock at all. * feat(listen): attributable live speaker-ID match exits After the 2026-09-25 incident the queued owner detections returned before any match decision without a single log line; the affected account could only be attributed by replaying the saved conversation's speaker ids. Every early return in speakers._match_unmapped and its callers that drops a queued detection now emits exactly one bounded reason as a Prometheus counter (label = reason only) plus one log line: window_outside_buffer (the post-failover clock bug's signature, previously disguised as an inverted-clamp "too short"), too_short, no_pcm, stale_generation and already_mapped. Pending evidence accumulation is not a drop - it already logs and its speaker reaches a decision on the next clip - so it is not counted as an exit. The existing speaker_id_evidence/speaker_id_decision lines, the new exit line and the match-failure line all carry the recording session id (never uid, never transcript content). * test(listen): owner recognition acceptance across an STT failover Hermetic acceptance test for the 2026-09-25 incident: the owner voiceprint is enrolled, the owner's pre-failover clip accumulates evidence, the provider dies mid-conversation, the real failover path rebuilds the socket and the replacement stream's timestamps restart at zero, and the owner's later segments must be is_user=true in BOTH the WebSocket output and the persisted conversation. Drives the actual receiver (decode, capture clock, epoch translators, the actual _failover_stt_socket rebuild), the actual TranscriptProcessor.process_loop, the actual SpeakerMatcher and the actual fenced persistence against in-memory doubles and a deterministic fake embedding model (fixed-frequency bins; no model download). The replacement provider also opens a new diarization scope, so recognition must recover from a fresh unmapped speaker - exactly the incident's Speaker 1. Passes with AUDIO_TIMELINE_V2 off and on; at the merge base (771fe0175e, in a temporary worktree under .local/) the same file fails both parameterizations on the is_user assertion - the queued detections land before the 60s ring buffer and never decide. Speaker-ID queueing in the legacy persistence path now queues from the provider's raw segments (each carrying its capture window) instead of the post-merge turns: the live merge relabels a merged turn with the absorbing id, whose window is not in the batch's id-keyed map. The matcher's covered-audio subtraction already dedupes overlapping re-sends. The dev probe's alignment scenario gains the same assertion where the probe identity has a voiceprint (persisted all-is_user plus one live-delivered is_user segment; NOT_RUN, never PASS, without one). The probe is not run by this change. * feat(speaker-tag): attributable owner confirmation outcome The 2026-09-25 incident: a client-side "That's me" answer reported succeeded=true yet left no owner_voice_confirmation and did not update the voiceprint - the outcome existed only as an unlabeled Prometheus counter, so the loss could not be attributed to a conversation. store_owner_voice_sample now logs one line per outcome naming the conversation id (never the uid); the recording session id is not on this path because prompts are answered after the socket closed, so the conversation id is the join key. Clip-path proof: a hermetic test runs the real store_owner_voice_sample through the real owner_clip_window and the real span-carrying clip path (batch upload -> span-aware listing -> merge -> sample-accurate trim) on a v2 conversation that experienced a failover, and asserts by byte identity that the pooled sample is the owner's actual post-failover audio window - not the pre-failover phrase the legacy first-audio + restarted-provider-time formula selects. * docs(listen): capture-clock speaker ID for all server-STT sessions; match-exit counter Document that the capture sample clock, per-provider-epoch translators and the capture-positioned ring buffer are internal to one listen socket and run for every single-channel server-STT session - only the v2 persistence stays behind AUDIO_TIMELINE_V2 - and that with the flag off only the speaker-ID window relocates. Document the bounded omi_speaker_id_match_exits_total reasons, the session id on the speaker-ID log lines, and the attributable That's-me outcome line. * fix(storage): database-layer pure span helpers; content-hash batch no-overwrite Move the pure v2 span helpers into a stdlib-only database/audio_timeline.py and import them relatively from database/conversations.py: hermetic tests exec database modules against a stubbed utils package, so a new module-scope utils.audio_timeline import there broke test collection (ModuleNotFoundError: utils.audio_timeline). utils/audio_timeline re-exports the shared helpers as the single source of truth. parse_span_blob_metadata now requires a real mapping: a test double's auto-attribute metadata previously parsed as span {start:1.0,...} (MagicMock supports __float__), silently rerouting legacy per-chunk merges into the v2 span-trim branch and dropping the second chunk. upload_audio_chunks_batch compares content, never plaintext size: equal length is not identity (fixed-size 60 s PCM chunks), and enhanced-protection objects are ciphertext whose length cannot prove a retry. A SHA-256 of the plaintext payload is compared against the existing blob (decrypted first for enhanced); identical retries are idempotent no-ops, unreadable or differing content fails closed without overwriting. * fix(listen): flag-off VAD remap, thread-safe callbacks, run-based owner ranges P0: with the capture clock on and v2 off, the non-passthrough callback kept make_stream_callback's semantics: the active gate's remap_segments runs on provider timestamps before anything is buffered, and the epoch translation only attaches the capture window without rewriting start/end, so flag-off stored/emitted times stay byte-identical to origin/main (with v2 on the send-map translation replaces the mapper; never both). Deepgram SDK callbacks arrive off the event loop; receiver-built callbacks and the managed-chain enqueue now hop onto the listen loop (_run_on_listen_loop) before touching timeline/send-map state, and the managed chain uses the receiver's pinned persistence mode instead of always owner-fencing (_enqueue_epoch_segments). conversation_sample_ranges entries coalesce into same-conversation runs, so retention is time-based (120 s) instead of the ~10 s a 512-frame cap held; a late final now resolves its owner instead of dropping as late_owner_dropped. Capture origins carry a pinnable flag: only fresh v2 conversations may pin; a resumed row adopts its persisted started_at (datetime/number/ISO string, parsed by persisted_started_seconds in contracts) and an unparseable one locks the row legacy — never a fresh pin. Anchor compaction now fails closed: wall_strict refuses samples in evicted intervals and the translator rejects such segments instead of extrapolating across dropped hiatuses. * fix(listen): introductions run with capture windows; v2 batch parity P0: queue_from_raw only re-homes the duplicate embedding enqueue; name introduction detection runs on the merged segments exactly as on origin/main, so a flag-off pendant user saying 'My name is Alice' keeps the person creation, suggestion event and assignment updates. _process_v2_batches: only a pinnable origin (or an already-pinned marker) writes the v2 marker + started_at — a resumed row projects against its adopted started_at and stays legacy for its lifetime; a legacy-locked row (unparseable started_at) keeps its transcript on the legacy base. A photo-only drain, or a late-owner-only drain with current photos, now runs the current conversation's write so photos are never dropped. Late-but-open owners are persist-only (documented); Diarization Completed accounting is shared with the legacy loop via the same per-conversation speaker map, and the delivery block is one shared _deliver_live_updates used by both paths. * fix(pusher): span-walking extract, gap-split 101 frames, flushed-range conflicts AudioRingBuffer.extract walks the per-write span ledger and copies the bytes overlapping the window span by span, so a clip crossing a >2 s client stall returns the audio on both sides instead of converting the whole wall delta into one byte offset (wrong PCM or ring wraparound). The write path and per-byte copy are unchanged from origin/main. ListenPusherSession flushes one 101 frame per contiguous same-conversation group, splitting when the next run's projected start is more than 1 ms past the previous run's end, so a disconnect-length gap is never erased from the stored audio while the header claims the first run's position. Legacy runs without a projection keep the legacy grouping. Pusher v2 overlap verification extends past the live buffer through a bounded digest ledger of flushed runs ((start, end, sha256)); a range that can be neither byte-compared nor digest-matched is a conflict — the frame is dropped and counted in omi_audio_timeline_replay_conflicts_total instead of appending an unverified suffix over stored bytes. The private-cloud overflow-warning test now asserts the structural invariant (every enqueue into private_cloud_queue sits in a function that warns) after the three inline enqueue sites were consolidated into one guarded helper. * test(listen): pin round-3 fixes through the real receiver/gate/pipeline test_audio_timeline_round3: flag-off remap equality with the gate mapper, passthrough/v2 variants, unmappable-segment survival, SDK-thread loop hop, 30 s-late owner resolution on coalesced runs, evicted-interval refusals, resumed-row (datetime and string started_at) never marked v2, photo-only and late-owner photo drains, fresh-row pin control. test_listen_speaker_id_failover now drives the real initialize_stt and _rebuild_stt_socket_locked through provider-connector doubles with a REAL active VADStreamingGate (deterministic Silero fake) behind the receiver's own GatedSTTSocket install — removing send_tracker=epoch from the install now fails the wiring assertions and the recognition acceptance. Adds the >2 s client-stall case asserting the ring buffer extracts exact bytes across the gap, the P0 introduction regression (introduction handled identically to origin/main with capture windows present), and Diarization Completed for both persistence modes. Also: span-walk stall tests for the ring buffer (including wraparound), 101-frame gap-split tests, flushed-range replay conflict/digest-match tests, and the pipeline doc updates for all changed behavior. * test(listen): accept epoch in the parity socket double; pin audio_timeline field The parity runtime's _create_stt_socket double predates the epoch argument the receiver passes (round-1/2 addition the coordinator's partial CI run never reached), so initialize_stt failed silently and the capture-once acceptance lost its socket. Conversation.audio_timeline is pinned in the trust-boundary field set as server-authored provenance, not projection-family (never client-authored, no text). * fix(pusher): drain the retry lane when no batch is pending; fix connect double arity A failed private-cloud upload's retry batch lives in ready_batches, but the loop's 'if not pending: continue' skipped the ready-lane drain, leaving the loop condition truthy forever — a busy spin once shutdown is set (the upload- failure test hung at 100% CPU). The guard now also checks ready_batches. The circuit-breaker connect double predates the audio_timeline parameter the branch added to _connect_to_trigger_pusher; the signature mismatch raised at call time before the double could set its started event, hanging the stale- inflight test on await started.wait(). * fix(listen): managed chain keeps the legacy offset rebase in clock-only mode; rebuild-factory stubs With the capture clock on and v2 persistence off, the managed chain's callback translated epochs and dropped the leg's own offset/last_end clock, so flag-off emitted times restarted at 0 per leg instead of staying monotonic like the pre-timeline managed chain. Clock-only legs now translate first (attaching the capture window from provider timestamps), then apply the same gate remap and offset rebase as the epoch-less path, and enqueue through the receiver's pinned mode. v2 legs keep the projected translation. The _stt_rebuild tuple became a callback FACTORY in round 1 (a fresh epoch translator per rebuild); three failover test files still stubbed the old callbacks-tuple shape and crashed the rebuild with a TypeError. * chore(failure-classes): FC-transcript-and-audio-on-independent-clocks Defines the class this branch closes: transcript times and stored/buffered audio placed by independent clocks (STT provider offset vs frame arrival), which drifted up to 54 minutes and silently broke clip playback and live owner recognition after a provider failover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(listen): owner fail-open only inside the retention horizon A sample older than the oldest retained run no longer fails open to the single surviving conversation: retention may have evicted the run — and every boundary — that owned the sample, so the retained window cannot prove ownership and the segment stays late_owner_dropped (re-review N1). The single-conversation fail-open now applies only between retained runs, inside the retention horizon, where no run covering the sample could have been evicted yet. * fix(storage): an exists() probe failure fails closed like an unreadable blob A raised blob.exists() on the v2 no-overwrite path was swallowed as 'absent', so the upload opened the blob for write and replaced whatever object actually sat at the colliding 3-decimal batch key (re-review N2). Any exception from the probe now raises the same ValueError as an unreadable existing blob and never opens the blob for write. * fix(pusher): keep the new tail past last_end on an unverifiable v2 replay An overlap that could be neither byte-compared nor digest-matched dropped the whole 101 frame, losing the frame's genuinely new audio past the accepted end as a coverage hole (re-review N3). The conflict branch now trims to the bytes past audio_timeline_last_end and continues the run there, still never storing the unverified overlap and still counting the conflict. The flushed-run digest ledger is now recorded only after a batch's GCS upload succeeds (one contiguous entry per batch), so a chunk evicted by the bounded queue or a batch dropped after exhausting its retries leaves no entry: replays of that range are unverifiable conflicts instead of being trusted by a digest whose bytes were never stored. * fix(pusher): pace ready-lane upload retries one process interval A failed upload re-queued itself at the front of the ready lane, so the in-turn while-loop retried it immediately: one fast permanent error burned the whole retry budget in a single tick, and one slow timeout blocked this socket's uploader for back-to-back attempts (re-review N4). The retry now keeps its queue time and is gated by retry_at for one PRIVATE_CLOUD_SYNC_PROCESS_INTERVAL tick; the gate is skipped during shutdown so the drain stays bounded. * fix(listen): deferred STT callbacks count rejections and carry their own segments A provider callback deferred onto the listen loop via call_soon_threadsafe ran on the caller's segment list and, if it raised, died as a bare asyncio callback error: the batch was gone with no counter and no log (re-review N5). The hop now copies the segment list (a provider may reuse its buffer once the callback returns) and wraps the deferred action so a failure increments omi_audio_timeline_segments_total{outcome=rejected} for the session's mode and logs one bounded line (exception type and count only). The closed-loop drop stays. * test(pusher): overflow warning must guard the private-cloud enqueue The overflow check counted the 'private_cloud_queue full' string anywhere in the enqueuing function, so a second enqueue on a path the fullness check never runs on (before it, or in an early return above it) would drop the oldest chunk silently (re-review N6). The AST test now also requires the warning's fullness branch to precede each enqueue on its statement path, and proves the checker flags a synthetic early-return enqueue. * docs(listen): round-4 replay, ownership, and loop-hop behavior The digest ledger is per uploaded batch and written only after the GCS upload succeeds; an unverifiable replay keeps its new tail past the accepted end; upload retries are paced by the process interval; the ownership fail-open is bounded by the retention horizon; deferred STT callbacks count rejections on their own segment copy. * chore(deploy): classify AUDIO_TIMELINE_V2 as non-secret config Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(listen): pyright-clean audio timeline typing; regenerate API contracts Types PrivateCloudChunk's optional v2 span, binds the pusher socket once per flush and ack wait, drops unused imports/re-exports and the private span-helper aliases, and registers AudioTimelineProvenance with the Dart schema group. Regenerates the app-client OpenAPI contract and the Dart, TypeScript and Swift client models for the new optional fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(app): changelog fragment for the audio timeline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(storage): store audio-timeline chunk spans as {start, end} objects Firestore rejects an array directly inside an array, so AudioFile.chunk_spans as [start, end] pairs would have failed every v2 audio_files write on dev (hermetic fakes accepted it), and the generated Dart reader for the nested list did not compile. Spans are now ChunkSpan objects, every reader goes through database/audio_timeline.chunk_span_bounds, and a test pins that a dumped AudioFile carries no nested arrays. Client models regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 7 小时前 | |
Add web/app .env.template for dev onboarding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 5 个月前 | |
feat(web): migrate app runtime to moonshine | 1 个月前 | |
chore(web/app): add Prettier config matching sibling web apps web/app was missing the .prettierrc.js that web/frontend and web/personas-open-source have, so the pre-commit Prettier hook fell back to defaults and rewrote quote style. Add the shared config (singleQuote). | 4 个月前 | |
docs: take operator pages off docs.omi.me Unlisted Mintlify MDX is still a public URL. Move runbooks, flags, invariants, and agent rules next to owning code, add docs/AGENTS.md as the site allow-list, and correct the live kill-switch contract after the JIT authority page leaves the site. Co-authored-by: Cursor <cursoragent@cursor.com> | 26 天前 | |
chore(web): build and serve moonshine with bun | 1 个月前 | |
chore(web): drop Leaflet No web surface renders Leaflet maps any more, so remove leaflet, react-leaflet and @types/leaflet, and stop copying and linking leaflet.css in the moonshine asset build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 11 天前 | |
feat(web): one shared page chrome, on shadcn primitives Timeline and Memories were meant to match and only did by coincidence. They now share one `PageToolbar` — a left slot for view controls, a right-aligned search field, a right slot for actions — so they match by construction, and Tasks and Connectors use it too. None of them render a page title any more. The sidebar already says where you are, and a heading repeating it is a second answer to a question nobody asked. The manual refresh buttons go with them: the data already refreshes on cache invalidation, so the button was a placebo. `PageHeader` survives for detail and sub pages, which legitimately need a back button beside a title. shadcn/ui is initialised properly — its dependencies were already installed but it had never been set up. The primitives are restyled onto the Omi tokens; the generator emits literal `oklch(...)` strings, none of which are valid classes here, so none of that survived. Tasks loses the coloured bar down the left of each card. What it encoded is now words: the due line already said "3 days late", and overdue gets a neutral chip. The duplicated due-date maths behind it is extracted to lib/taskDue.ts with an injectable clock and tests, rather than living twice in two components. Purple is now gone from the interface entirely, so the tokens themselves are deleted from tailwind.config.ts and globals.css — that is what stops it growing back. | 1 个月前 | |
fix(web): unblock dependency installs in web/app `npm install` failed outright in web/app with "Unable to resolve reference $postcss": the overrides block referenced `$postcss`, but npm 10 resolves `$name` only against `dependencies` and postcss is a devDependency. `npm ci` reads the lockfile and is unaffected, which is why the Dockerfile build kept working and nobody hit this — but no dependency could be added to the package. Replaced the reference with the literal `~8.5.18` it resolved to, so the override is unchanged in meaning. `npm run lint` was separately broken: `next lint` was removed in Next 16, so it parsed "lint" as a directory and exited with "Invalid project directory provided, no such directory: .../web/app/lint". Switched to `eslint .` and added the flat config that ESLint 9 requires, matching web/admin. Verification: - `npm install --save-dev vitest jsdom @testing-library/react @testing-library/jest-dom` now succeeds (added 776 packages). - Lockfile delta audited: 0 packages removed, 117 added (test toolchain only), 2 transitive patch bumps (@tybys/wasm-util 0.10.2->0.10.3, tinyglobby 0.2.16->0.2.17). No production dependency version changed. - `npm run lint` now executes and reports 98 real findings instead of erroring. Failure-Class: none | 1 个月前 | |
feat(web): migrate app runtime to moonshine | 1 个月前 | |
chore(web): drop Leaflet No web surface renders Leaflet maps any more, so remove leaflet, react-leaflet and @types/leaflet, and stop copying and linking leaflet.css in the moonshine asset build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 11 天前 | |
Add web/app dashboard with conversations, memories, tasks, and chat features - New Next.js app at web/app with full dashboard UI - Conversations: split view, folders, merge, search, date filtering - Memories: list/graph views, filters, bulk actions, quick add - Tasks: hub/list views, calendar, progress tracking, bulk actions - Chat: AI assistant with file uploads, voice input, app selector - Apps: explore, install, create custom apps - Settings: profile, integrations, subscription management - Cloud Run deployment workflow and Dockerfile | 8 个月前 | |
feat(web): one shared page chrome, on shadcn primitives Timeline and Memories were meant to match and only did by coincidence. They now share one `PageToolbar` — a left slot for view controls, a right-aligned search field, a right slot for actions — so they match by construction, and Tasks and Connectors use it too. None of them render a page title any more. The sidebar already says where you are, and a heading repeating it is a second answer to a question nobody asked. The manual refresh buttons go with them: the data already refreshes on cache invalidation, so the button was a placebo. `PageHeader` survives for detail and sub pages, which legitimately need a back button beside a title. shadcn/ui is initialised properly — its dependencies were already installed but it had never been set up. The primitives are restyled onto the Omi tokens; the generator emits literal `oklch(...)` strings, none of which are valid classes here, so none of that survived. Tasks loses the coloured bar down the left of each card. What it encoded is now words: the due line already said "3 days late", and overdue gets a neutral chip. The duplicated due-date maths behind it is extracted to lib/taskDue.ts with an injectable clock and tests, rather than living twice in two components. Purple is now gone from the interface entirely, so the tokens themselves are deleted from tailwind.config.ts and globals.css — that is what stops it growing back. | 1 个月前 | |
ci: align merge gates with the Moonshine runtime Verified locally with bun run typecheck, bun run test, backend chat tests, and the generated Firestore index check. | 1 个月前 | |
feat(web): migrate app runtime to moonshine | 1 个月前 | |
test(web): raise vitest timeout for userEvent-heavy tests under load The full-suite run under a loaded runner (parallel manifest checks) timed out userEvent typing tests non-deterministically at the 5s default — GoalComposer/Sidebar/BulkActionBar measured 5-8s wall standalone-fast but failed in ~1 of 3 full runs, including on pristine main. 15s budget; all 415 tests pass. | 26 天前 | |
feat(web): add the goals and scores data layer Desktop's Home is goals plus a task-completion score, both served by backend routes the web client never called: /v1/goals/** (23 endpoints) and /v1/scores. This adds the client half. - src/types/goals.ts re-exports the generated Goal types and declares the score wire shapes. Scores are hand-written because the scores router is absent from docs/api-reference/app-client-openapi.json, which is what the TS generator reads; the file points at backend/models/score.py as the authority. - src/lib/goals.ts holds the pure logic: progress percent, completion, metric formatting, display ordering, default-tab resolution. - src/lib/api.ts gains getGoals/createGoal/updateGoalProgress/deleteGoal and getScores. It uses /v1/goals/all rather than /v1/goals/canonical/list: the canonical route is gated on task-system enrollment and would 403 for users who are not enrolled, which is most of them. - useGoals applies writes optimistically and rolls them back on failure. Progress is measured from min_value, not from zero, so a 40->80kg goal sitting at 40 reads 0% rather than 50%. The optimistic rollback needs a committed-value ref. The first version captured the pre-write goal inside the setGoals updater, which does not work: React runs the updater during the next render, after a rejected request's catch has already run, so the captured value was still undefined and the rollback silently did nothing. useGoals.test.tsx covers both rollback paths and would have caught it. Also adds vitest.setup.ts to register jest-dom matchers. Verification: - `npm run check` -> tsc --noEmit clean, 49 tests passed across 5 files - goals.test.ts covers min_value-relative progress, clamping, zero-span division, boolean goals, sort tiers, and the unknown-default_tab fallback - useGoals.test.tsx covers load failure, create failure, optimistic write before server confirmation, progress rollback, and delete restore Failure-Class: none | 1 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 4 天前 | ||
| 7 小时前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 26 天前 | ||
| 1 个月前 | ||
| 11 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 天前 | ||
| 8 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 26 天前 | ||
| 1 个月前 |