59 Commits

Author SHA1 Message Date
bvandeusen a77b71e0e2 fix(journal): drop broken WeatherCard from journal screen
The WeatherCard widget expected a flat shape (`current_temp`,
`condition`, `today_high`, ...) but the prep payload sends raw
OpenMeteo data nested under `forecast_json` per location. Every
field read as null, so the card pinned to the top of the journal
chat showed "null°" and "null/null" — visual noise covering up
the prep prose.

The prep prose itself already mentions weather plainly ("Weather at
home will reach a high of 15.9° with a 0% chance of precipitation"),
so the visual card is redundant. Removing it eliminates the bug
without losing any actual info; the weather data is still in
metadata.sections.weather if a future reader needs it.

Also deletes the unused widget file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:54:22 -04:00
bvandeusen 76aff4ea9e feat(offline): tier 2 phase 4 — per-row pending-sync indicators
A small cloud-upload glyph appears next to any row whose id has a
queued offline write (offline-created temp ids and pending edits both
count). Tooltip reads "Pending sync — will save when online". Renders
nothing during normal online operation so the list stays clean.

- DB: `watchPendingIds(domain)` streams the union of target_id and
  temp_id across the queue, scoped per domain.
- Per-domain Riverpod stream providers for notes / tasks / projects.
- New `PendingSyncBadge` widget — used by KnowledgeItemCard (both
  list and grid variants), `_ProjectCard`, and `_TaskRow` in the
  project workspace.

flutter analyze clean; 21 tests pass. Closes #147 — all four phases
of Tier 2 offline mode are in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:14:36 -04:00
bvandeusen fe10067761 feat(offline): tier 2 phase 2 — extend cache to tasks/projects/milestones/events/conversations
Phase 1 cached only notes. Phase 2 brings the read-through pattern to every
domain the app reads in bulk:

- Drift schema v2: 5 new cached_* tables + onUpgrade migration. Calendar
  events use range-scoped read/write (replaceEventsInRange) so disjoint
  month fetches don't clobber each other; milestones are per-project.
- Wrap TasksRepository, ProjectsRepository, MilestonesRepository, and
  ChatRepository (conversation list only) with NetworkException fallback.
  Writes hit the API then sync the cache.
- New EventsRepository wrapping EventsApi; calendar_provider and
  event_form_sheet repointed at the repository.
- OfflineBanner now surfaces getLatestSync() — most-recent timestamp
  across all domains — so the hint reads sensibly regardless of screen.

flutter analyze clean; existing tests pass. Phase 3 (offline write queue)
and Phase 4 (read-only UI indicators) still to come.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:22:06 -04:00
bvandeusen b9e68e3bc8 feat(design): surface phase — Lucide icons, input radius, Illuminated Transcript, ActionColors
Per-screen application of the design system to the Flutter app.
Mirrors the web's surface phase landed in FabledScribe v26.04.28.1.
Foundation port shipped in 0f05f47; this is the surface work.

Lucide icon migration
- Added lucide_icons ^0.257.0 dependency
- 107 Material Icons references → LucideIcons across 21 files. Drop-in
  IconData swap (Icon(LucideIcons.X) instead of Icon(Icons.x)).
- Lucide import added to each touched file.

Input border radius
- theme.dart inputDecorationTheme borderRadius 24 → 8 in both light
  and dark themes. Doc says radius-md (8px) for inputs; previous pill
  shape was Material default that the doc deviates from.

Illuminated Transcript pattern (ChatMessageBubble)
- User bubble: accent-tinted border → neutral Pewter (scheme.outline).
  Asymmetric corner already correct (bottomRight 4px).
- Assistant bubble: topLeft corner 4 → 16; only bottomLeft stays 4
  (the "tail" effect, mirroring web's `border-bottom-left-radius: 4px`).
  Background switched from surfaceContainerHighest (Slate) to surface
  (Iron) per the doc spec "card surface".
- Assistant bubble glow shadow added — accent-tinted blur (28px alpha
  0.14) + depth shadow (8px alpha 0.4 black). Mirrors web's
  --color-bubble-asst-shadow.

ActionColors wiring (Hybrid rule)
- 5 'Delete' confirm buttons across notes / tasks / chat conversations
  / calendar event sheet → Oxblood action-destructive via the
  ActionColors ThemeExtension defined in the foundation port. Foreground
  for ghost/text variants, backgroundColor for filled.
- Calendar event Save button → Moss action-primary. The first call
  site to wire ActionColors.primary; serves as the pattern for future
  Save reclassifications.
- Other Save buttons (note edit, task edit, project edit, etc.) still
  flow through colorScheme.primary (dusty violet) and read as
  brand-moment. Reclassifying those is deferred — the wiring pattern
  is established and can be applied incrementally as files are touched.

Indigo cleanup
- 4 hardcoded #7C3AED / #5B21B6 literals → dusty-violet equivalents
  (#5B4A8A / #3F3560). Spots: project_tasks_screen color fallback
  (×2), journal_screen gradient.

Verification
- flutter analyze: No issues found

What's deferred
- Per-screen Save / Cancel reclassification beyond the calendar event
  Save button. Wiring pattern established; rollout opportunistic.
- Long-form 1.7 line-height on assistant Markdown content (would
  require MarkdownStyleSheet work; minor).
- Surface walk on Knowledge / Projects / Settings screens for any
  hardcoded styling that needs touch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:14:28 -04:00
bvandeusen dd250788f6 feat(journal): replace briefing surface with journal; remove news/RSS
The backend retired /api/briefing/* and the RSS feature entirely. This
Flutter change mirrors what landed web-side: rename the briefing surface
to journal, repoint at /api/journal/*, and drop the news/RSS UI since
its endpoints no longer exist.

New (mirrors briefing structure with adapted shapes):
- lib/data/api/journal_api.dart — getToday, getDay, getDays, triggerPrep
- lib/data/models/journal_day.dart — {day_date, conversation, messages}
- lib/providers/journal_provider.dart — async notifier, sendReply, polling,
  silent refresh, regeneratePrep. Mirrors the briefing notifier 1:1
- lib/widgets/journal_prep_card.dart — adapted briefing_digest_card
- lib/screens/journal/journal_screen.dart — adapted briefing_screen,
  weather card preserved (rendered from msg_metadata.sections.weather
  on the daily-prep assistant message). News cards / RSS reactions /
  article-discuss removed
- lib/screens/journal/journal_history_screen.dart — past days picker
  pulls /api/journal/days, drills into /api/journal/day/<iso>

Wiring:
- Routes.briefing → Routes.journal (constants.dart)
- Routes.news removed
- briefingApiProvider → journalApiProvider (api_client_provider.dart)
- newsApiProvider removed
- app.dart: shell tab "Briefing" → "Journal"; News destination removed
  from nav rail, bottom nav, and the More sheet
- splash_screen.dart and login_screen.dart: redirect Routes.journal
  instead of Routes.briefing
- chat_api.dart: drop openArticleInChat (calls deleted /api/chat/from-article)
- settings_provider.dart: drop rssEnabled getter and rssEnabledProvider

Deleted:
- lib/screens/briefing/ (whole directory)
- lib/screens/news/ (whole directory)
- lib/data/api/briefing_api.dart, news_api.dart
- lib/data/models/briefing_conversation.dart, briefing_feed.dart, news_item.dart
- lib/providers/briefing_provider.dart, news_provider.dart
- lib/widgets/briefing_digest_card.dart, news_card.dart
- test cases for NewsItem and BriefingFeed in test/widget_test.dart

flutter analyze: 0 issues.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 07:58:09 -04:00
bvandeusen 01aa362d3c feat(offline): detect server offline and keep returning users signed in
Previously, if the backend was unreachable at launch, the splash screen
routed to the login screen. The server URL remained persisted in
SharedPreferences but visually appeared lost, frustrating any user who
isn't the service operator.

- New AuthStatus.offline distinguishes network failures (NetworkException)
  from HTTP 401 in AuthNotifier.verify().
- Persist has_ever_logged_in flag on first successful verify/login.
- Offline + ever-logged-in lands on the briefing with a sticky offline
  banner (retry button) instead of being punted to login.
- OfflineBanner widget is Tier-2-ready so we can surface "last sync X min
  ago" once real caching lands (Fable task #147).
2026-04-18 12:58:18 -04:00
bvandeusen aba0ca6256 feat: gate News tab and briefing news cards on server rss_enabled setting
Fetch server-side settings on app launch via new serverSettingsProvider.
Hide News from bottom sheet and NavigationRail when RSS is disabled.
Skip news card rendering in briefing messages when disabled.
Refactor tab navigation to use dynamic tabs list and route-based refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 14:29:05 -04:00
bvandeusen 70a3279192 feat: background update download, snackbar prompt, chat image rendering
Update provider: auto-downloads APK in background after finding a newer
version, prompts via snackbar only when ready, cleans up old APKs on
startup. Replaces modal dialog with dismissible snackbar.

Chat bubble: resolve relative image URLs (/api/images/{id}) against
server base URL with auth cookies so search_images results render on
the phone app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 12:38:48 -04:00
bvandeusen fc6c9648f9 fix(voice): eliminate dual-recorder conflict and defunct element crashes
VadHandler is now the sole mic owner — removed separate AudioRecorder that
caused audio focus contention on Android. Audio from onSpeechEnd is encoded
as WAV for Whisper. Provider switched to autoDispose to match widget lifecycle,
preventing defunct element assertions. Recording UI deferred until mic is open.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 08:04:03 -04:00
bvandeusen 58d4cfab4d feat: tablet landscape improvements
Shell: move Projects/News/Calendar into ShellRoute so the NavigationRail
persists across all screens. Show all 6 nav destinations on tablet
instead of 3+More overflow. Phone bottom nav unchanged.

Chat: master-detail layout on tablet — conversations list (320px) with
inline chat panel. Tapping a conversation updates state instead of
pushing a new route.

Knowledge: responsive grid (2 cols portrait, 3 cols landscape) with
card layout showing icon, title, body preview, and tags. Fix snippet
data — read json['snippet'] which the API actually sends. Bump snippet
length from 120 to 200 chars. Tasks now show description as snippet.

News: responsive grid with the same breakpoints. Larger snippet
(5 lines) in grid mode via snippetMaxLines parameter.

Briefing: centered reading column (maxWidth 700) on wide screens,
matching the web UI layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 01:31:52 -04:00
bvandeusen 48c134ce6a feat(voice): pulse mic button with live amplitude
VoiceState now carries a normalized mic amplitude (0..1) updated
from the existing onAmplitudeChanged subscription, with a 0.02
change threshold so we don't spam rebuilds.

VoiceMicButton swaps the constant-rate AnimationController for an
AnimatedScale + animated glow driven by the live amplitude. 0.1
floor keeps the button breathing on silence; 120ms ease-out smooths
between 200ms samples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 22:46:48 -04:00
bvandeusen fa84e40efc feat(briefing): stall watchdog + pull-to-refresh; chat attaches to in-flight streams
Briefing chat now uses the same StreamIterator + 45s per-event timeout
as the main chat provider, so a dropped SSE socket no longer leaves
isBriefingStreaming stuck true. Adds refreshMessages() that unfreezes
state when the server-side message is complete, wired to a new
pull-to-refresh gesture and the app lifecycle-resume hook.

Chat provider gains attachToGeneration() — safe to call unconditionally
on screen init, so landing on a conversation that's already mid-stream
(e.g. the /news discuss button, which now creates a conv and auto-kicks
generation server-side) picks up live tokens instead of freezing on the
placeholder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 07:55:47 -04:00
bvandeusen 75b7d6d0fe feat(chat): stall watchdog, pull-to-refresh, and unfreeze on refresh
Mobile chat occasionally froze with the input disabled and nothing
progressing — the underlying cause is SSE sockets dropping silently on
mobile network handoffs / proxy idle timeouts. Dio never observes the
close, so the send loop's `await for` hangs indefinitely with
isStreaming=true.

Three changes:
- Wrap the SSE stream in a StreamIterator with a 45s per-event timeout
  as a stall watchdog. On timeout, break out and let the polling pass
  reconcile state from the server.
- Add pull-to-refresh on the chat message list and an AppBar refresh
  button with inline spinner feedback. Works on both empty and
  populated states.
- MessagesNotifier.refresh() now also clears isStreaming /
  streamingStatus when the server reports the latest assistant message
  as complete — so a stuck UI unfreezes the moment the user pulls down
  or taps refresh, even if the original SSE loop is still hanging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:13:46 -04:00
bvandeusen 3e888b6458 fix: eliminate pull-to-refresh flicker by holding stale data during re-fetch 2026-04-08 14:39:50 -04:00
bvandeusen 6c29b685e8 feat(theme): shift Android palette from indigo to deep violet to match web identity 2026-04-08 13:54:39 -04:00
bvandeusen d2582f9111 feat: reduce resume cooldown to 30s; add pull-to-refresh to News, Calendar; refresh chat on resume 2026-04-07 17:43:46 -04:00
bvandeusen 946b70ecc4 fix: use dialog context in event delete confirmation
Same navigator mismatch as the chat delete bug — outer context inside
showDialog builder resolves to the wrong navigator ancestor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:35:41 -04:00
bvandeusen 6ea268bf58 fix: use dialog context in delete confirmation to prevent shell nav pop
Navigator.pop(outerContext) inside a showDialog builder resolves to the
ShellRoute's nested navigator instead of the root navigator where the
dialog lives, popping the conversations route and showing a black screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:33:04 -04:00
bvandeusen 79dce1a01c feat: wire discuss button in briefing RSS cards, cap cards at 3
Adds discussArticle() to BriefingApi and wires it through to the
RSS news cards in BriefingScreen so tapping Discuss opens a chat
conversation seeded with the article. Also caps RSS cards per
message at 3 to avoid overly long briefing threads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:49:16 -04:00
bvandeusen cb3a09756f fix: create conversations with empty title so server auto-names them
The app was creating conversations with title 'New conversation'.
The server only generates a title when conv_title is falsy (empty).
With a non-empty title, should_gen_title is False for the first
message (msg_count % 10 != 0), so auto-naming never fired.

Now creates with empty title (matching web app behaviour). The list
still displays 'New conversation' as a UI placeholder until the
server-generated title arrives.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:46:23 -04:00
bvandeusen d530920284 fix: load tasks via TasksApi in Knowledge view, remove tab counts
Tasks are not part of the knowledge API (backend rejects type=task).
When the Tasks tab is selected, fetch from /api/tasks and convert to
KnowledgeItem. Also removes per-type counts from tab labels to prevent
tabs from awkwardly resizing when counts load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 12:36:00 -04:00
bvandeusen e2a358a158 feat: show tool status notifications in chat during generation
Parse SSE `status` events alongside `chunk` events and display the
status text next to the spinner while the assistant is generating.
Previously the Android app discarded all non-chunk SSE events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 11:46:42 -04:00
bvandeusen 5b639dbd4c feat: add CalendarScreen with month strip and agenda list 2026-04-06 10:32:13 -04:00
bvandeusen 334882520c feat: add EventFormSheet with CRUD, recurrence picker, and color chips
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:31:08 -04:00
bvandeusen e7d174cef7 feat: add NewsScreen with feed filter, reactions, and discuss 2026-04-06 08:29:30 -04:00
bvandeusen 95d0f529ea fix: invalidate project tasks on return from task edit screen 2026-04-06 07:15:35 -04:00
bvandeusen 1b08b2fd9e feat(voice): integrate voice mode into Briefing screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 14:47:38 -04:00
bvandeusen 2231c60bfb feat(voice): integrate voice mode into Chat screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 14:45:53 -04:00
bvandeusen d97f7b0ebd feat(knowledge): wire 4-tab shell, retire LibraryScreen, complete Knowledge View feature 2026-04-05 00:03:23 -04:00
bvandeusen 2ab24a99a8 feat(knowledge): add edit button to ProjectTasksScreen app bar 2026-04-04 23:56:40 -04:00
bvandeusen e39d31fe43 feat(knowledge): add ProjectsScreen and ProjectEditScreen, sort projects by updated_at 2026-04-04 23:56:18 -04:00
bvandeusen a50193dbc0 feat(knowledge): update Project model, ProjectsApi sort, and NoteEditScreen noteType support 2026-04-04 23:52:10 -04:00
bvandeusen f5ba2d25a3 feat(knowledge): add KnowledgeScreen with tabs, search, tag filters, and infinite scroll 2026-04-04 23:50:19 -04:00
bvandeusen 23509adfa8 feat: news story cards in Android briefing screen
Replaces bare Story-N reaction rows with full NewsCard widgets: source label,
relative timestamp, linked headline, 2-line snippet, and 👍/👎 reactions.
Reads rss_items from message metadata (requires backend ≥ this sprint).

Adds url_launcher ^6.3.1 for opening article links in the browser.
Adds https/http <queries> entries to AndroidManifest for Android 11+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 00:08:51 -04:00
bvandeusen 7fce19a37c feat: silent background polling for briefing screen
Timer.periodic every 60s calls silentRefresh() on BriefingNotifier.
silentRefresh() patches AsyncData directly — never triggers AsyncLoading —
so existing content stays on screen while the fetch is in flight.
WidgetsBindingObserver pauses polling when app is backgrounded.
Polling is also skipped while a reply is actively streaming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 17:43:06 -04:00
bvandeusen 46d0427901 feat: weather card and RSS reactions in briefing screen
- Parse metadata field on Message model (weather, rss_item_ids)
- Add WeatherCard widget: location, current temp, condition, today hi/lo,
  delta from yesterday, scrollable 3-day forecast strip
- BriefingScreen shows WeatherCard above the first assistant message that
  carries weather metadata (mirrors web BriefingView.vue behaviour)
- RSS reaction buttons (👍/👎) shown below assistant messages with
  rss_item_ids; optimistic toggle with rollback on failure
- Add postRssReaction / deleteRssReaction to BriefingApi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 17:30:26 -04:00
bvandeusen 5ca5856f15 Project task view: tapping a project shows milestone-grouped task list
- ProjectLibraryCard.onTap navigates to /projects/:id/tasks
- New ProjectTasksScreen: milestone sections with coloured dot,
  done/total counter and thin progress bar; unassigned tasks at bottom
- Status cycle icon updates optimistically (pendingStatus map) so
  the list doesn't flash on every tap; syncs global tasksProvider
- New route /projects/:id/tasks registered in app.dart
- TasksApi.getByProject + TasksRepository + projectTasksProvider (family)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 21:40:14 -04:00
bvandeusen 0971433c7a refactor: remove briefing digest card, show full message list directly
The summary card duplicated the first assistant message and consumed
screen real estate without benefit on small screens. Replaced with:
- Full message list via ChatMessageBubble from the top
- SliverFillRemaining empty state ("No briefing yet today" + generate button)
  when no messages exist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 19:08:21 -04:00
bvandeusen 844f68d376 fix: update loop, ghost queue item, briefing card scroll trap
- Update dialog no longer re-appears on every shell re-mount; check()
  is skipped if status is already available/upToDate/downloading
- Offline queue dequeue now happens before the mounted check, preventing
  ghost items when the widget disposes mid-drain
- Briefing digest card and messages now share a CustomScrollView so
  expanding the card doesn't trap the user without scroll access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:23:24 -04:00
bvandeusen a687e3637f feat: upgrade Riverpod 3, go_router 17, google_fonts 8, package_info_plus 9
Migrate StateNotifierProvider/StateNotifier → NotifierProvider/Notifier,
StateProvider → NotifierProvider with simple notifier, FamilyAsyncNotifier
removed in Riverpod 3 (replaced with constructor-arg pattern), and rename
.valueOrNull → .value throughout all providers and screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 06:21:17 -04:00
bvandeusen def7519feb fix: resolve all flutter analyze warnings and infos
- app.dart: add braces to single-statement if body
- briefing_api.dart: escape <id> in doc comment (unintended HTML)
- notes_api.dart, tasks_api.dart, projects_api.dart: use null-aware
  map elements (?'key': value) instead of if-null guards
- task_edit_screen.dart: remove unused tasks_api.dart import
- task_edit_screen.dart, project_selector.dart: suppress
  deprecated_member_use on DropdownButtonFormField.value (value is
  the controlled-widget param; switching to initialValue would break
  current-selection display)
- project_selector.dart: use _ instead of __ for ignored error params
2026-03-12 00:36:05 -04:00
bvandeusen e86d1a59af fix: redirect to /briefing after auth (was /notes which no longer exists) 2026-03-12 00:32:38 -04:00
bvandeusen 6232c7c99a feat: app overhaul — Briefing-first navigation, Library, capture queue
Plans 1-3 implemented:

Plan 1 — Foundation
- Add google_fonts ^6.2.1
- lib/core/theme.dart: slate-indigo ColorScheme (dark/light), Fraunces
  headings, GradientButton widget
- lib/providers/capture_work_queue_provider.dart: in-memory sequential
  work queue; CaptureWorkQueueNotifier drains one item at a time;
  captureResultProvider feeds snackbars to UI
- lib/app.dart: wire fabledDarkTheme/fabledLightTheme; replace blocking
  _QuickCaptureBar with queue-based implementation (progress bar, badge)

Plan 2 — Navigation
- 3-tab shell: Briefing · Library · Chat (was Notes · Tasks · Projects · Chat)
- lib/screens/library/library_screen.dart: unified notes+tasks+projects list
  with filter pills, status sub-filter for tasks, live search, FAB
- lib/widgets/library_item_card.dart: NoteLibraryCard, TaskLibraryCard
  (status cycle), ProjectLibraryCard
- lib/screens/chat/conversations_tab_screen.dart: focused replacement for
  ConversationsListScreen
- Delete 5 dead screens: notes_list, tasks_list, project_list,
  conversations_list, quick_capture

Plan 3 — Briefing
- lib/data/models/briefing_conversation.dart
- lib/data/api/briefing_api.dart: getToday, getHistory, getMessages,
  triggerSlot
- lib/providers/briefing_provider.dart: BriefingNotifier with sendReply
  (optimistic + SSE + poll, same pattern as MessagesNotifier) and refresh
- lib/widgets/chat_message_bubble.dart: extracted + redesigned shared bubble
  (ghost user bubbles, left-accent assistant bubbles)
- lib/widgets/briefing_digest_card.dart: expandable first-message card
- lib/screens/briefing/briefing_screen.dart: digest card, conversation list,
  streaming reply bar, refresh button, history overflow menu
- lib/screens/briefing/briefing_history_screen.dart: read-only past dates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 23:17:38 -04:00
bvandeusen 3c3055d536 Improve project-task integration and sub-task support
- TasksApi: add getSubTasks(parentId) + parentId param to create()
- TasksRepository: expose new API methods
- app.dart: invalidate projectsProvider + projectMilestonesProvider
  after quick capture creates a task; taskNew route reads ?projectId=
  query param and passes as initialProjectId to TaskEditScreen
- project_list_screen.dart: _AddTaskRow passes projectId query param
  so the task editor pre-fills the project on navigation
- task_edit_screen.dart: fully rewritten with initialProjectId support,
  sub-task fetching/creation, and _SubTasksSection widget

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:59:58 -05:00
bvandeusen 868cb0e49e Add milestone data layer and redesign Projects tab
Milestone data layer:
- lib/data/models/milestone.dart: Milestone model with progress fields
  (total, completed, pct) from backend progress endpoint
- lib/data/api/milestones_api.dart: CRUD client for
  /api/projects/:id/milestones
- lib/data/repositories/milestones_repository.dart: thin repository wrapper
- lib/providers/milestones_provider.dart: FutureProvider.family keyed by
  project ID — lazily fetched per project when expanded
- api_client_provider.dart: registers milestonesApiProvider and
  milestonesRepositoryProvider

Projects tab redesign (project_list_screen.dart):
- Each project is now an ExpansionTile showing open task count in subtitle
- Expanding a project fetches its milestones and groups tasks under milestone
  headers with inline progress bar (completed/total count)
- Tasks show status icon, due date with overdue highlighting, priority dot
- 'No milestone' section at bottom for unassigned tasks
- Tapping a task navigates to the task editor
- Long-press on project title opens archive/complete/delete bottom sheet
- 'New task' button at bottom of each expanded project section

Bump version to 26.03.02+1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:33:45 -05:00
bvandeusen fceae5529d Add Projects feature; sync Note/Task models with backend additions
## New: Projects
- Project model, API, repository, Riverpod provider
- ProjectListScreen: active/archived sections, create dialog, long-press status/delete
- ProjectSelector widget: DropdownButtonFormField for note + task editors
- Projects tab added to shell (bottom nav + navigation rail, 4th position)
- /projects route registered in GoRouter ShellRoute

## Updated: Note model
- Added tags: List<String>, projectId: int?, milestoneId: int?
- NotesApi.create/update pass tags and project_id
- NotesRepository and NotesNotifier signatures updated
- NoteEditScreen: chip-based tag input + ProjectSelector

## Updated: Task model
- Added projectId: int?, milestoneId: int?, parentId: int?
- TasksApi.create passes project_id; update payload includes project_id
- TasksRepository and TasksNotifier signatures updated
- TaskEditScreen: ProjectSelector added; project_id sent on save

## Provider fix
- ProjectsNotifier.update renamed to updateProject to avoid conflict
  with AsyncNotifier.update(FutureOr<State> Function(State)) base method

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:52:59 -05:00
bvandeusen abf91874c3 Show specific error reason on setup screen connection failure
Instead of a generic "Could not reach server" for all failures, show
the actual cause: SSL cert error, timeout, or the raw connection error
message. This makes it possible to diagnose setup issues on-device
without needing a debugger.

Also extend connect timeout from 5s to 10s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:48:49 -05:00
bvandeusen ccaec61de2 Fix setup screen always rejecting valid servers
Dio throws DioException for 4xx responses by default, so a server
returning 401 on /api/auth/status (correct behaviour for an
unauthenticated request) was caught and shown as "Could not reach
server", preventing the URL from ever being saved.

Set validateStatus: (_) => true so any HTTP response (200, 401, 404…)
counts as "reachable". Only actual network failures (no response) now
trigger the error message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:38:01 -05:00
bvandeusen 467fa6a553 Migrate flutter_markdown → flutter_markdown_plus; fix app label
flutter_markdown was discontinued; flutter_markdown_plus is the
maintained drop-in replacement (same API, updated imports only).

Also set android:label to "Fabled" so the app appears correctly
on the home screen and in the app drawer instead of "fabled_app".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:26:01 -05:00
bvandeusen ad3b317115 Add self-hosted update system via Forgejo releases
The app now checks for new APK releases on a Forgejo instance and can
download and install them without leaving the app.

Settings:
- New "Update repository URL" field (e.g. https://git.example.com/user/fabled_app)
- "App version" tile with a manual "Check" button and live status display
- Download progress bar and "Install" button appear when update is found

Startup:
- Shell runs a silent background check on first load when a repo URL is set
- A dialog appears automatically if a newer release is found

Provider (update_provider.dart):
- UpdateNotifier: idle → checking → available → downloading → available
- Hits Forgejo /api/v1/repos/{owner}/{repo}/releases/latest
- Parses semver tag, finds .apk asset, downloads with progress via Dio
- Opens the APK with open_file to trigger the system package installer

Android:
- REQUEST_INSTALL_PACKAGES permission
- FileProvider configured for open_file (shares APK URI with installer)
- res/xml/file_paths.xml covers external-files and cache directories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:08:17 -05:00