387 Commits

Author SHA1 Message Date
bvandeusen dac5433353 fix(journal): captures panel filter uses date_from and date_to
/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>
2026-05-22 14:59:50 -04:00
bvandeusen 9d70c7be76 fix(journal): rephrase captures-button title to avoid Vue template escape
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>
2026-05-22 10:37:49 -04:00
bvandeusen a73dd17a1b feat(journal): right-rail captures panel + manual curator trigger (Phase 1b)
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>
2026-05-22 10:04:56 -04:00
bvandeusen 39ab5d69a9 feat(voice): admin UI to browse + install piper voices from HuggingFace
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>
2026-05-22 08:18:22 -04:00
bvandeusen a28f75994a feat(voice): swap kokoro TTS → piper-tts
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>
2026-05-22 07:59:09 -04:00
bvandeusen d345b32856 feat(llm): user-controlled think mode (default off); remove qwen3 hardcode
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>
2026-05-21 22:00:44 -04:00
bvandeusen e6f2ee2b94 feat(frontend): pin/unpin/auto-pin UI in HistoryPanel
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.
2026-05-13 14:00:57 -04:00
bvandeusen 59dee3a19f feat(frontend): NoteVersion pin fields + pin/unpin client helpers 2026-05-13 13:59:47 -04:00
bvandeusen b519a1c140 feat(frontend): auto-consolidate tasks toggle in General settings
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.
2026-05-13 12:23:06 -04:00
bvandeusen 257b306a27 feat(frontend): gate body editor when task body is auto-maintained
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.
2026-05-13 12:21:51 -04:00
bvandeusen 8b0878f227 feat(frontend): description field in task editor + Goal block in viewer
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).
2026-05-13 12:20:42 -04:00
bvandeusen bb650ba563 feat(profile): Settings panel with recent journal observations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:44:51 -04:00
bvandeusen c663532fd4 feat(profile): Settings toggle for nightly journal closeout
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:43:59 -04:00
bvandeusen 090b7d83dd feat(frontend): API client for profile observations + closeout flag
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:43:27 -04:00
bvandeusen 36cd08c236 feat(events): expose recurrence presets in EventSlideOver
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>
2026-04-29 20:19:49 -04:00
bvandeusen 2db23cec7a refactor(events): turn EventSlideOver into a centered modal with auto-save
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>
2026-04-29 13:14:17 -04:00
bvandeusen bc239119f3 fix: SettingsView TS errors + calendar month/year font
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>
2026-04-28 07:58:15 -04:00
bvandeusen 30dfbce426 fix(typography): strip chrome italic; reserve italic for emphasis
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>
2026-04-28 07:54:26 -04:00
bvandeusen 65ba1cc82a fix(typography): bump italic-Fraunces section/field labels to readable size
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>
2026-04-28 07:42:05 -04:00
bvandeusen 4cfea784a9 fix(projects): tab counter NaN when a status bucket is empty
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>
2026-04-28 07:40:21 -04:00
bvandeusen 39d56cfcf3 feat(settings): re-add journal config to Profile tab; clear briefing remnants
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>
2026-04-28 00:05:07 -04:00
bvandeusen 3f287d7417 fix(design): brighten status-dot indicator lights
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>
2026-04-27 23:33:33 -04:00
bvandeusen 3c1ec4077f feat(design): surface phase PR 7 — edge surfaces polish
Per the spec, the final per-surface PR bundles the smaller / chrome-y /
library-driven views (Header / Home / Calendar / Graph) together with
EventSlideOver since it's the calendar's primary interaction surface.

Button reclassification per Hybrid rule
- CalendarView btn-new-event: accent gradient → Moss action-primary.
  Creating an event is a workflow action, not a brand moment.
- EventSlideOver btn-primary (Save): accent gradient → Moss
- EventSlideOver btn-secondary (Cancel/No): muted ghost → Bronze
  action-secondary
- EventSlideOver btn-danger-ghost (Delete): hardcoded #ef4444 →
  --color-action-destructive ghost (with filled hover)
- EventSlideOver btn-danger (confirm Delete): hardcoded #ef4444 →
  --color-action-destructive filled

Brand-moment CTAs kept on accent
- HomeView btn-workspace-hero: home-page hero CTA into the active
  project — central brand-moment surface, gradient stays
- HomeView btn-workspace-sm: per-card workspace icon with the
  accent-tinted bg + hover-to-accent pattern, kept

Two-weights-only
- Snapped every font-weight 600/700 to 500 across the bundle
  (HomeView, GraphView, CalendarView, AppHeader, EventSlideOver).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:17:04 -04:00
bvandeusen 541e2ed713 feat(design): surface phase PR 6 — Settings polish
Settings is the densest button surface in the app. Per the surface-
phase spec, classify every button on a deliberate token.

Button reclassification per Hybrid rule
- btn-save (~13 instances across General/Account/Notifications/
  Integrations/Data/Briefing/Admin tabs): flat accent → Moss
  action-primary
- btn-primary (Groups tab "Save Retention", "+ New Group"): flat
  accent → Moss action-primary
- btn-secondary (Detect timezone, Test SMTP, Refresh, Export, Add
  slot, Preview voice, etc.): outline-on-bg-secondary +
  hover-to-accent → Bronze action-secondary, filled
- btn-danger filled (Reset VAPID, Revoke API key Yes): added the
  rule (was previously unstyled — relied on no-rule). Now Oxblood
  action-destructive.
- btn-danger-outline (Invalidate sessions, Clear observations,
  Delete personal data): generic --color-danger → Oxblood
  action-destructive
- btn-danger-sm hover (group + member delete): --color-danger →
  --color-action-destructive
- btn-delete row hover, btn-confirm-delete (user delete two-stage):
  --color-danger → Oxblood. Confirm filled, cancel ghost.
- btn-toggle-open ("Open registration"): flat accent → Moss
- model-delete-btn hover: --color-danger → Oxblood
- btn-warn hover: muted-border-warning → filled-warning

Two-weights-only
- Snapped every font-weight 600/700 to 500.

Out of scope for this PR
- Voice/tone copy audit across labels and help text — per spec,
  opportunistic only. Skipped.
- Settings row de-bordering — current section dividers are
  structural; left as-is.
- Validation-state colors (.input-error etc.) keep --color-danger
  (Error terracotta) per the doc — Error is for validation/error
  messages, Oxblood is for destructive actions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:13:38 -04:00
bvandeusen ff498ce1a4 feat(design): surface phase PR 5 — Project + Workspace polish
Per the surface-phase spec for the Project list, Project view, and
3-panel Workspace cluster:

Button reclassification per Hybrid rule
- ProjectView:
  - btn-workspace ("Open Workspace"): kept on accent gradient — the
    workspace is the project's central feature moment, a Scribe-
    flavored action, brand moment per the doc.
  - btn-share: muted-outline → Bronze action-secondary
  - btn-danger-outline (Delete project): generic-border + danger-on-
    hover → Oxblood outline + filled hover (proper destructive)
  - btn-save-panel: accent gradient → Moss action-primary
  - btn-ms-confirm (milestone confirm): flat accent → Moss
  - btn-ms-cancel: ghost → Bronze action-secondary
  - .modal-btn-danger: --color-danger → --color-action-destructive
- ProjectListView:
  - btn-primary ("+ New Project"): flat accent → Moss action-primary.
    Empty-state CTA (.empty-action) keeps accent — that *is* a
    brand moment per Hybrid, "create your first project".
- WorkspaceTaskPanel + WorkspaceNoteEditor:
  - btn-delete-task / btn-delete: hover --color-danger → Oxblood
  - btn-delete-confirm / btn-confirm-delete: --color-danger pair →
    --color-action-destructive pair, with filled hover for stronger
    affordance
  - btn-save (WorkspaceNoteEditor): flat accent → Moss
- WorkspaceChatWidget:
  - History toggle: replaced unicode ▾ with Lucide ChevronDown
    (PR 1 carry-over for emoji-as-icon)

Two-weights-only
- Snapped every font-weight 600/700 to 500 across the cluster.

Borders kept (per structural-not-decorative rule)
- Milestone group cards: standalone containers, keep
- Project cards in list: standalone containers, keep
- Task slide-over panel: drawer modal, keep
- Workspace 3-panel separators: major section dividers, keep

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:09:33 -04:00
bvandeusen efb3534f3a feat(design): surface phase PR 4 — Knowledge cluster polish
Per the surface-phase spec for the Notes/Tasks viewers and editors:

Long-form line-height
- prose.css: bumped global .prose from 1.6 to 1.7. Applies to Note
  viewer body, Task viewer body, anywhere markdown renders into a
  reading surface. Chat assistant bubble already had the explicit
  override; now consistent with the rest.

Button reclassification per Hybrid rule
- Shared editor-shared.css:
  - btn-save: accent gradient → Moss (action-primary). Saving is
    "operating the software", not a brand moment.
  - btn-delete: --color-danger (Error terracotta) → Oxblood
    (action-destructive). Layout updated for inline-flex so the
    Trash2 icon at call sites lines up alongside the label.
- NoteEditorView, TaskEditorView: Delete buttons now contain a
  Trash2 icon per Hybrid's "destructive paired with icon" rule.
- NoteViewerView, TaskViewerView:
  - btn-edit (and TaskViewer's btn-advance): accent gradient → Moss.
    Switching to edit / advancing status are workflow actions.
  - btn-convert, btn-share: ghost-on-hover-to-accent → Bronze
    action-secondary (alternate paths).

Two-weights-only
- Snapped every font-weight: 600/700 to 500 across editor-shared.css
  and the five Knowledge-cluster views.

Out of scope for this PR (deliberate punt)
- Smaller utility buttons (.btn-suggest-tags, .btn-link-all,
  .btn-add-subtask, the AI-assist generate/proofread/accept/reject
  set, etc.) — currently ghost-styled, generally compliant. Will
  revisit only if they read off in practice.
- Filter chip / sub-task list-row border audit — deferred since
  current styles already lean on background tint for affordance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:02:59 -04:00
bvandeusen 4192a64c0f feat(design): surface phase PR 3 — Chat surface polish
Per the surface-phase spec for the Chat cluster (ChatView, ChatPanel,
ChatMessage, ChatInputBar, ToolCallCard):

Long-form line-height
- ChatMessage: assistant-bubble .message-content jumps from 1.55 to
  1.7 — chat is a reading surface, not a snippet stream. User
  bubbles stay tighter.
- JournalView: dropped its :deep(.role-assistant .message-content)
  override; ChatMessage handles it now and Journal inherits.

ToolCallCard borders
- Removed the outer 1px border. ToolCallCard always renders inside
  an assistant bubble; the bubble already contains it. Background
  tint differentiates without re-bordering. Per the
  structural-not-decorative rule.
- Error state preserved as a 3px left-edge accent in --color-danger,
  mirroring the assistant bubble's own left-edge pattern.

Button reclassification per Hybrid rule
- ChatView .bulk-link "All"/"None": accent text → --color-text-secondary
  (these are tertiary list-control affordances, not brand moments)
- ChatView .bulk-delete-btn: --color-danger (Error terracotta) →
  --color-action-destructive (Oxblood) per Hybrid; paired with a
  Trash2 icon since destructive should always be reinforced by an
  icon, not just color
- ChatView .btn-delete-conv hover: same Error → Oxblood swap
- .btn-new-conv stays accent (brand moment, correct already)
- .btn-send stays accent gradient (primary brand moment, foundation)

Two-weights-only
- Snapped every font-weight: 600/700 to 500 across ChatView,
  ChatPanel, ChatMessage, ToolCallCard per the doc rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:42:44 -04:00
bvandeusen f20e54a068 fix(journal): widen events window to match prep's framing
The right rail's loadEvents was using "now → now + 14 days", which
filtered out events earlier today that had already passed. The prep
uses "today 00:00 → today 23:59" so it includes those events and
explicitly notes they're "in the past". The two surfaces talked
about different sets of events.

Widen the right rail to "today 00:00 → today + 14 days 23:59" so
events from earlier today still surface and group under "Today". The
prep and the right rail now reference the same set within today.
Events further out (next 14 days) keep working as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:55:01 -04:00
bvandeusen 3d916d7357 feat(design): surface phase PR 2 — Journal polish
Per the surface-phase spec for Journal:

- Weather refresh button: ↻ unicode → Lucide RotateCcw (PR 1
  carry-over for emoji-as-icon)
- .journal-title: dropped redundant font-family override (h1 inherits
  Fraunces from theme.css); also snapped weight 700 → 500 per doc's
  "two weights only" rule
- .weather-tab.active, .events-day-label, .event-title, .panel-label:
  snapped 600/700 → 500 per same rule
- Added long-form 1.7 line-height to journal-chat-panel assistant
  bubble content (the daily prep is prose, not chat snippets)
- Removed dead .news-section / .news-card / .news-* / .reaction-btn
  CSS — news functionality was retired with the briefing→journal
  migration (PR #43); styles were never cleaned up

Buttons audited per Hybrid rule: btn-trigger ("Refresh prep") and
weather-refresh-btn are tertiary actions, already correctly styled
as Pewter ghost. .btn-send (Journal Send) flows through ChatInputBar
on the accent gradient — brand moment, kept as-is.

Borders audited per structural-not-decorative: header bottom,
sidebar left, section dividers, form input borders all kept (all
genuinely structural). No decorative borders to remove.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 20:53:09 -04:00
bvandeusen df423ab906 fix(task-editor): prevent preview pane and work log from flex-shrinking
.task-main is a flex column. Children default to flex-shrink: 1, so
when body content is taller than the available column height, flex
squeezes .preview-pane back to its min-height (200px) and the
overflow renders visibly on top of subsequent siblings — making the
TaskLogSection appear in the middle of the body content.

Surfaced by viewing a task with an unusually long markdown body in
preview mode. Pre-existed PR 1, just rarely hit.

NoteEditorView uses the standard block-flow .editor-main and isn't
affected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 20:45:50 -04:00
bvandeusen d023209f13 chore(deps): regenerate package-lock for lucide-vue-next
CI's npm ci step requires lock and package.json to be in sync.
Adds the lucide-vue-next ^0.469.0 entry plus the indirect
@esbuild platform variants that npm pulls in on lockfile refresh.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 20:05:50 -04:00
bvandeusen 93a3beb5d6 feat(design): surface phase PR 1 — Lucide icon migration + scale enforcement
Replaces every hand-inlined SVG in the chrome (60 across 15 files)
with Lucide components. Snaps icon sizes to the doc's 16/24 scale —
all icons in this pass land at 16. AppLogo wordmark stays as the
legitimate "custom app icon" exception per the doc; GraphView's <svg>
mount point for D3 stays as well.

Replaces emoji-as-icons in clear button-affordance cases with their
Lucide equivalents: ✕ → X (close/dismiss buttons across 8 files), ✓ →
Check (ProjectView task-advance button), 🎤 → Mic (ChatPanel voice
CTA), 📎 → Paperclip (ChatView context toggle), &uarr; → ArrowUp
(ChatInputBar Send), &times; → X (ChatPanel context-note remove), ☀/☾
→ Sun/Moon (AppHeader theme toggle).

Inline emoji punctuation in textual confirmation copy ("Saved ✓",
"Created ✓", `done: "✓"` status maps, `'✓' : '📄'` interpolations)
is left for surface-phase voice/tone touchups — replacing requires
structural template changes and is best done per-component.

Stroke weight stays at Lucide's default 2px; tightening to the doc's
1.5/1 is deferred per the surface-phase spec (option ii: drop-in +
scale enforcement).

Adds lucide-vue-next ^0.469.0 to frontend/package.json. Docker
rebuild handles the install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:56:01 -04:00
bvandeusen 7a9a8b7819 feat(design): foundation pass — dusty violet palette, warm parchment light, Inter + JetBrains Mono
Applies the FabledSword + Scribe iteration palette to theme.css end to
end: indigo (#7c3aed) → dusty violet (#5B4A8A) accent, cool grey →
Obsidian/Iron/Pewter dark surfaces, warm parchment (#F5F1E8) light
mode, Inter body + JetBrains Mono code loaded alongside Fraunces, and
neutral hairline scrollbars (chrome is structural, not branded).

Adds the action token set (--color-action-primary Moss,
--color-action-secondary Bronze, --color-action-destructive Oxblood,
--color-action-ghost-border Pewter) but does not yet reclassify any
buttons — surface-phase work. Buttons remain dusty-violet gradients in
the meantime, by design.

Removes deprecated --color-accent-warm; replaces concrete usages with
--color-text-secondary (dates, flavor copy) or --color-warning (paused
status). Sweeps hardcoded indigo literals in component scoped CSS so
they don't bypass the token system.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 17:20:01 -04:00
bvandeusen a3071a53cb feat(routing): land on Journal at root; move Knowledge to /knowledge
Routing changes:
- / now redirects to /journal (Journal becomes the home view)
- /knowledge becomes a real route pointing at KnowledgeView (was a redirect to /)
- /notes redirect target updated from / to /knowledge (the Knowledge surface,
  which is where the notes-related dashboard lives)

To revert (Knowledge as home): change the redirect target on the / route
back to "/knowledge". Knowledge stays a real route either way, so the swap
is a one-line edit.

Nav: Knowledge link in AppHeader now points to /knowledge. The "active" check
simplified to route.path === "/knowledge" since / is no longer Knowledge's
home. The brand logo link stays at "/" — clicks still go "home", which is
now Journal.

Keyboard shortcuts in App.vue (Escape, g h, g t) still navigate to "/" and
correctly land on Journal via the redirect — no change needed there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:46:47 -04:00
bvandeusen c9f2134ad4 feat(journal): conversational LLM-generated daily prep (replaces card)
The structured prep card was data-rich but voiceless. Replaced with an
LLM-generated conversational opener — same shape the briefing's compilation
slot had — that renders as a normal assistant chat bubble at the top of
the day's conversation.

Backend (services/journal_prep.py):
- Renamed generate_daily_prep -> gather_daily_sections (still pure data
  fetching, no LLM); kept the old name as a backwards-compat alias.
- New _generate_prep_prose: hands the gathered sections to generate_completion
  with a warm-conversational system prompt; returns prose. Falls back to a
  plain greeting if the LLM call fails or no model is configured.
- ensure_daily_prep_message now persists the prep as role='assistant' with
  the prose as content. Structured sections stay on msg_metadata for
  provenance. Auto-upgrades legacy system-role preps in place on next call.

Frontend:
- Drop the <article class="daily-prep"> structured block from JournalView.
  The prep is now just the first chat bubble — picks up the existing
  Illuminated Transcript styling automatically.
- Drop dayMessages / prepMessage / prepSections / asArray helpers — no
  longer needed.
- ChatMessage hideMessage filter: comment refined to clarify it only
  catches LEGACY system-role prep rows. Current preps are assistant-role
  and render normally.

Net effect: open /journal -> first thing you see is a warm assistant bubble
that talks about your day -> input bar below to reply.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 15:42:30 -04:00
bvandeusen dda62265c9 fix(journal): drop dangling JournalSetupWizard import
The setup-wizard component was deleted in the RSS hard-cut (it was
briefing-config-shaped), but JournalView still imported it — breaking
the Vite build.

Removed the import + showWizard / wizardChecked / checkSetup /
onWizardDone plumbing. JournalView now mounts straight into the day view.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 12:53:06 -04:00
bvandeusen dbd9f00061 refactor: hard-cut RSS infrastructure (scope C)
Removes the entire RSS feature surface — feeds, items, embeddings, reactions,
discussion-note flow, briefing news context, settings, env-vars, and DB
tables. Keeps the URL-generic article-reader (the read_article LLM tool)
under a clean module so the LLM can still fetch arbitrary article content
from URLs the user provides.

Backend:
- New services/article_fetcher.py — single source of trafilatura URL→text
- New services/tools/article.py — read_article tool (was nested under tools/rss)
- Delete services/rss.py, rss_classifier.py, rss_filtering.py, article_context.py
- Delete services/tools/rss.py
- Delete models/rss_feed.py (RssFeed, RssItem), models/rss_item_embedding.py
- services/embeddings.py: drop upsert/semantic_search/backfill RSS helpers
- services/llm.py: remove _build_briefing_article_context, briefing-conv branch,
  ARTICLE_DISCUSS_SEED skip-RAG branch; drop get_rss_items / add_rss_feed from
  the actions list
- services/generation_task.py: drop _maybe_save_article_discussion_note + caller
- routes/chat.py: drop /api/chat/from-article/<id> endpoint
- routes/journal.py: re-import via web.py refactor (article_fetcher path)
- services/tools/__init__.py: register `article`, drop `rss`
- services/tools/_registry.py: drop the requires=='rss' check
- app.py: drop backfill_rss_item_embeddings + backfill_rss_article_content tasks
- config.py: prose-only edit (no env var change — RSS env vars were never first-class)

Frontend:
- stores/settings.ts: drop rssEnabled
- SettingsView.vue: drop the RSS-classification mention
- api/client.ts: drop openArticleInChat (the from-article endpoint is gone)

Tests:
- Delete tests/test_rss_service.py, test_news_api.py, test_article_reading.py

Migration:
- 0042_drop_rss: DROP TABLE rss_item_embeddings, rss_item_reactions, rss_items,
  rss_feeds; DELETE settings rows for rss_enabled / briefing_*_topics

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 12:33:30 -04:00
bvandeusen cacfcac86a fix(journal): restore weather + events panels; hide daily-prep system msg in chat
The journal UI was over-stripped earlier — weather panel, current-conditions
poll, and the upcoming-events sidebar were all dropped. Restored those (calls
the new /api/journal/weather, /api/journal/weather/current,
/api/journal/weather/refresh, /api/journal/weather/geocode endpoints).

Also: the daily-prep system message was rendering as flat text inside
ChatPanel because there's no .role-system bubble styling. Added a hideMessage
guard in ChatMessage so daily-prep system messages don't render in the chat
stream — they're already shown above as a structured prep card.

News / RSS reactions / article-discuss are intentionally NOT in the journal
(scoped out per user direction). The broader RSS infrastructure cleanup is
a separate, larger task.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 12:22:51 -04:00
bvandeusen 873e70c7ea feat(journal): add JournalView UI with /journal route + nav link
Restores a working browser surface for the journal feature, which was
left UI-less when Stage F of the original plan was deferred. JournalView
is a fresh write (not a rename of BriefingView) — drops the briefing-
specific weather panel, news cards, RSS reactions, article-discuss
button, and setup wizard, since none of those have journal-backend
equivalents.

What it does:
- Fetches /api/journal/today on mount; shows today's daily-prep card
  (rendered from msg_metadata.kind === 'daily_prep' sections)
- Day picker via /api/journal/days lets you switch to past days
- Refresh-prep button hits /api/journal/trigger-prep
- Center is the existing ChatPanel pinned to today's journal conversation
  (so the chat input + SSE + tool-call cards inherit unchanged)
- Right sidebar keeps the upcoming-events list (uses the generic
  /api/events endpoint, not a briefing-specific one)

Also:
- Replace the dead Briefing API client functions with Journal equivalents
  (getJournalConfig, saveJournalConfig, getJournalToday, getJournalDay,
  getJournalDays, triggerJournalPrep, list/update/deleteJournalMoment).
- Remove NewsView.vue — it was orphaned (no route, no nav) and depended
  on the deleted /api/briefing/* endpoints. If a standalone news surface
  is wanted later it'll need to be rebuilt against new endpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 11:54:06 -04:00
bvandeusen 7602bf2293 feat(briefing): hard-cut tear-down
Backend:
- Delete briefing services (pipeline, scheduler, conversations, profile, tools)
- Delete routes/briefing.py + remove blueprint registration
- Move _get_temp_unit into services/weather.get_temp_unit (reads top-level temp_unit setting)
- Rename briefing_preferences.py → rss_filtering.py (functions are RSS-specific)
- Strip briefing scheduler hooks from app.py
- Strip briefing scheduler call from routes/settings.py
- Update test imports (test_rss_service, test_tz_helpers)

Frontend:
- Delete BriefingView, BriefingSetupWizard, BriefingToolStatusRow
- Strip /briefing route + nav links (AppHeader, KnowledgeView)
- Strip Settings → Briefing tab + state + functions + imports
- Strip briefing-intermediate handling from ChatMessage
- Hide /news route + nav links (NewsView depended on briefing endpoints; orphaned in tree)
- Drop unused useSettingsStore from AppHeader

The Android BriefingScreen lives in a separate repo and is not touched here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:33:37 -04:00
bvandeusen e652dece9b refactor: rename Fabled Assistant to Fabled Scribe
Updates user-visible branding across frontend (PWA manifest, page title,
Settings, push fallback), backend (email templates and subjects, LLM
system prompt, CalDAV displayname, SMTP from-name default), README,
quickstart compose, and MCP server description.

Also updates the CI image path and quickstart image reference to
git.fabledsword.com/bvandeusen/fabledscribe in preparation for the
Forgejo repo rename. Internal Python package, env vars, and database
schema unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:28:25 -04:00
bvandeusen f422d4158c style(workspace-notes): larger, more spacious note title
Title input bumps to 1.4rem with Fraunces serif (matches the
design language's heading treatment) and gains 0.9/1.1rem padding
on the title row for breathing room.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:52:58 -04:00
bvandeusen 70ec060b7a style(workspace-notes): move active-note indicator to right edge
Anchors it to the editor side rather than the tasks-panel side.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:43:59 -04:00
bvandeusen 3b41a45757 style(workspace-notes): drop border between toolbar and body
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:35:45 -04:00
bvandeusen 19e1ca505d style(workspace-notes): unify editor chrome, distinguish rail
- Drop border-bottom lines between header / title / tags / toolbar
  so they read as one continuous input surface.
- Add a single border-top above the editor body so the body's
  existing boundary stays clear.
- Notes rail gets --color-bg-card so it no longer visually merges
  with the tasks panel (both previously shared --color-surface).
  Dropped the rail's border-right since the tint now carries the
  division from the editor pane.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 13:30:17 -04:00
bvandeusen f75c40b417 refactor(workspace): match KnowledgeView mini-chat dock style
- Widget is now a bottom-anchored dock spanning the content width
  (max-width 1200px, rounded top corners, bg-secondary, upward shadow)
  instead of a floating corner box.
- Drop the closed state and ✕ button — widget is always present,
  matching KnowledgeView's pattern. Collapsed = input-only dock,
  expanded = full ChatPanel above the input.
- Grid ratio 1fr:2fr → 0.85fr:2.15fr so tasks get a touch less room
  and notes a touch more.
- Workspace note-rail (the notes picker column inside the editor)
  widens from 155px to 200px so note titles breathe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 12:53:02 -04:00
bvandeusen 649c0f124b feat(workspace): history dropdown on project chat widget
Clicking the ▾ button reveals conversations where
rag_project_id === projectId, most-recent first. Selecting one
repoints the widget (and the workspace_conv_{pid} storage key) to
that conversation without deleting the previously active one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:10:54 -04:00
bvandeusen 6026c24450 refactor(workspace): two-column grid; widget drives tool-call refresh
WorkspaceView now renders tasks (1fr) on the left and notes (2fr) on
the right with the floating WorkspaceChatWidget layered on top. The
widget owns the SSE streamingToolCalls watcher and emits note-changed
and task-changed events; the view listens and reloads the affected
panel refs.

Dropped from the view:
- ChatPanel mount, empty-state quick chips, and Chat header toggle
- The conversation lifecycle + SSE watcher (moved into the widget)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:09:50 -04:00
bvandeusen ae5c61a179 feat(workspace): floating project chat widget
Modeled on KnowledgeView's mini-chat. Three states (closed handle,
collapsed input-only, expanded full panel). Takes over the project
conversation lifecycle from WorkspaceView (resume-or-create on mount,
delete-if-empty-and-new on unmount) and adds a restart action. Reuses
ChatPanel for expanded view and ChatInputBar for collapsed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:08:25 -04:00
bvandeusen cac62c6caf feat(briefing): add 2-week upcoming events list below weather card
Fetch and display the next 14 days of events in the briefing right column,
grouped by day with color dots, time, and location. Fills the empty space
below the weather card.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:14:20 -04:00