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>
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>
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>
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>
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).
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
- 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
- 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>
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>
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>
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>
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>
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>