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>
Without a consistent signing key, each CI build was signed with the
ephemeral debug keystore from the Flutter container — causing Android
to reject updates with INSTALL_FAILED_UPDATE_INCOMPATIBLE.
Secrets stored in repo: RELEASE_KEYSTORE_BASE64, RELEASE_KEYSTORE_PASSWORD,
RELEASE_KEY_ALIAS. CI decodes the keystore and writes key.properties
before building so every release APK is signed identically.
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>
On shell mount, reads the IANA timezone from the device via
flutter_timezone and POSTs it to PUT /api/settings as user_timezone.
Mirrors the web app's App.vue behaviour so briefing events and the
chat route use the correct local time on Android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android 8+ requires canRequestPackageInstalls() to return true even when
REQUEST_INSTALL_PACKAGES is declared in the manifest. Add permission_handler
and check/request Permission.requestInstallPackages before downloading;
redirects user to Settings if not granted and shows a clear error message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The global receiveTimeout is 30s but quick capture runs LLM inference
that can take much longer. receiveTimeout fired, fell through to the
NetworkException fallback in dioToApp, and the work queue queued it as
an offline item.
- quick_capture_api.dart: override receiveTimeout to 120s for the
/api/quick-capture request
- api_client.dart: handle receiveTimeout/sendTimeout explicitly in the
error interceptor, converting them to AppException (not NetworkException)
so slow responses are never mistaken for being offline
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>
- Check OpenResult.type before resetting state — previously a failed
open_file call (e.g. installer blocked on emulator) silently called
state = const UpdateState(), nulling latestVersion and downloadUrl,
causing "Version null" and a non-functional Download button
- Show errorMessage in dialog with error colour so failures are visible
- Guard Download button on downloadUrl != null (no button if URL lost)
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>
pubspec.yaml now holds a placeholder (0.0.0+0) that is never updated.
CI strips the v* tag to derive --build-name (e.g. 26.03.12) and
--build-number (e.g. 260312) and passes them to flutter build apk.
signingConfigs.create() was running unconditionally, casting null
properties to String before the buildTypes guard could take effect.
CI has no key.properties so the cast threw. Now falls back to debug
signing when the keystore file is absent.
The narrow-layout body Column had no SafeArea, so the child screen's
AppBar was adding the full status bar height on top of the height already
consumed by _QuickCaptureBar's own SafeArea. MediaQuery.removePadding
(removeTop: true) on the Expanded child tells the inner AppBar the top
inset is already handled.
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>
The ? operator on map entries applies to the key, not the value.
String literal keys can never be null, so ?'key': value was flagged as
invalid_null_aware_operator. Reverted to if (x != null) 'key': x form
and disabled the lint project-wide since it doesn't apply here.
- 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
actions/checkout and actions/upload-artifact are JavaScript actions.
When container: is set, act runs them inside the container (not on the
host runner), but ghcr.io/cirruslabs/flutter:stable has no Node.js.
Replace both with equivalent shell commands:
- checkout: git clone + git checkout SHA
- artifact upload: curl to Forgejo API (best-effort, non-blocking)
The release APK attach step was already a pure shell step and is unchanged.
dev push → artifact named fabledapp-dev-<sha>
main push → artifact named fabledapp-<sha>
tag push → artifact + Forgejo Release
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tag pushes (v*) now build the APK and create a Forgejo Release with
the APK attached as a downloadable asset, in addition to uploading the
workflow artifact. Requires RELEASE_TOKEN secret (write:repository scope).
To release: git tag v26.03.09 && git push origin v26.03.09
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI skips on changes to platform dirs (ios, linux, macos, web, windows),
docs, and README. Build skips on test-only changes since tests don't
affect the release APK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ci.yml: flutter analyze + flutter test on every push, runs in
ghcr.io/cirruslabs/flutter:stable container (includes Flutter, Dart,
Java, Android SDK)
build.yml: release APK build on main push, uploaded as workflow artifact
(downloadable from Forgejo Actions UI, retained 30 days)
No secrets required. Uses existing swarm-runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After OpenFile.open() the state was reset to UpdateStatus.available,
which re-triggered the app.dart listener (downloading → available
satisfies the prev != available condition) and showed the dialog again.
Fix: reset to idle so the system installer can proceed uninterrupted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>