summaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-06-02Merge branch 'dev' into td/todo-fixAdam Malczewski
2026-06-02docs: handoff for cr/claude-reset-fix (wake probe body fix)Adam Malczewski
2026-06-02Merge branch 'dev' into cr/claude-reset-fixAdam Malczewski
2026-06-02feat(todo): port opencode's declarative whole-list todo toolAdam Malczewski
Replace the imperative id-based CRUD todo tool (add/update/list/get/remove) with opencode's declarative whole-list design: a single `todos` param that replaces the entire list each call. No model-visible ids, no delta reasoning, no "task not found" spirals. - core: TaskItem { id, content, status }; statuses pending|in_progress| completed|cancelled. TaskList.setTasks/getTasks/onChange. New rich TODO_DESCRIPTION adapted from opencode's todowrite.txt. - api: TASK_MANAGEMENT_GUIDANCE system-prompt section (from anthropic.txt); updated TOOL_DESCRIPTIONS.todo. Reload fix: TabStatusSnapshot now carries per-tab tasks so getAllStatuses rehydrates the panel on reconnect. - frontend: mirror types; hydrate tasks from snapshot in both restore paths; upgrade sidebar Tasks panel to render content + all four statuses + progress. - tests: new core task-list.test.ts (15); updated api TaskList mocks + getAllStatuses task-snapshot coverage. bun run check clean; 569 tests pass; all packages typecheck.
2026-06-02Merge branch 'dev' into tc/tab-controlsAdam Malczewski
# Conflicts: # packages/frontend/src/lib/components/ChatInput.svelte
2026-06-02fix(wake): probe with genuine Claude Code request shape so OAuth wakes succeedAdam Malczewski
The wake probe POSTed a bare { model, messages } body with no system[] identity. Anthropic validates system[] on OAuth (Pro/Max) subscription requests and rejects any that lack the verbatim Claude Code identity, so every scheduled wake (and the manual Wake-now button) failed silently — surfacing as a blank '— failed' status that then burned the retry budget. - Add pure buildWakeProbeBody(model) in @dispatch/core mirroring a genuine Claude Code request (billing header block + identity block + 'hi'), with a unit test for its shape. - wakeAllClaudeAccounts now sends that body plus the CLI session/request-id headers, and records 'HTTP <status>: <message>' on failure so the panel never shows a bare 'failed' and breakage stays debuggable.
2026-06-02feat(tabs): drag-reorder + double-click rename + per-tab chat draftAdam Malczewski
- TabBar: HTML5 drag-and-drop to reorder user tabs (subagent tabs untouched); double-click a tab title to rename (Enter/blur confirm, Escape cancel). - Store: add reorderTabs/renameTab/setDraft; per-tab in-memory `draft` and `manualTitle` fields. Manual rename suppresses first-message auto-title. - ChatInput: bind to the active tab's draft so switching tabs saves/restores unsent text instead of clobbering it. - Backend: updateTabPositions() + PATCH /tabs/reorder persist tab order to the existing `position` column; tabs without a stored position fall to the end then get explicit positions on first reorder. - Tests: store reorder/rename/auto-title-guard/draft coverage; core updateTabPositions coverage (FakeDatabase extended with transaction support).
2026-06-02Add HANDOFF.md for sb/status-barAdam Malczewski
2026-06-02Merge branch 'dev' into sb/status-barAdam Malczewski
2026-06-02Add status bar beneath chat input with send/stop button and context displayAdam Malczewski
Restructure ChatInput into two stacked bars: - Top bar: auto-resizing textarea + fixed-width send/stop button that morphs in place (no layout shift) across idle/generating states. - Bottom bar: agent status icon, context-window fill bar, and compact token count + percent (inert bar when model max is unknown). Wire contextLimit prop from App.svelte into ChatInput, reusing the shared computeContextUsage helper so it agrees with the sidebar.
2026-06-02Add HANDOFF.md for m1/minor-fixesAdam Malczewski
2026-06-02Fix cache req badge wrap, remove cache cost note, pace-aware key usage barsAdam Malczewski
2026-06-02chore: untrack and gitignore notes/wishlist.mdAdam Malczewski
2026-06-02Merge branch 'dev' into u3/agent-effort-levelAdam Malczewski
# Conflicts: # packages/api/tests/agent-manager.test.ts
2026-06-02wishlist: add major Workspaces feature with workspace-scoped agentsAdam Malczewski
2026-06-02test: prove Context Window view gets hydrated cacheStats.last after reloadAdam Malczewski
Cross-branch contract test (u2/context-window-view merged from dev): the Context Window panel derives current context from cacheStats.last via computeContextUsage. This drives the full path — persisted usage aggregate -> hydrateFromBackend -> cacheStats.last -> computeContextUsage -> '48,200 / 200,000' — proving the view shows real context size immediately after a reload on a new device (not 'No context data yet'). Guards the contract so neither persistence nor the view can silently break it.
2026-06-02Merge branch 'dev' into u1/usage-persistenceAdam Malczewski
2026-06-02fix: reconcile live cacheStats to DB truth on turn-sealedAdam Malczewski
Addresses the live-accumulator overshoot a Gemini review surfaced: the frontend adds every streamed usage event to cacheStats, but a rate-limited fallback attempt's usage is discarded server-side (never persisted). Live numbers overshot until a reload re-seeded from the DB aggregate. Fix: turn-sealed (emitted AFTER the atomic usage-row write) now carries the authoritative getUsageStatsForTab aggregate. The store REPLACES (not adds) cacheStats with it every turn — landing the just-sealed turn's usage AND self-healing any live drift, including the discarded-fallback overshoot. No extra round-trip (piggybacks turn-sealed); idempotent in the happy path. - core: add UsageStats type; getUsageStatsForTab returns it; turn-sealed gains optional usageStats field. - api: agent-manager reads getUsageStatsForTab post-flush and attaches it to the turn-sealed emit (try/catch: omit on DB error). - frontend: turn-sealed handler replaces cacheStats (undefined ⇒ untouched back-compat; null ⇒ clear). Tests: frontend reconcile/self-heal/back-compat/null-clear; api turn-sealed carries aggregate. 509 -> 514 passing; typecheck + biome green.
2026-06-02Merge branch 'dev' into u2/context-window-viewAdam Malczewski
2026-06-02wishlist: add new items - tab forking, per-tab input state, image ↵Adam Malczewski
attachments, better tab controls, agent tools isolation, chat settings conflict, backgrounding, tab naming fix, key usage tool, effort level per model/key, search code tool
2026-06-02feat(context-window): show current/max context usage per tab/modelAdam Malczewski
Add a 'Context Window' sidebar view showing the live context occupancy (latest request's input+output) against the model's maximum context window, resolved dynamically from the models.dev catalog. - core: models.dev catalog module (resolveContextLimit) with disk cache, TTL, stale-fallback + offline penalty memo; null for unknown models. - api: GET /models/context-limit?provider=&modelId=. - frontend: ContextWindowPanel + computeContextUsage helper; App resolves + caches the active model's max (anthropic/opencode-anthropic only); percent shown to 2 decimals; degrades to bare token count when max unknown. - tests: core catalog (13), api route (3), frontend helper (6).
2026-06-02fix(frontend): honest effort display in agent UI (gemini review)Adam Malczewski
Address two UI-accuracy issues found in review: - AgentBuilder: the per-model effort select no longer disguises an unset value as 'High'. Adds an explicit 'Inherit' option; choosing it strips the effort key so the saved TOML omits it (and the call site falls back to per-tab → default), matching displayed intent to persisted state. - ModelSelector: effort badges for models without an explicit override now reflect the actual effective effort (per-tab selector → default) instead of always showing the default constant, mirroring backend resolution.
2026-06-02feat: persist per-tab token/cache usage across reloadAdam Malczewski
Persist usage as invisible type:"usage" chunk rows (side channel): - core: add "usage" ChunkType + UsageData; exclude usage rows from getChunksForTab/getTotalChunkCount; add getUsageStatsForTab aggregate (exported from barrel); defensive skip in groupRowsToMessages. - api: agent-manager accumulates per-attempt usageRows and flushes them in the same atomic appendChunks call as the turn's content (discarded on a superseded fallback attempt). GET /tabs enriches rows with usageStats. - frontend: hydrateFromBackend seeds cacheStats from usageStats (reload only; no re-seed on statuses reconnect, so no double-count with live events). Tests: core DB-backed usage persistence/aggregate; api usage-row-per-event + fallback discard; routes GET /tabs usageStats; frontend hydrate seed + no-double-count + live-accumulation-after-seed. 495 -> 509 passing.
2026-06-02feat(agents): per-model reasoning effort levelAdam Malczewski
Add a per-model/key reasoning effort setting to agent definitions, surfaced and editable in the Agent Settings page and displayed at a glance in the model selector views. - core: single source of truth for effort levels (REASONING_EFFORTS, DEFAULT_REASONING_EFFORT='high', labels, isReasoningEffort guard); add 'xhigh' level; AgentModelEntry.effort; xhigh budget=24000 for classic-thinking Claude; default floor 'high'. Persist/parse effort in the agent TOML loader. - api: thread effort through the fallback chain with per-model -> per-tab -> default precedence; validate /chat + agentModels effort from the canonical list. - frontend: effort <select> per model row in AgentBuilder; effort badges in ModelSelector (agent + subagent chains); Thinking dropdown sourced from canonical list; per-tab default raised to 'high'. - tests: +15 (loader round-trip, agent xhigh budget, canonical list + guard, api precedence, route validation).
2026-06-01Add Phosphor icons; replace sidebar text with hamburger buttonAdam Malczewski
- Add phosphor-svelte ^3.1.0 to frontend deps. - Wire phosphor-svelte/vite (sveltePhosphorOptimize) as a fallback so stray named imports still tree-shake correctly. Per-icon imports like 'phosphor-svelte/lib/ListIcon' remain the preferred pattern; see the comment in vite.config.ts. - Replace the 'Sidebar' text in Header.svelte with a Phosphor List icon styled as a square daisyUI button (btn btn-square btn-sm btn-neutral) with aria-label preserved for screen readers.
2026-06-01merge: dev into r1/claude-reset-fixAdam Malczewski
Brings in the n2/ntfy-notifications feature (ntfy.sh push notifications with per-event toggles, subagent-suppression flag, topic-only input, Settings UI, dispatcher + transport + config modules, 12+ new tests), the header declutter (theme picker + Debug panel moved into Settings / sidebar), the shared theme boot-apply module, and an a11y label for the remove-panel button. No code changes from this branch were touched by the merge — the overlap was purely textual. Conflict resolution: 1. HANDOFF.md (add/add conflict). Both branches independently put a single-purpose HANDOFF.md at the repo root for their respective in-flight feature, matching the existing convention (c351719 did the same for this branch; 29bdd00 did the same for ntfy). After this merge both features ship, so neither is in-flight anymore. Archive both into notes/: - notes/wake-schedule-handoff.md (this branch — git tracks as a rename from HANDOFF.md) - notes/ntfy-notifications-handoff.md (dev — recovered from MERGE_HEAD before deletion) The root HANDOFF.md is intentionally absent post-merge; the next in-flight branch will create its own. 2. packages/api/tests/routes.test.ts (auto-merged). dev appended ntfy stubs to the vi.mock('@dispatch/core', ...) factory; this branch appended a 'Wake schedule routes' describe block at the bottom. The two regions don't overlap and the textual auto-merge is correct (verified: 6 describe blocks, both mock-stub regions and the new describe present, no conflict markers). Verification on the merge commit: bun run test → 31 files, 495 / 495 passing (was 431 on the branch + 64 from dev) bun run check → biome clean, 156 files bun run --cwd packages/frontend typecheck → svelte-check 0 errors, 0 warnings dev can now fast-forward to this commit: git checkout dev && git merge --ff-only r1/claude-reset-fix
2026-06-01fix(frontend): ClaudeReset — keep '11 AM' badge on one lineAdam Malczewski
The marked-hour summary badge ('11 AM', '12 PM', etc.) wrapped after the hour number when the trailing 'Probes …' text pushed against it, so '11' sat on the first line and 'AM' dropped to the second — ugly two-line badge. Add 'whitespace-nowrap' (prevents the space between hour and meridiem from breaking) and 'shrink-0' (so the badge never gets compressed narrower than its content) to the badge span.
2026-06-01docs: HANDOFF round-2 review followup + move review 2 into notes/Adam Malczewski
Append a 'Review followup — Round 2' section to HANDOFF.md documenting the round-2 Gemini-review fixes: - Critical: request-reorder desync (SnapshotSequencer assumed client send order == server processing order; not true on the wire). Fixed by promoting the per-hour pendingHours Set to a single global pendingHour mutation lock — serializing toggle POSTs eliminates the reorder window entirely. - High: toggle endpoint guessed user intent from server state, which combined with any desync to invert clicks. Fixed by requiring an explicit action: 'on' | 'off' field on every request. - Low: round-2 R2-3 (retry storm re-probes succeeded accounts) noted as a deliberate trade-off, not fixed. Move the round-2 review report from the project root into notes/claude-reset-review-2.md to match the convention established for the round-1 report. Net delta vs branch base: 431 tests (was 427 after round 1; +4 contract tests for the explicit-action endpoint). Biome and svelte-check clean.
2026-06-01fix(frontend): ClaudeReset — global mutation lock + explicit action intentAdam Malczewski
Round-2 Gemini review found that the SnapshotSequencer's 'most-recent client seq wins' rule only protects against RESPONSE reordering. If the network reorders the REQUESTS themselves (B reaches the server before A), the server's snapshot reflecting the true final state may carry the older client seq and get discarded — UI permanently desyncs. Two related fixes: 1. Replace pendingHours: Set<number> (per-hour lock) with a single pendingHour: number | null (global mutation lock). All 24 toggle buttons go disabled while any POST is in flight. This serializes mutations on the wire, eliminating the request-reorder failure mode entirely. 2. Send the action explicitly. toggleHour now derives 'on' or 'off' from its local state and passes it to postToggle, which sends it on the wire. Pairs with the matching backend contract change — the server no longer guesses from its own state, so even if a stale UI made it through it would just be an idempotent no-op or timestamp refresh instead of an inverted click. The SnapshotSequencer is retained — it still guards the GET-on-mount vs first-click race (where the two requests are NOT both mutations and the global lock doesn't apply). UX note: per-hour 'cursor: wait' visual is preserved for the hour whose request is in flight (so the user can see which click is pending), while the OTHER hours go merely disabled (no cursor change) — a clearer 'busy' signal than dimming everything uniformly. svelte-check: 0 errors, 0 warnings. 431 / 431 tests pass.
2026-06-01fix(api): wake-schedule toggle requires explicit action: 'on' | 'off'Adam Malczewski
Round-2 Gemini review surfaced that the toggle endpoint derived add-vs- remove from its own in-memory state, which combined catastrophically with any UI desync: a user clicking to turn ON an hour the UI showed as off, but the server had as on, would silently get the hour turned OFF. The clicks felt 'inverted' and the only recovery was a full reload. Fix: require an explicit `action` field on every /toggle request. The client must declare its intent; the server is no longer allowed to guess. Idempotency rules: - action: 'off' on an already-off hour → 200, no-op success. - action: 'on' on an already-on hour → 200, REPLACES timestamps (so a recovering UI can re-assert the user's wall-clock intent without a delete-then-add round trip). - Missing or invalid action → 400. The 'off' path no longer reads or requires `timestamps`. The 'on' path still requires all four slot timestamps as finite Unix-ms numbers (the skewed-toggle relaxation from round 1 is preserved). Tests: - toggle() helper auto-derives action from `timestamps` presence, so the existing 12 tests stayed terse. One test that relied on the old 'empty body = add' behavior now passes `action: 'on'` explicitly. - Added 4 new contract tests: * rejects requests missing/with-invalid action * action='off' on an already-off hour is idempotent * action='on' on an already-on hour replaces timestamps (the round-2 desync-recovery scenario) * action='off' ignores stray timestamps payloads 29 / 29 routes tests pass; 431 / 431 across the workspace.
2026-06-01Merge branch 'dev' into n2/ntfy-notificationsAdam Malczewski
Brings in: theme picker consolidation, sidebar Debug panel, header declutter, a11y label on remove-panel button. Conflicts in SettingsPanel.svelte (theme picker insertion site overlaps the ntfy block) and HANDOFF.md (each branch maintains its own). # Conflicts: # HANDOFF.md
2026-06-01feat(notifications): topic-only input (drop URL validation)Adam Malczewski
The Settings field is now a plain topic name (e.g. `my-secret-topic`) instead of a full URL. The transport always posts to `https://ntfy.sh/<topic>` (URL-encoded), and the only server-side check is "non-empty when enabled". Removes the user-visible "string does not match the expected pattern" error people hit when typing a bare topic. - packages/core/src/notifications/ntfy.ts: drop validateTopicUrl; add buildNtfyUrl(topic) + exported NTFY_BASE_URL. - packages/core/src/notifications/types.ts, config.ts: rename topicUrl -> topic; update docs. - packages/api/src/routes/notifications.ts: only validates non-empty topic when enabled. Also fixes a latent bug where notifySubagents was dropped on every PUT (was not passed to normalizeNtfyConfig). - packages/frontend/src/lib/components/SettingsPanel.svelte: relabel field "Topic URL" -> "Topic"; placeholder "your-secret-topic"; updated helper copy. - Tests updated: rewrote validateTopicUrl coverage as buildNtfyUrl coverage + proof that previously-rejected topics (dots, spaces, unicode, "Any Topic Whatsoever") now POST cleanly. - HANDOFF.md: added a short "topic-only input" section.
2026-06-01docs: HANDOFF review followup + move reset review report into notes/Adam Malczewski
Append a 'Review followup' section to HANDOFF.md documenting the three Gemini-review fixes (clock-skew toggle, snapshot race, transactional persist) and the two nits, with file-by-file scope, verification output (427 tests, biome clean, 0 svelte-check errors), and the design-pushback items deliberately deferred (snapshot polling, DST drift). Move the root-level review report (claude-report.md) into notes/claude-reset-review.md to match the existing convention from 4e63651 (root .md files collected under notes/). Renamed from 'claude-report.md' to disambiguate from the unrelated cache-miss notes/claude-report.md.
2026-06-01fix(frontend): ClaudeReset — global snapshot sequencer fixes cross-hour raceAdam Malczewski
Replaces the per-hour inFlightSeq with a single shared SnapshotSequencer used by both loadFromServer() and postToggle() (Gemini #2, High; nit #4). The bug: applySnapshot replaces the *whole* schedule object. The old per-hour counter could not stop request A for hour 9 (knows only about hour 9) from clobbering request B for hour 10 (knows about both) when B returned first and A straggled in — hour 10 would visually vanish. Same race existed between the initial-mount loadFromServer and a quick user toggle: whichever lost the race won the UI. Fix: every request to /models/wake-schedule (GET and POST) bumps a single monotonic seq. On response, sequencer.accept(seq) returns false if any newer request has already won; we drop the snapshot. Also drops the inFlightSeq mechanism entirely — it was redundant with pendingHours for user clicks AND insufficient for the cross-hour and initial-load races, so two mechanisms became one.
2026-06-01feat(frontend): SnapshotSequencer — reusable 'most-recent request wins' ↵Adam Malczewski
race guard Tiny, dependency-free class for the common pattern where a component fans out multiple HTTP calls that each return a full snapshot of shared state, and applying an older snapshot would clobber a newer one. begin() tags a new request, accept(seq) decides whether to apply the response. Pulled out as its own module (rather than inlined in ClaudeReset) because the next consumer of this pattern shouldn't have to re-derive it. The contract is small enough to test exhaustively in isolation: - accepts the first response unconditionally - accepts responses in send order - rejects an older response that arrives AFTER a newer one (the core race that motivated this) - rejects ALL stragglers once a newer one wins - handles the initial-load vs first-click race - equal seq is idempotent accept (defensive) - begin() seqs are monotonic and unique - state inspector reflects the watermark 8 tests, all green. No Svelte dependency — usable from any TS file.
2026-06-01fix(api): wake-schedule — accept skewed toggles, atomic persist, ↵Adam Malczewski
boot-recovery reason Three review-finding fixes in models.ts + regression tests: 1. POST /wake-schedule/toggle no longer rejects 'past' timestamps (Gemini #1, High). Client-server clock skew + request latency meant a freshly-computed nextOccurrenceAt(HH:MM) for an imminent slot could land in the past by the time the server validated it, silently failing the UI toggle. The scheduler's recoverScheduleEntry already fires within MISSED_WAKE_GRACE_MS and rolls forward by 24h × N, so the strict <= now check was actively harmful. Kept Number.isFinite + slot-present validation. 2. persistSchedule is now transactional (Gemini #3, Medium). The old DELETE-then-N-INSERTs path, when an INSERT failed mid-loop, left the table empty (DELETE had committed) and silently wiped the user's schedule on next boot — the catch swallowed the error. Wrapped both in db.transaction(...): on failure everything rolls back, in-memory state is untouched, and the previously persisted snapshot stays intact. 3. Boot-recovery reason no longer masked when boot recovery + due slots coincide (Gemini #5, Nit). Capture bootFireRequested before clearing the flag and append ' (boot recovery)' to the reason so the lastWake/pendingRetry surface tells the truth. Tests: - Replaced 'POST toggle rejects past timestamp' (the bug-as-feature test) with 'POST toggle ACCEPTS a slightly-past timestamp (clock skew / latency)' regression guard. - Added 'POST toggle rejects NaN / Infinity / non-number slot values' to lock the malformed-input path. - Added 'snapshot remains consistent across toggle round-trips (persistSchedule atomicity)' — exercises GET/POST cycles to ensure the transactional impl agrees with itself across add/remove. All 427 tests pass; biome clean.
2026-06-01docs: update HANDOFF.md with notifySubagents flag + bumped test countAdam Malczewski
2026-06-01feat(notifications): add notifySubagents toggle to suppress subagent turn pingsAdam Malczewski
A parent agent that spawns 8 subagents was producing 9 "Turn complete" notifications per round — almost always noise. New `notifySubagents` config flag (defaults to false) gates `turn-completed` and `turn-error` from any tab with a `parentTabId`. The flag is intentionally NOT applied to `permission-required` — a subagent's permission prompt still needs a human tap to proceed, so suppressing it would silently hang the subagent. `agent-spawned` is already top-level-only by construction. Wiring: - core/notifications/types.ts: NtfyConfig.notifySubagents: boolean - core/notifications/config.ts: defaults to false; normalize() tolerates missing / wrong-typed values and falls back to false - core/notifications/dispatcher.ts: new optional TabParentLookup option (getTabParentId). When notifySubagents=false AND the lookup returns a non-empty parent id string, turn-completed/turn-error are dropped. Lookup failures (no lookup configured, throws, returns undefined) fall back to "treat as top-level" so legitimate top-level events are never silently dropped when the DB is briefly unreadable. - api/app.ts: wires getTabParentId via core's getTab(id)?.parentTabId - frontend SettingsPanel.svelte: "Include subagent tabs" checkbox with an explanatory hint that permission prompts still fire Tests (+9): - 3 in config.test.ts: default-false, explicit-true, wrong-typed fallback - 6 in dispatcher.test.ts: suppression of turn-completed/turn-error from subagents, no suppression when flag is true, permission-required not gated, graceful fallback when lookup is missing/throws/returns undefined Live ntfy.sh round-trip re-verified (status: 200).
2026-06-01docs: update HANDOFF for 4-slot probing + DB migrationAdam Malczewski
2026-06-01feat(wake): probe 4 times per marked hour (:00 :15 :30 :45), coalesce ↵Adam Malczewski
same-tick fires Marking an hour on the Claude Wake Schedule panel now schedules FOUR probes within that hour instead of one. Rate-window edges are unforgiving — a single probe at :15 can miss the actual reset moment by up to 14 minutes; hitting :00 / :15 / :30 / :45 puts us within ~7 minutes of any reset that happens during that hour. When multiple slots come due in the same 30s scheduler tick (or recover together at boot), they coalesce into a SINGLE upstream wake call — no point hitting Anthropic 4× in the same window. DB schema - wake_schedule is now (hour, slot_minute, next_wake_at) PK (hour, slot_minute). Destructive migration: detect old single-row-per-hour schema by absence of the slot_minute column and DROP TABLE. No other table is touched. Per user direction: no back-compat for old rows. API - POST /models/wake-schedule/toggle add: { hour, timestamps: { '0': ms, '15': ms, '30': ms, '45': ms } } — all 4 slots required, all must be future Unix ms. Delete shape unchanged ({ hour }). - GET /models/wake-schedule shape: schedule: { '9': { '0': ts, '15': ts, '30': ts, '45': ts }, ... } probeSlotMinutes: [0, 15, 30, 45] resetOffsetHours, lastWake, pendingRetry (unchanged from prior commit) Frontend - Computes 4 timestamps client-side (next occurrence of HH:MM in local TZ) and sends them in one request. - markedHours summary now says 'Probes :00 :15 :30 :45 → reset by ~Xh later'. - Same in-flight tracking / current-hour ring / status row as before. Tests - wake-scheduler.test.ts unchanged (pure helpers still correct; added PROBE_SLOT_MINUTES + isProbeSlotMinute exports). - routes.test.ts rewritten for the new payload shape: 12 wake-schedule tests covering snapshot shape, add/remove (full 4-slot round-trip), validation (range, integer, past-slot, missing slot, non-object, missing timestamps), independent multi-hour scheduling, and re-toggle replacement. 417 tests total (was 414).
2026-06-01docs: update HANDOFF.md with Gemini review triage + post-fix stateAdam Malczewski
2026-06-01fix(notifications): address Gemini review — tighten validation, sanitize ↵Adam Malczewski
Click, support Basic auth, non-optimistic UI clear Acted on 4 of 6 findings from the gemini-3-flash-preview second-opinion review (the other 2 were verified-wrong or judged not worth the complexity — see HANDOFF.md). core/src/notifications/ntfy.ts: - validateTopicUrl now enforces ntfy's actual topic-name constraints: exactly one path segment, 1–64 chars, charset [A-Za-z0-9_-]. Prevents users from saving topic URLs that look fine but silently 404 at publish time (cf. binwiederhier/ntfy#1451 for the 64-char limit and binwiederhier/ntfy's topic-name regex for the charset). - Click header now passes through sanitizeHeader, closing the same CRLF-injection vector that Title/Tags already had. - Authorization header construction now factors through a small buildAuthHeaderValue helper: a value that already starts with a scheme token ("Bearer xyz", "Basic dXNlcjpwYXNz") is used verbatim, so users of private ntfy servers that want Basic auth can paste the full header value. Bare tokens still get the "Bearer " prefix automatically. frontend/SettingsPanel.svelte: - clearNtfyAuthToken() was optimistic: it flipped hasAuthToken=false locally before awaiting the network call. If the request failed the UI lied about server state, and worse — a subsequent Save() with authToken:undefined would silently re-arm the original token. Now awaits the response, surfaces failures via the existing ntfySaveError banner, and only mutates local state on success. Adds a ntfyClearingToken loading flag so the button disables + spins during the request. Tests: +6 in ntfy.test.ts (multi-segment rejection, charset rejection, length boundary, 64-char acceptance, Basic auth pass-through, Click sanitization). All 442 tests pass; biome clean; svelte-check clean; manual ntfy.sh end-to-end re-verified.
2026-06-01docs: update HANDOFF.md with post-review fixes and triageAdam Malczewski
- Document the Gemini Flash 3 Preview review pass. - Record triage table of all 4 findings (1 fixed Major, 1 deferred Minor, 1 subsumed by the fix, 1 fixed nit). - Update verification numbers (404 tests, 142 files checked, 167 modules built). - Update manual smoke-test instructions to cover the fresh-install theme bug regression (was the headline finding). - Note the open recommendations (System theme, storage-key audit, ordering) that were intentionally deferred.
2026-06-01a11y(sidebar): label the remove-panel button for screen readersAdam Malczewski
Gemini review nit. The ✕ button on each sidebar slot (idx > 0) was read by screen readers as "multiplication sign" or "cross". Adds aria-label="Remove panel" so the action is announced clearly. Also gitignore claude-report.md (Gemini review artifact, not source).
2026-06-01fix(theme): consolidate boot apply and Settings picker into shared moduleAdam Malczewski
Gemini review surfaced that App.svelte (onMount theme apply) and SettingsPanel.svelte (theme <select>) hand-rolled their own defaults and could disagree: - App.svelte only set data-theme if localStorage had a value, so on a fresh install daisyUI fell back to the first theme in app.css (light). - SettingsPanel.svelte hardcoded a UI default of "dark". Result: a first-time user saw a light app but a Settings panel that claimed "dark" was selected. Picking *any* value in the dropdown was the only way to reconcile reality with the UI. This commit: - Adds packages/frontend/src/lib/theme.ts as the single source of truth: THEMES list, Theme type, THEME_STORAGE_KEY, DEFAULT_THEME, plus loadStoredTheme() and applyTheme() that handle SSR / private-mode / bad-value cases. - Rewires App.svelte's onMount to call applyTheme(loadStoredTheme()), so the boot apply always writes a known good theme to the DOM (even on fresh installs), matching what Settings will show. - Rewires SettingsPanel.svelte's picker to use the shared module, dropping its duplicate THEMES const, duplicate storage key, duplicate apply/persist logic, and the conflicting "dark" fallback. - Adds 11 unit tests in tests/theme.test.ts covering the default-fallback, known/unknown stored values, SecurityError-on-read, SSR (no localStorage), DOM-attribute write, persistence round-trip, and the "DOM still updates if storage write throws" contract. The daisyUI plugin block in app.css still lists themes — that's a CSS-time concern and can't be imported from TS, so it's kept in sync by convention (noted in the new module's doc comment).
2026-06-01docs: HANDOFF.md for claude-reset-fix branchAdam Malczewski
2026-06-01fix(frontend): ClaudeReset — live currentHour, real Set derived, in-flight ↵Adam Malczewski
tracking, status row Bugs fixed - fadedHours was $derived((): Set => {...}) — returned a *function*, not a Set. blockClass() then called fadedHours() once per of the 24 buttons, defeating Svelte's memoization. Now uses $derived.by(() => Set), passed in to blockClass as a value. - currentHour was $derived(new Date().getHours()) which is computed once on mount and never updates. After midnight (or any hour boundary) the 'now' ring stayed on the wrong block. Now driven by a nowMs $state bumped by a 30s setInterval, cleaned up on destroy. - Rapid double-clicks could land out of order ('last response wins, not last click'). Now tracks an in-flight Set + per-hour sequence counter; stale responses are dropped and pending buttons are disabled. - No feedback on wake success/failure. Snapshot now includes lastWake + pendingRetry, surfaced as a colored status row. Cleanups - resetOffsetHours pulled from the server snapshot (was hardcoded +5). - fadedHours window is now resetOffsetHours - 1 (was hardcoded 4). - onclick handler short-circuits when the hour is already pending.
2026-06-01fix(api): wake scheduler — missed-wake recovery, retry consolidation, ↵Adam Malczewski
status surface Bugs fixed - Missed wakes silently lost. The old loadScheduleFromDB just pushed any past next_wake_at to its 'next occurrence' in *server* local time, so a wake that fired while the API was down never ran — defeating the whole point of the panel (overnight task picks up after a 5h rate-window reset). Now: if missed by <= 2h we fire it on the next tick; either way the entry is rolled forward by 24h-multiple steps. - Server-TZ drift. nextOccurrenceAt15 used the server's local TZ, so on a UTC Docker host running for a user in PST the reschedule slowly migrated the fire time. Now we advance by 24h * N from the original client-supplied timestamp, preserving the user's wall-clock intent. - Retry storm. Every failed wake pushed a new entry into a retries[] array, all converging at the same +5min instant. Replaced with a single shared pending-retry slot whose budget resets on subsequent failures. - Retry race with fresh fires. If a tick fired AND a retry was due in the same iteration we'd double-hit the upstream. Now retries only run on ticks where no fresh wake fired. New behavior surfaced on /wake-schedule: { schedule, resetOffsetHours, lastWake, pendingRetry } POST /wake-schedule/toggle now also rejects non-integer hours (4.5, etc.) and returns the same snapshot shape so the client can stay in sync. Tests: 9 new HTTP route tests covering snapshot shape, add/remove, validation (range, integer, past timestamp, missing timestamp), and independent multi-hour scheduling.
2026-06-01feat(api): extract pure wake-scheduler helpers (nextDailyAfter, ↵Adam Malczewski
recoverScheduleEntry) Side-effect-free module so missed-wake recovery and rescheduling can be unit-tested without booting Hono or touching SQLite. - nextDailyAfter: advances by 24h increments until strictly > now (handles multi-day gaps in a single step instead of looping a day at a time). - recoverScheduleEntry: classifies a past next_wake_at into 'fire now, then advance' vs 'silently advance' based on MISSED_WAKE_GRACE_MS (2h). - CLAUDE_RESET_OFFSET_HOURS / resetHourFor: single source of truth for the '+5h reset' display, previously hardcoded in three places. Includes 12 unit tests covering grace boundaries, multi-day skip, custom grace windows, and midnight wraparound.
2026-06-01docs: add HANDOFF.md for ntfy notifications featureAdam Malczewski