The earlier sed-delete of the Push/ChatHistory/About sections from
the Notifications tab also clipped the tab's outer </div>. Vue's
type-checker happily accepted the unbalanced structure (templates
type-check on script bindings, not tag pairing) but Vite's Vue
compiler failed at build time:
Element is missing end tag.
file: src/views/SettingsView.vue:1062:5
(The reported line 1062 was the outermost .settings-content div —
Vue's parser blames the outermost open tag when an inner sibling
goes unclosed.)
Confirmed by counting: 115 open <div, 116 </div> before — and now
116/116 after restoring the closing tag between the Email Notifications
section and the Integrations tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The generated Claude Code snippet was outputting:
claude mcp add ... scribe-dev --url <URL> --header ...
But `claude mcp add` errors out with `unknown option '--url'`. The
URL is a positional argument, not a flag:
claude mcp add [--transport ...] [--scope ...] <name> <url> [--header ...]
Dropped --url and put the URL inline as a positional. Claude Desktop
JSON snippet was already correct (uses {url, headers} keys, not a
CLI flag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings → MCP Access now lets you tune the generated snippet:
- Server name input (default: 'scribe', stored per browser in
localStorage). The name appears both in the claude mcp add
command and as the JSON key in claude_desktop_config.json's
mcpServers map.
- Scope dropdown: user / project / local. Drives the --scope
flag in the claude mcp add snippet. Picks 'project' to commit
the server into the current repo's .mcp.json.
User-visible 'Fable' → 'Scribe' in MCP Access tab copy (lead text,
Claude Desktop step). Branding pivot in the rest of the app
(assistant_name placeholder, SMTP defaults, version line, etc.) is
deferred — chat/journal copy is going away in Phase 7 anyway.
Deliberately NOT touched:
- Tool names (fable_*) — protocol-level identifiers; renaming
breaks any Claude session, agent, or automation that referenced
them. Warrants its own phase.
- mcp/server.py: FastMCP('fable', ...) server name — same reason.
- Internal package name fabledassistant — per the project's
naming convention (CLAUDE.md memory), internal stays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the apikeys settings tab for the new MCP architecture:
- Tab label: 'API Keys' → 'MCP Access'
- Shows the in-app MCP URL (<origin>/mcp) with a copy button
- Claude Code snippet uses --transport http + --url + --header
- Claude Desktop snippet uses {url, headers: {Authorization}}
- Drops the wheel-download flow, the 'Other' client tab, and the
stdio env file / Claude config download helpers — those were
for the standalone fable-mcp package which goes away in phase 8
The api_keys backend stays unchanged — keys double as bearer tokens
for the /mcp endpoint via the existing auth.py middleware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated changes per operator request 2026-05-24:
1. Settings UI rename matching the language we actually use:
- Chat Model -> Chat & Voice Model
- Worker Model -> Curator Model
Setting KEYS (default_model / background_model) unchanged on
purpose; renaming them requires a migration touching 50+ call
sites for purely UX-facing benefit.
2. Settings UI help text rewritten:
- Chat & Voice: documents that it handles chat AND small
conversational automations (titles, tags). Recommends
OLLAMA_NUM_PARALLEL=2+ on the Ollama server so background
automations get their own KV-cache slot and don't evict
the chat model's working state.
- Curator: notes the app enforces SERIAL execution regardless
of NUM_PARALLEL — only one curator pass runs at a time. This
matters most for 70b CPU models where a second instance
would waste system RAM.
3. Enforce serial curator execution globally:
- New module-level _CURATOR_RUN_LOCK in services/curator.py.
- run_curator_for_conversation now wraps its body in 'async
with _CURATOR_RUN_LOCK' — every entry point (scheduler sweep,
manual route trigger, future hooks) is serialized through it.
- is_curator_running() helper exposes the lock state.
- routes/journal.py manual trigger checks is_curator_running()
first and returns 409 {busy: true} immediately rather than
blocking the HTTP request for minutes waiting for a 70b CPU
pass to finish. The user can retry once the curator clears.
Why a 409 instead of queue: a curator pass on a 70b CPU model
can take 5+ minutes. Tying up an HTTP worker that long is bad;
making the user wait without feedback is worse. 409 surfaces
the busy state immediately and the user retries when they want.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chat and background model roles effectively swapped during the
conversation+curator pivot, but call sites still used OLD routing.
This commit re-routes each call to the model whose new role fits.
Moved to background_model (worker — heavy, deliberate):
- services/journal_prep.py: daily prep generation.
- services/user_profile.py: observation consolidation.
Moved to default_model (chat — small, fast):
- services/chat.py save_response_as_note: note title generation.
- services/tag_suggestions.py: tag suggestions.
Already routed correctly (unchanged): curator, closeout, consolidation,
project summaries, history summarization.
SettingsView.vue: help text rewritten for both model fields to
describe new roles. Background Model UI label renamed to Worker
Model so the heavier role is visible from the picker. Warning copy
updated to recommend OLLAMA_MAX_LOADED_MODELS=2+ so chat and worker
can stay loaded simultaneously.
Schema names default_model and background_model unchanged on purpose
(renaming requires migration + touches ~50 call sites for UX-only gain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The frontend half of the review queue. Closes the curator approval
loop end-to-end.
JournalView.vue:
- New 'Needs Review' section in the right rail, ABOVE the Captures
panel (per the design decision: pending stands out, captures are
ambient). Hidden entirely when nothing is pending so the rail stays
calm.
- Each pending action renders as a card:
- Header: action_type chip (e.g. 'update_note') + human-readable
title built from pendingTitle() ('Update Famous Supply network
restage', 'Delete Old grocery list', etc.).
- Diff body:
- For deletes: a red 'Permanent delete' warning.
- For updates: field-level diff rows (field name | old | → | new)
computed by pendingDiff(), which compares the curator's payload
against the snapshot taken at proposal time. Skips lookup-only
params (query, task, project, milestone, confirmed) so the diff
shows only what'd actually change.
- Empty-diff fallback for tools without snapshot helpers.
- Approve / Reject buttons. Disabled while a request is in flight
via reviewingIds Set so double-clicks can't fire twice.
- Approve calls approvePendingAction → server replays the original
tool call with authority='user'; toast on success/error.
- Reject calls rejectPendingAction → marks rejected, no execution.
- Both actions refresh the pending list AND the moments list (since
approving an update_note could affect what shows in captures).
- loadPendingActions() also runs after every manual curator trigger
and on initial mount, so the panel reflects current state without
manual page refresh.
CSS: warm-tinted panel using --color-warning so the section visually
distinguishes from the neutral captures feed below. Approve button
in success-green, reject in muted. Diff rows use a grid layout with
old-value strikethrough and an arrow separator.
End-to-end demo loop:
1. Have a journal conversation that includes 'mark the Famous Supply
task as done'.
2. Wait for curator sweep or hit 'Process captures'.
3. Curator search_notes('Famous Supply'), then update_note(...) is
intercepted by execute_tool(authority='curator') and queued.
4. The Needs Review panel shows: 'Update task Famous Supply network
restage' with status diff todo→done.
5. Click Approve → execute_tool replays with authority='user' →
the task moves to done. Card disappears from Needs Review.
This is the last C* commit in the queue. The curator now has a safe
path to mutate user data via proposals, with the user firmly in the
loop on every change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HTTP surface for the review queue. Three endpoints, all under
the existing /api/journal blueprint to keep the journal-related routes
together:
- GET /api/journal/pending — list current user's pending actions.
- POST /api/journal/pending/<id>/approve — replay the proposed tool
call via execute_tool(authority='user'). On success, marks
the row 'approved'; on replay error, leaves it pending so
the user can retry.
- POST /api/journal/pending/<id>/reject — marks 'rejected' with no
execution.
Each route is a thin wrapper around services/pending_actions and
delegates user-scoping to the service (which checks user_id on every
load — actions are private to the proposer).
api/client.ts:
- PendingCuratorAction interface mirroring the backend dict shape:
id, user_id, conv_id, action_type, target_type/id/label, payload,
current_snapshot, status, timestamps.
- listPendingActions / approvePendingAction / rejectPendingAction
helpers for the upcoming Needs Review panel.
C5 next: the panel itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-in-one cleanup motivated by the chat hang in dev 2026-05-22.
The crash root cause from the guarded-task traceback:
UnboundLocalError: cannot access local variable 'get_setting'
where it is not associated with a value
File generation_task.py:257, in run_generation
think = (await get_setting(user_id, 'think_enabled', 'false'))...
generation_task.py imports get_setting at module top, but a later
'if voice_mode: from ... import get_setting' block scopes it as a
function-local. When voice_mode=False the local import never runs,
but Python had already flagged get_setting as local for the entire
body — the think_enabled read at line 257 hit UnboundLocalError.
The line itself was dead-weight anyway. With the conversation+curator
architecture: chat ships tools=[] (think on a no-tools pass is pure
latency cost; nothing for the model to reason ABOUT in tool-call
terms), and the curator hardcodes think=False already. The user
setting was a holdover from before the architecture pivot. Removing
it entirely is cleaner than fixing the scoping bug to preserve a
toggle nobody should be using:
- generation_task.py: think hardcoded False. Removed the get_setting
call (which fixes the UnboundLocalError as a side effect).
- SettingsView.vue: dropped the Enable model thinking checkbox, the
thinkEnabled / savingThinkEnabled refs, the saveThinkEnabled
function, and the think_enabled load step.
- Migration 0050: DELETE FROM settings WHERE key='think_enabled'
to clean up any stored rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/api/journal/moments takes date_from + date_to query params, not the
single 'date' name the frontend was sending. Filter was silently
ignored; the panel showed every moment in the database ordered by
recency, making it look like a weird recap of past events instead of
today's captures.
No backend change; just send the right param names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vue's template parser doesn't handle JS-style \\' escaping inside
double-quoted attribute values, so `today\\'s` produced a compiler
crash during the production frontend build. Rephrased to avoid the
apostrophe entirely. No functional change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend half of the conversation+curator architecture. Pairs with the
backend in commit a7002a8. With this commit, you can have a journal
conversation (chat model has no tools, doesn't try to capture), then
press a button and see what the curator extracts.
JournalView.vue:
- New "Captures" section in the right rail, above the existing
"Upcoming" events block. Shows moments from the selected day with
timestamp, content, and entity/task/note chips.
- "Process captures" button (Sparkles icon). Disabled for non-today
days because we're not back-running the curator over historical
conversations. Toast on success/failure with timing + tool-call
count from the CuratorRunResult.
- Captures auto-load on day change AND immediately after a curator
run completes — the right rail reflects current state without a
page reload.
- Bound CSS scoped to the rail: cards with a primary-color left
border, monospaced timestamps, chips for people/places/tasks/notes.
api/client.ts:
- CuratorRunResult type matching the backend dataclass.
- runJournalCurator(convId) helper.
- Pass empty body to apiPost() to satisfy the 2-arg signature
(caller-side fix, not a backend change).
What's not in this commit (deferred):
- The captures panel doesn't show captures from days where the curator
hasn't run yet, even if they would later be captured. Visible only
AFTER a curator pass. (Phase 2's scheduler closes this gap by
running automatically.)
- No edit/delete affordances on captures yet — that comes when we
add the moment-editing UI (out of scope for the conversation+curator
architecture commit chain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Building on the kokoro→piper swap (B1), this adds the admin-side
voice management story so additional voices can be installed without
rebuilding the image. The bundled two voices stay as immediate defaults;
everything else is opt-in via a one-click install from the catalog.
Backend (services/voice_library.py):
- fetch_catalog() pulls voices.json from the piper-voices HF repo with
a 24h in-memory TTL. Manual refresh available via ?refresh=1 on the
library endpoint.
- shape_catalog_for_ui() projects the raw HF dict (~250 voices, lots of
nesting) into UI-friendly cards: id, name, language, country, quality,
size, install state. Sorted by language_code then name for stable
display. Install state distinguishes bundled (read-only) from user
(admin-installed, can be removed).
- install_voice() downloads .onnx + .onnx.json into /data/voices with
atomic .tmp → rename so a failed partial download can't leave a
corrupt model around. Idempotent — re-installing an already-present
voice is a no-op.
- uninstall_voice() removes /data voices; bundled /opt voices raise
PermissionError (403 at the route layer).
- Strict voice-id regex prevents path traversal in install/uninstall.
Routes (admin-only, since these write to shared /data and affect all
users on the instance):
- GET /api/voice/voices/library
- POST /api/voice/voices/install
- DELETE /api/voice/voices/<voice_id>
Frontend:
- New "Voice Library" section in Settings → Voice, visible only to
admin users. Collapsed by default; expand to load the catalog
on-demand (doesn't hammer HF for non-admins).
- Free-text filter across id, language code, language name, country,
and dataset name. Refresh button forces a catalog re-fetch.
- Per-voice row shows id, language/country/quality/speaker count, size,
and either an Install button, a Remove button (user voices), or a
"bundled" badge (read-only voices in /opt/piper-voices).
- Installs and uninstalls refresh both the library list AND the active
voice picker so the new voice is immediately selectable.
- VoiceLibraryEntry exported from api/client.ts; new client helpers
getVoiceLibrary/installVoice/uninstallVoice.
Tests:
- Pure-transformation unit tests for shape_catalog_for_ui,
_resolve_file_urls, and the voice-id regex (path-traversal coverage).
- DB/network paths (fetch_catalog, install_voice) need a real
environment — left to CI integration tests or device verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kokoro has been stale upstream since April 2025 (`requires_python<3.13`),
which broke the Python 3.14 build. Piper is the active replacement:
maintained by OHF/Home Assistant, depends only on onnxruntime +
pathvalidate (no torch, no spacy, no transformers), and has cp314
support today.
Dockerfile:
- Add `pip install piper-tts` after the STT install.
- Bundle two default voices (en_US-amy-medium, en_US-ryan-medium) into
/opt/piper-voices at build. Additional voices can be downloaded into
/data/voices via the admin UI (separate commit).
- Image add over the STT-only baseline: ~150 MB.
services/tts.py — full rewrite:
- New voice-discovery layer scans /opt/piper-voices + /data/voices for
.onnx + .onnx.json pairs. /data wins over /opt for the same id so
admin-downloaded voices can override bundled defaults.
- Single PiperVoice kept warm; switches via _switch_voice() when the
user changes their voice_tts_voice setting.
- list_voices() returns metadata read from .onnx.json sidecars (label
derived from filename, language, quality, sample_rate).
- synthesise() uses piper's SynthesisConfig; converts kokoro-shaped
`speed` multiplier to piper's `length_scale` (1.0 / speed).
- `voice_blend` parameter accepted but ignored — piper has no blend
equivalent; first entry's voice is used if anything is passed.
- Dropped: HuggingFace commit-hash tracking (~80 lines), the daily
check_for_kokoro_updates task, voice-tensor blending math.
routes/voice.py:
- tts_backend reports "piper" in /api/voice/status.
- /api/voice/voices no longer requires tts_available() — even with
the active voice failed to load, the catalog still lets the user
pick a different one.
- Synthesise request body dropped the voice_blend field; speed and
voice still supported.
alembic 0047_reset_voice_tts_settings:
- Deletes any stored voice_tts_voice (kokoro IDs that don't map to
piper) and voice_tts_blend (no piper equivalent) rows. Both
re-default cleanly on next read.
frontend:
- VoiceBlendEntry type removed from api/client.ts.
- synthesiseSpeech() signature dropped the voiceBlend parameter.
- SettingsView.vue Voice Blend section removed entirely (slider,
preview, slot management). voice_tts_blend save path removed.
- Default voice id changed from "af_heart" to "en_US-amy-medium".
- VoiceEntry gains optional language/quality/sample_rate fields
from the richer piper sidecar metadata.
Voice paths remain lazily guarded — `VOICE_ENABLED=false` (default)
starts the app cleanly regardless of which TTS deps are present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat generation pipeline previously forced think=True unconditionally
to match qwen3's combined think+tools template, locking the system into
that model family. Bench data (2026-05-21, qwen3:30b-a3b/qwen3:32b on
CPU) showed thinking adds 1-2 minutes per turn for unclear quality
benefit — qwen3:30b-a3b even produced more rambling with think on.
This decouples think from the model family by reading a per-user
`think_enabled` setting (default `false`). Non-qwen3 models can now run
through the same pipeline without the silent-generation failure mode
that content-gated thinking would have caused — they just don't think.
qwen3 users who still want thinking can opt in via the Settings UI.
Settings UI:
- New "Enable model thinking" checkbox in General → Assistant section.
- Help text explains the default-off rationale and when to opt in.
- Persists via the existing settings API; no schema migration needed
(Setting is key/value text).
Telemetry to confirm whether this regresses tool-call reliability on
qwen3 (the current model) is in a follow-up commit (generation_tool_log).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version list rows now render a kind-aware badge: filled circle for
manual pins (with the label inline), half-filled circle for auto-pinned
versions. The right pane gains a control row above the diff:
- Unpinned: 'Pin version' button → label input → Save creates a manual
pin with that label.
- Manual: 'Edit label' + 'Unpin' buttons.
- Auto: 'Pin permanently' (promotes auto → manual with editable label).
Local state is patched from the API response so the UI updates without
reloading the panel.
New Tasks section in the General tab with a single checkbox controlling
whether the consolidation pipeline fires automatically. Persists to the
auto_consolidate_tasks user setting (string 'true'/'false'). Manual
'Re-consolidate' in the task editor bypasses the gate.
When consolidated_at is set on a task, the editor:
- shows a banner above the body indicating the body is auto-summarized
- hides the Write tab; locks the body view to read-only preview
- exposes a Re-consolidate button that calls POST /api/tasks/:id/consolidate
and refreshes the body from the response
Pre-consolidation behavior is unchanged — the Write tab and TiptapEditor
remain available.
Note type gains description and consolidated_at fields. TaskEditorView
adds a Goal textarea above the body editor (wired through dirty/save/
autosave paths). TaskViewerView renders Goal as a subordinate block
above the body, plus a subtle 'Auto-summarized from work logs' banner
when consolidated_at is set.
Also adds a consolidateTask client function for the upcoming
re-consolidate button (Task 11).
Adds a "Repeat" select (None / Daily / Weekly / Monthly / Yearly) that
reads/writes the existing Event.recurrence RRULE. CalDAV-imported rules
with extra parts (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR) surface as a disabled
"Custom" option with the raw rule shown read-only — visible but
preserved unless the user explicitly picks a preset to replace it.
EventUpdatePayload.recurrence is now string | null so we can clear via
PATCH; backend service already treats null as "clear" (recurrence is in
the nullable set in update_event).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: the right-edge slide-over panel pinned its action buttons
to a thin floor band where the destructive ghost-style Delete button was
functionally invisible against the dark surface. Save / Cancel / Delete
all sat in the same floor strip, isolated from the form content.
This refactor changes the surface and the commit model.
## Centered modal, not slide-over
Backdrop dim covers the whole viewport; the panel sits centered with a
12px corner radius and a soft shadow. The form scrolls internally when
content overflows the viewport (max-height: calc(100vh - 2.5rem)).
File kept as `EventSlideOver.vue` to avoid touching the three consumers
(CalendarView, HomeView, ToolCallCard).
## Action buttons removed; close = save
- Save button: gone. Auto-save fires when the user closes via X, Esc,
backdrop click, or pressing Enter inside a text field.
- Cancel button: gone. Esc / X / backdrop click already cover dismiss;
a labeled "Cancel" was redundant.
- Delete button: moved to the header as a Trash2 icon (edit mode only).
Click → header swaps to inline confirm "Delete this event? [Yes,
delete] [No]" — same two-step flow, just relocated. Esc during the
confirm cancels back to edit mode rather than closing the modal,
giving the user a clear way out of the destructive prompt.
## Validity-aware close
All exit paths funnel through `attemptClose`:
- Form valid → save (POST or PATCH), then close.
- Form invalid in EDIT mode → discard the in-memory change and
close, with a toast naming the missing field
("Title required — change discarded"). Keeps the user from
silently corrupting an event.
- Form invalid in CREATE mode → close silently. Nothing was
committed; calling that out adds noise.
Emit signature unchanged (close / created / updated / deleted), so the
three consumers continue to work without edits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI typecheck failed: JournalLocation interface requires `address: string`,
but Profile-tab Locations code created defaults like `{ label: 'Home' }`
without it. Symptoms were six TS2741 errors in SettingsView.vue.
Fix: include `address` in defaults and treat the user's place-name input
as the address field. Now homeQuery / workQuery sync to
locations.{home|work}.address — both at load time and on geocode.
loadJournalConfig now reads address (was reading label, which is fixed
to "Home"/"Work"); geocodeFor writes the typed query into address while
also setting lat/lon on success.
Calendar Month/Year title was actually rendering Fraunces, not Inter as
I'd claimed. FullCalendar renders .fc-toolbar-title as an <h2>, which
the global theme.css `h1, h2 { font-family: 'Fraunces' }` rule catches
before the parent .fc font-family inherit can do anything. At 1.1rem
it's below the doc's "Fraunces only at ≥18px" threshold, so explicit
Inter override on the deep selector.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per the design rule "italic is for emphasis, not for design", removed
every chrome `font-style: italic` declaration across the frontend.
42 declarations gone across 24 files: empty-state placeholder copy,
loading messages, "no results" hints, ghost text, voice/role labels,
field placeholder text, brand wordmark, et al.
Markdown content emphasis is unaffected — `<em>` and `<i>` tags from
`*emphasis*` markup still render italic via browser default styling
(prose.css doesn't override em behavior). User-typed emphasis in
notes, journal entries, and chat messages keeps its italic.
Specific spots that lost the decorative italic:
- KnowledgeView .filter-label, .empty-narrator
- NoteEditorView .ef-label, link-suggest related
- ChatPanel .empty-msg, .empty-greeting, .role-label
- ChatMessage .role-assistant .role-label (the "Fable" voice tag —
was italic per the doc's Illuminated Transcript spec, but per the
new typography rule the speaker tag stays regular and lets the
border-left + glow do the bubble framing on their own)
- AppHeader .brand-text ("Fabled" wordmark)
- editor-shared.css .title-input::placeholder
- ProjectView .project-title-input::placeholder
- HomeView .urgency-loading
- WorkspaceTaskPanel .empty-group
- WeatherCard .weather-unavailable
- SharedWithMeView .empty-msg
- DiffView empty-state spans
- ToolCallCard .tool-event-more
- SettingsView 7 spots (.you-label, .geo-pending, hint text, etc.)
- + several other empty-state / hint text spots
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KnowledgeView's "Type" / "Tags" filter section headers (.filter-label)
and NoteEditorView's entity-field labels (.ef-label — Email, Phone,
Address etc on person/place notes) were using italic Fraunces accent
at 0.72rem / 0.78rem. The italic + small + decorative-serif combo
read as illegible flourish rather than functional section headers.
Bumped:
- .filter-label 0.72rem → 0.95rem, margin-bottom 6px → 8px
- .ef-label 0.78rem → 0.92rem
Italic Fraunces accent preserved (keeps the branded character that
matches the rest of the surface). Just enlarged to a readable size.
ChatMessage's .role-assistant .role-label kept at 0.8rem italic
Fraunces — that's the doc's Illuminated Transcript voice label, a
decorative speaker tag rather than a functional heading.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
services/projects.py:get_project_summary built task_counts dynamically
from a GROUP BY query, so a project with no done tasks would omit the
'done' key entirely. Frontend's TypeScript interface declares all three
lifecycle keys as required, and ProjectView.vue summed them to render
the Tasks tab counter — undefined + N = NaN.
Two fixes:
1. Backend: initialise task_counts with {todo: 0, in_progress: 0,
done: 0} so the service returns the contract its consumers expect.
Catches the same problem for HomeView's project widget and any
other consumer.
2. Frontend: defensive ?? 0 on the tab-counter sum, so the existing
deploy renders correctly even before the backend rolls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Briefing Settings section was removed during the briefing→journal
migration but its data (locations, temp unit, prep schedule) is still
read by the journal backend. There was no UI to set any of it, so
weather couldn't render and prep timing wasn't tunable.
Re-adds the missing config inside the existing Profile tab — it's all
"about the user" data and a separate Journal tab would just clutter
the sidebar. New sections:
Locations (after Work Schedule)
- Home and Work place-name inputs with on-blur geocoding via
/api/journal/weather/geocode
- Temperature unit toggle (Celsius / Fahrenheit)
- Status messages distinguish ok / pending / error
Journal (before What the Assistant Has Learned)
- Daily prep auto-generate toggle
- Prep generation hour:minute (24-hour input)
- Day rollover hour (so 1–3am entries still count as the previous day)
- All controls disable cleanly when prep is off
Cleanup
- Profile "About You" desc: "chat and briefings" → "chat and the daily journal"
- Profile "Interests" desc: "personalise news and briefing context" →
"personalise the journal's daily prep and chat responses"
- Profile "Work Schedule" desc: "Helps the briefing" → "Helps the journal"
- Profile "What the Assistant Has Learned" desc: clarifies the summary
is included in the journal's system prompt; observations come from
journal + chat (not briefing)
- General "Timezone" desc: "schedule briefings" → "schedule the daily
journal prep"
- Removed the dead `.briefing-*` CSS block (~190 lines of styles for
retired briefing UI: feed-row, slot-row, add-feed-form, etc.) and
replaced with fresh `.location-row`, `.unit-toggle`, `.unit-btn`,
`.checkbox-label`, `.time-row`, `.time-input`, `.geo-msg` rules used
by the new sections. unit-btn.active uses Moss action-primary per
Hybrid; tokens flow through the rest.
The "What the Assistant Has Learned" section was confirmed
load-bearing for the journal — `journal_pipeline.py:139` calls
`build_profile_context()` which feeds learned_summary plus other
profile fields into the journal's system prompt. Not a remnant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "ready" / "cold" / "unavailable" dots in the AppHeader status
indicator were pulling --color-success / --color-warning /
--color-danger which after the foundation pass became Moss /
Warning gold-brown / Error terracotta — all visibly muted. The
green-ready dot in particular read too dark to register as a
"ready light", and the pulse glow (bright emerald) had nothing to
glow off of.
Status dots are indicator *lights*, not semantic-palette UI
elements. Decouple them with hardcoded vital values:
- ready: #4ade80 (matches the existing pulse glow)
- cold: #facc15
- red: #ef4444
Orange already used a hardcoded bright #f97316; left as-is.
The rest of the system continues to use --color-success /
--color-warning / --color-danger semantically (toast-success,
validation errors, etc.) — only the indicator-light contexts get
the brighter palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>