[CI verification only — do not merge] prove Rust-from-source CI works - #3
Closed
BrawlerXull wants to merge 38 commits into
Closed
[CI verification only — do not merge] prove Rust-from-source CI works#3BrawlerXull wants to merge 38 commits into
BrawlerXull wants to merge 38 commits into
Conversation
Removes the deprecated HTTP sync path so all synchronization flows through
the native TaskChampion Rust FFI bridge, matching the single-path target
architecture.
- Delete lib/app/v3/net/{fetch,add_task,complete,delete,modify,origin}.dart
and lib/app/v3/db/update.dart (the HTTP push-sync helper).
- Rewrite saveCredentials() to validate credentials via the native sync_()
FFI call instead of an HTTP GET /tasks probe; drop the taskReplica branch.
- Remove the taskc-HTTP sync call sites (home_controller.refreshTasks and its
callers, show_tasks, taskc_details, home_page_app_bar) while preserving all
local SQLite operations.
- Rebrand the 8 ccsync* localization keys to syncServer* and reword copy from
"CCSync" to "TaskChampion sync server" across all 9 language files; collapse
the mode-branched sync-URL label to the single path.
- Drop the obsolete HTTP fetchTasks test group and its generated mocks; realign
the localization snapshot tests to the reworded strings.
http stays in pubspec (still used by pushNotification_service.dart).
flutter analyze: 0 errors. No test regressions vs main (pre-existing headless
plugin/MethodChannel failures unchanged).
Follow-up hardening after an adversarial review of the CCSync retirement:
- saveCredentials(): validate the entered credentials with sync_() BEFORE
persisting them via setTaskcCreds(), so invalid credentials are never
written to the active profile when validation fails. (The prior order —
which mirrored the proposal's example — persisted first, leaving bad creds
on disk while telling the user the check failed.) Validation is a live sync,
so saving now requires connectivity to the sync server.
- Remove the orphaned taskchampionBackendUrl localization getter from the
abstract Sentences class and all 8 locale implementations; it became dead
when the credentials view's mode-branched URL label collapsed to the single
syncServerBackendUrl path.
- Fix a stray non-Urdu token ("używaj") left in syncServerEasySyncTitle on a
line the rebrand already touched.
flutter analyze: 0 errors. Full suite unchanged at +317 -18 (no new failures).
…attributes Overhauls the Rust FFI bridge (Deliverable 2): Build config - Pin Cargo.toml to edition 2021 for broader Rust/flutter_rust_bridge compatibility; add the thiserror dependency. - Add rust/build.rs: rerun-on-source-change tracking, target-platform detection (exposed via the TC_HELPER_PLATFORM compile-time env), and an opt-in (FRB_CODEGEN=1) binding-regeneration trigger. Modularization & typed errors - Extract the duplicated storage-open boilerplate into storage.rs, the task serializer into serialize.rs, and a thiserror-based TcHelperError into utils/error.rs. api.rs is now a thin FFI layer of delegators. - Remove every .unwrap() panic; each FFI entry point returns Result<_, String> so flutter_rust_bridge surfaces failures to Dart as catchable exceptions with a real message, instead of relying on panics. Surfaced task attributes - The serializer now emits annotations, dependencies (depends), is_blocked, is_blocking, and recur, which the Dart TaskForReplica model previously anticipated but never received. TaskForReplica gains those fields + parsing (reusing the existing Annotation model). - urgency is intentionally NOT surfaced: TaskChampion 2.0.3 does not compute or store an urgency value on Task, so there is nothing authoritative to expose. Regenerated the flutter_rust_bridge bindings for the new signatures (getAllTasksJson stays Future<String>; the mutating calls are now Future<void>, throwing on error — all existing Dart callers ignored the old int return). Deferred (needs mentor coordination + CI cross-compilation, not doable here): purging the tracked android jniLibs *.so binaries. There is currently no Android cargo/cargo-ndk integration, so Gradle bundles the committed .so directly; removing them before automated cross-compilation exists would break Android builds. cargo build + cargo test: pass. flutter analyze: 0 errors. Full test suite unchanged at +317 -20 (pre-existing headless failures only). Runtime FFI behaviour still needs validation via an on-device build.
Adds a self-contained Hugo site and the CI to publish it (Deliverable 5).
Site (website/)
- Themeless Hugo site (all layouts in-repo, no submodule/theme fetch): landing
page, downloads page, and a docs skeleton, with a small dark stylesheet.
- A `nightly-builds` shortcode renders the build log; the downloads page shows
the most recent nightly APKs, newest first, with per-build status.
- Ships a custom-domain CNAME (taskwarrior.ccextractor.org).
Build log
- scripts/update_build_log.py prepends {ts, sha, msg, status, artifact, build}
to website/data/nightly_builds.json (keeps the 90 most recent). The file ships
empty ([]) and is populated by CI.
CI
- build-nightly.yml: daily at 02:00 UTC (+ manual) — builds the signed nightly
APK, records the result via the collector, and commits the log to main.
- deploy-website.yml: on pushes touching website/** — builds with Hugo and
deploys to GitHub Pages via actions/deploy-pages.
- nightlydepolyci.yml: ignore website/** and scripts/** so a build-log commit
does not trigger a redundant APK rebuild.
Verified locally: warning-free `hugo --minify` build (Hugo 0.164.0), and the
collector + shortcode round-trip (entry renders on the downloads page; empty
state renders when the log is []).
Deviation from the proposal, documented in website/README.md: uses the modern
GitHub Actions Pages deployment instead of renaming fdroid-repo -> public-site,
so the site source stays in website/ on main with no dedicated deploy branch.
Manual infra steps (enable Pages, DNS, branch protection) are listed there.
Follow-up to the CCSync retirement: the profile page still surfaced the "CCSync" brand -- the "changed profile mode to CCSync" snackbar label and a commented-out "CCSync (v3)" mode radio. Rebrand the label to "Taskchampion" (matching the retained TW3C mode) and delete the dead commented block.
pubspec used a caret constraint, so `flutter pub get` could resolve the newer 2.12.0 runtime while the committed FFI bindings were generated with codegen 2.11.1. That mismatch crashed the app at RustLib.init() on startup. Pinning the exact 2.11.1 (matching rust/Cargo.toml's =2.11.1) keeps the Dart package, generated bindings, and native library in lockstep.
…rust_bridge to exact 2.11.1 Adds test_dependencies_and_annotations_surface asserting depends[]/is_blocked/is_blocking and annotation entry(RFC3339)+description. Pins flutter_rust_bridge (was ^2.11.1) to avoid resolving 2.12.0, which crashes at RustLib.init().
The committed .so predated the serializer overhaul (Feb build vs June source), so the app ran stale native code. Rebuilt via cargo-ndk. Stopgap until CI compiles from source (see build-tc-helper.yml).
- home_page_body: reactively read tasksFromReplica in the Obx so the replica list rebuilds when the async FFI fetch completes (was empty: taskReplica flips true before the list is populated). - task detail: surface annotations/depends/is_blocked/is_blocking/recur (read-only) for replica tasks. - safe_tour: guard tutorial_coach_mark.show() against unmounted target keys (fixes recurring 'obtain target position (null)' FormatException); applied to profile + manage-task-server tours.
Regenerates the native lib from rust/ on each run so it can't go stale; builds the APK against it. Unverified on CI. armeabi-v7a left as a documented TODO (needs aws-lc-sys bindgen).
The Taskchampion (replica) home never showed tasks despite the FFI returning them. Four compounding bugs:
- show_tasks_replica wrapped its body in an Obx that read no observable -> ObxError -> blank error widget. Reactivity now lives in the parent (home_page_body) Obx, so this is a plain build.
- home_controller.onInit never initialized pendingFilter/waitingFilter from their persisted values, so pendingFilter defaulted false and the list filtered for completed tasks only, hiding all pending ones.
- the project filter treated an empty-string projectFilter ('') as an active filter, dropping every task whose project is null.
Verified on-device: replica home lists tasks; detail view surfaces annotations/depends/is_blocked/is_blocking/recur.
…gen; rebuild both ABIs 32-bit ARM has no pre-generated aws-lc-sys bindings, so it needs the crate's 'bindgen' feature (libclang). Scoped that feature to ONLY [target.armv7-linux-androideabi] so arm64/iOS/host keep using pre-generated bindings (forcing bindgen globally broke the iOS link). Rebuilt both Android ABIs from source; CI installs libclang + builds both. Host cargo test green.
tc_helper only ever uses the remote TaskChampion sync server (ServerConfig::Remote). taskchampion's default 'sync' feature also pulled server-aws + server-gcp, dragging in the entire AWS + Google-Cloud SDKs and aws-lc-rs/aws-lc-sys. Switched to default-features=false + [server-sync, bundled] (ureq + ring). Wins: - iOS xcframework now builds (aws-lc-sys 0.30.0 failed to cross-compile PQ/kyber for iOS); rebuilt device arm64 + fat simulator from the new Rust. - No more bindgen hack for 32-bit ARM (aws-lc gone); dropped libcrc_fast entirely. - libtc_helper.so 29M->6.2M (arm64), 20M->4.7M (v7a); release APK 60M->36M. - Smaller dependency/attack surface; faster builds; simpler CI (no cmake/ninja/libclang). Verified: host cargo test (2) pass; both Android ABIs + all 3 iOS targets build; real-server sync returns Ok() (ureq+ring); on-device the replica home lists tasks + detail rows render with the new lib, no RustLib version crash.
The search box only fed the local-taskc list (searchedTasks); the replica view (TaskReplicaViewBuilder) read tasksFromReplica directly and ignored the query, so typing in search did nothing in TaskChampion mode. Add a reactive searchQuery (kept in sync by search()/toggleSearch()) and filter the replica snapshot by task.description in home_page_body's Obx, so the list narrows on each keystroke and restores when the query is cleared. Matching is case-insensitive substring, consistent with the taskc path.
Expand the site beyond the initial skeleton: - New Features page (wired into the nav) covering the full task model, offline-first behaviour, native TaskChampion sync, and cross-platform reach. - New Docs guides that auto-list on /docs/: getting-started, sync-setup, architecture (with a data-flow diagram), and an FAQ. - Home page: three more feature cards + an "Explore all features" link. - CSS: style markdown tables, code blocks, and blockquotes on content pages.
Saving TaskChampion credentials validated and synced the replica, but the home task list was only ever populated by an explicit refresh. So right after configuring sync for the first time the user saw an empty list (until a manual refresh or an app restart) even though the sync had succeeded — which reads as "sync is broken". After a successful save, reload the sync-mode flag and run a sync+refresh on the HomeController so the freshly-synced tasks appear at once. Guarded with Get.isRegistered and a try/catch so a transient refresh failure can never flip an already-successful credential save. Verified on-device against a live TaskChampion server: configuring a fresh profile now lands on a populated task list without a manual refresh.
Two cosmetic issues in the profile sync UI: - The "changed profile mode to<Mode>" snackbar was missing a space before the mode name (rendered "...mode toTaskchampion"). Add the separator. - The per-profile config action was hard-coded to "Configure Taskserver" even for Taskchampion (v3) profiles. Label it from the profile's actual mode so a v3 profile reads "Configure Taskchampion" (the destination screen was already correct; only the label was wrong). Both verified on-device across TW2<->TW3C mode switches.
The right-drawer Sort By offers 8 columns, but the replica task list only
handled Modified / Due till / Priority — Created, Start Time, and Project fell
through to `default: return 0`, so those buttons did nothing. Priority also
sorted alphabetically ('H' < 'L' < 'M'), which is the wrong severity order.
- Add `entry` to TaskForReplica (the Rust serializer already emits it; the
model just dropped it) so "Created" can sort.
- Handle Created (entry), Start Time (start), and Project in the replica sort
switch, alongside the existing Modified / Due till.
- Sort Priority by severity rank (H > M > L > none) instead of alphabetically.
- Drop the redundant pre-sort; unsorted/unsupported columns (Tags, Urgency —
Urgency isn't surfaced by taskchampion 2.0.3) fall back to newest-modified.
Filters (Status Pending/Completed, Project) were already correct — verified.
Sort verified on-device against a live server: Created- matched the CLI's
entry order exactly; Priority- correctly floated H tasks to the top.
The Sort By chips built their sort value from the *localized* label
(filterDrawerCreated etc.), but every sort switch matches hardcoded English
keys ('Created+', 'Modified-', ...). So in any non-English locale the stored
value (e.g. Hindi "निर्मित+") never matched, and all sorting silently did
nothing — in the local task list too, not just the replica.
Pair each option with a stable English key (used as the sort value and matched
by the switches) plus the localized display label. The chip shows the
translated word; the stored value stays locale-independent.
Verified on-device in Hindi: tapping निर्मित (Created) now sorts by entry —
order matched the CLI's entry ordering exactly.
These were the last two no-op sort options in Taskchampion mode. - Tags: sort by the task's tags sorted-and-joined, so tasks sharing tags group together and untagged tasks fall to one end. - Urgency: TaskChampion 2.0.3 doesn't compute or store urgency (it's a Taskwarrior-CLI concept), so add TaskForReplica.computeUrgency() — a faithful port of Taskwarrior's default urgency algorithm and coefficients (priority 6/3.9/1.8, due 12, next-tag 15, active 4, age 2 over 365d, annotations/tags/ project 1, blocking 8, blocked -5, waiting -3). `scheduled` and UDAs are omitted (not surfaced by the serializer / not present). Verified: 14 unit tests cover the formula term-by-term; on-device, Urgency- matched the `task` CLI's own urgency ranking (React Redux 24.80 on top), and Tags- surfaced exactly the tagged tasks grouped by tag.
Brings Taskwarrior's report system to the app — named bundles of filter + sort + columns, instead of hand-building a filter/sort each time. - ReportDefinition / ColumnSpec / SortCriterion models (+ Taskwarrior `report.*.sort`/`.columns` string parsing). - VirtualFilterEngine: the 8 virtual tags (+ACTIVE/+READY/+BLOCKED/+BLOCKING/ +OVERDUE/+WAITING/+PENDING/+COMPLETED/+DELETED), attribute filters (status:/project:/priority:), negation (-TAG), and compound AND expressions, evaluated over TaskForReplica (the active TaskChampion path). - ReportService: the 9 core default reports (next/active/ready/blocked/waiting/ completed/recurring/overdue/all) + an executor (filter -> multi-key sort, reusing computeUrgency). Custom reports sort above defaults; same-named custom overrides a default. - TaskrcParser: extracts user-defined reports (those with a .sort) from a .taskrc, marked isCustom. Note: under TaskChampion a "waiting" task is pending with a future wait (no distinct status), so the waiting report uses the +WAITING virtual tag. 22 unit tests cover filters, executor, report merge, and .taskrc parsing.
Wires the report data layer into a screen and the app: - ReportEngineController: loads the report catalogue (custom .taskrc reports first, then defaults) and runs the selected report over a fresh replica snapshot. - ReportEngineView: a report picker (custom grouped above defaults, each with its description + filter expression) that opens a filtered+sorted task list with a count, pull-to-refresh, and tap-through to task detail. - TaskrcService: loads user-defined reports from an optional .taskrc in the app documents dir (path surfaced in the UI); silently no-ops if absent. - New /report-engine route + binding; a Reports icon in the home app bar. Verified on-device against the live server: the picker lists all 9 default reports; "next" filtered to status:pending; "overdue" applied the +OVERDUE virtual filter (35 tasks, matching the CLI's overdue set) sorted due+.
…ng engine - +OVERDUE matched non-pending tasks. Taskwarrior's OVERDUE virtual tag only ever applies to pending tasks; a completed/deleted task with a past due date is never "overdue". Confirmed on the live server: 13 completed tasks have a past due date and would have wrongly matched a bare +OVERDUE filter (e.g. in a future custom .taskrc report not ANDed with status:pending). Fixed by requiring status == 'pending' in evaluateTag's OVERDUE case. - project: used a raw prefix match (startsWith), so `project:work` would also match an unrelated project like `workshop`. Taskwarrior's project filter is hierarchical: a match requires an exact match or a dot boundary (`work`/`work.sub`, not `workshop`). Fixed _matchAttribute accordingly. - Hardware/gesture back button on the report engine screen was inconsistent with the in-app back arrow: from report results, tapping the AppBar arrow correctly returns to the picker first, but the hardware back button popped straight to Home, skipping the picker. Confirmed on-device. Fixed by wrapping the screen in PopScope and routing both back paths through one _handleBack() so they can't drift apart again. 2 new regression tests added (+OVERDUE excludes non-pending; project: dot boundary) — full suite 38/38 passing. Verified on-device: hardware back now correctly does results -> picker -> home; existing reports (overdue: 36 tasks, all pending) show no regression.
…mpty state
runReport() swallowed any exception from Replica.getAllTasksFromReplica() and
just cleared the results list, so a genuine failure (e.g. replica not
configured, FFI error) rendered identically to "this report legitimately has
zero matches" — the user had no way to tell a broken sync from an empty report.
Add a hasError flag, set on catch, and show a distinct message in that case
("Couldn't load tasks for this report...") with pull-to-refresh to retry,
instead of the generic "No tasks match this report."
Verified on-device: normal report runs are unaffected (hasError stays false,
same task counts as before).
Audited profile management, home controller, and the add-task flow (areas not yet reviewed this session) via parallel code review, then verified each candidate by reading the exact source and tracing the real mechanism before fixing — two initially-flagged "silent data loss" claims about saveTask()/copyWith() turned out to be false positives (Rust's update_task_impl does a targeted merge of only the supplied keys, so annotations/depends/recur are never touched by a save) and were correctly discarded. Confirmed and fixed: - profiles.dart deleteDatabase(): checked whether a profile's .db file exists, then deleted the *unrelated* `current-profile` marker file instead of the db file it just confirmed existed (copy-paste error). Now deletes the db file itself. The old code could silently corrupt which profile is "current" when deleting an unrelated profile, and never actually freed the db file's space. - splash_controller.dart changeModeTo(): unconditionally called selectProfile(currentProfile.value) after changing ANY profile's mode. selectProfile() unconditionally clears HomeController.tasks as a side effect, so changing an inactive profile's sync mode silently emptied the *active* profile's visible task list (TW2 mode) until a manual refresh — even though nothing about the active profile changed. Now only refreshes live state when the profile being modified is the one actually active. - home_controller.dart _profileSet(): the waiting-filter reload logic read the persisted value, and if it was true, called toggleWaitingFilter() (flipping it to false) before reading it back — so a profile with the waiting filter enabled had it silently disabled every time HomeController initialized (i.e. every app cold start). Simplified to a direct read, matching the already-correct pendingFilter pattern on the line above it. - add_task_bottom_sheet_new.dart: neither save handler (TW2 or TW3C/replica) reset `selectedDates` after adding a task, unlike every other field (description, project, tags, priority). A due/wait date picked for one task silently carried over and got applied to the next task added, in both modes. Verified: flutter analyze clean (0 errors, same pre-existing infos/warnings as baseline); full test suite shows the same failure count before and after (confirmed by diffing test/utils/taskfunctions/profiles_test.dart specifically against a stash of the pre-fix state) — no regressions. On-device live verification for the profile/task-list-wipe and date-carryover fixes is pending device reconnection (adb lost the connection mid-session).
…hing) Continued the audit into task_database.dart (legacy TW2 SQLite layer), TaskForC (lib/app/v3/models/task.dart), and profile/directory switching in home_controller.dart. Each candidate verified against the actual source before fixing. - task_database.dart updateTask(): `taskDepends` was populated from `task.tags` instead of `task.depends` (copy-paste error). setDependsForTask() does a full delete-then-insert, so every legacy-mode task update silently destroyed the task's real dependency relationships and replaced them with its own tags. Now reads task.depends. - task.dart TaskForC.fromJson(): annotations were hardcoded to an empty list regardless of the JSON payload (the debugPrint right above it even checked whether json['annotations'] was null, suggesting awareness the field could be present) — every legacy-mode task load silently dropped its annotations. Now parses them via Annotation.fromJson(). Also: `urgency` is declared nullable but fromJson() force-called `.toDouble()` on it with no null check, a crash risk on any task with no urgency value. Now null-safe. - task_database.dart: openForProfile() didn't await _open(), and 6 methods (get/setTagsForTask, get/setDependsForTask, get/setAnnotationsForTask) called ensureDatabaseIsOpen() without awaiting it, unlike every other call site in the file. Narrow race window (only matters before the db has ever been opened), but a real, fixable inconsistency. - home_controller.dart refreshTaskWithNewProfile() / changeInDirectory(): both re-pointed `storage` to the new profile/directory but then just called _refreshTasks() with whatever filter/sort/tag state happened to be in memory from the PREVIOUS profile — never reloading the new profile's own persisted Query/storage.tabs state. So switching profiles (or changing the base directory) silently carried over the old profile's project/tag/sort filters onto the new profile's tasks. Both now call _profileSet() to fully reload state from the profile actually being switched to. - home_controller.dart _profileSet(): selectedTags.addAll(...) only ever accumulated tag filters. Since _profileSet() now also runs on every profile switch (not just app boot), a plain addAll would leak the previous profile's tag filters into the new one. Changed to assignAll() (a proper replace, confirmed via RxSet source: clear() + addAll()). Ruled out as false positives during verification (documented for the record, not fixed): TaskForC "silent annotation loss" claims in save/complete paths turned out fine in an earlier round (Rust does a targeted merge); this round, `SplashController.baseDirectory()` "called as a method" is GetX's Rx.call() idiom (sugar for .value), used identically in 5+ files across the app — not a bug. Verified: flutter analyze clean (0 new errors/warnings beyond pre-existing baseline); 4 new unit tests for TaskForC.fromJson (annotations + nullable urgency) all pass; full test suite shows the same pre-existing failure count as before (headless-environment sqflite/plugin unavailability, unrelated). On-device live click-through still pending — device has not reconnected.
Addresses the "compile automatically on every build" half of the Rust
deliverable (Issue-tracker item 2b): instead of relying on a pre-built .so,
`flutter run` / `flutter build` now compile the tc_helper native library from
rust/ so the packaged binary always matches the current Rust source.
- Adds a `cargoBuildTcHelper` Gradle task wired into `preBuild` that runs
cargo-ndk for arm64-v8a + armeabi-v7a, emitting libtc_helper.so straight into
src/main/jniLibs/<abi>/ (single jniLibs source — avoids the "Duplicate
resources" sourceSet-merge error you get from a second jniLibs dir).
- Resolves the Android NDK automatically (android.ndkDirectory, else the newest
under $SDK/ndk) and adds the Android std targets on demand, so a fresh Rust
toolchain self-heals.
- If cargo/cargo-ndk aren't on PATH the task skips with a warning and the build
falls back to the committed .so, so a checkout without a Rust toolchain (and
today's CI, which has none yet) still builds. When the toolchain IS present, a
real compile error fails the build so problems surface loudly.
Verified locally: with the committed jniLibs removed, a clean
`flutter build apk --flavor production` compiled both ABIs from source and
packaged lib/{arm64-v8a,armeabi-v7a}/libtc_helper.so into the APK; with them
present, the build still succeeds (the fresh compile overwrites them in place).
Follow-up (item 2b, remainder): once the Android-building CI workflows install
the Rust toolchain, the committed jniLibs/*.so can be git-ignored, removed from
the tree, and purged from history; iOS/desktop can get analogous compile hooks.
…pion The app had two scattered TaskChampion utility folders — utils/taskc/ (the sync protocol payload/message/response/codec types plus the reporting engine's virtual_filter_engine + taskrc_parser) and utils/taskchampion/ (credentials storage). Merge the former into the latter so all TaskChampion utilities live under one directory, following the existing lib/app/utils/<area>/ convention. Pure relocation: files moved via git mv, all import paths rewritten (utils/taskc/ -> utils/taskchampion/), and the mirrored test files moved to test/utils/taskchampion/. No logic changed. Verified: flutter analyze 0 errors; the moved protocol tests (codec/message/ payload/response) all pass; on-device the app builds (Rust auto-compiled), launches, and the reporting engine runs a report end-to-end (exercising the relocated virtual_filter_engine at runtime).
The app carried two parallel task models — TaskForC (local/Taskserver, which
stores entry/modified as strings) and TaskForReplica (TaskChampion, which
stores them as epoch seconds). Shared logic had to be hard-typed to one of
them, so filtering, sorting, reports and urgency only ever worked for a single
sync mode.
Introduce TaskLike: the canonical read contract both models now implement.
Most attributes already had compatible types, so implementing it required no
field changes; the two attributes whose representations genuinely differ are
exposed as normalized accessors (entryDate/modifiedDate), letting each model
keep its own storage format.
- lib/app/models/task_like.dart — the contract, plus shared date
normalization (parseTaskDate/epochToDate).
- lib/app/models/task_urgency.dart — Taskwarrior's urgency algorithm, lifted
off TaskForReplica and retyped to the contract so every model gets the same
ranking. TaskForReplica.computeUrgency() now delegates, so its public API
and behaviour are unchanged.
- VirtualFilterEngine and ReportService are now generic over
<T extends TaskLike>, so they work for both models while preserving each
caller's concrete element type (no List<TaskLike> leaking into callers).
- ReportService sorts entry/modified via the normalized accessors, with
explicit null-ordering (missing dates sort first ascending) and a urgency
cache that no longer collides when uuid is null.
Net effect: the local/Taskserver path gains reports, virtual-tag filtering and
urgency for free, and the reports/burndown view unification is now unblocked.
Two real bugs were caught by the new edge-case tests while writing this, both
in the shared date parser, and both from trying DateTime.tryParse before the
specific formats (which made those branches dead code):
- a Taskwarrior compact stamp without a trailing Z was read as *local* time
and then shifted by the device's timezone — the same data would resolve
differently per user;
- epoch seconds given as a string were read as a year ('1717243200' became
the year 171726). Specific formats are now matched first, with a
minimum-digit guard so a short numeric value is never silently read as a
1970s timestamp.
Verified: flutter analyze 0 errors; 29 new edge-case tests covering date
formats, cross-model equivalence (identical tasks must produce identical
urgency/filter/report results through either model), sorting null-ordering,
null-uuid caching, empty lists and all-null/garbage-date tasks; the existing
urgency and reporting suites still pass unchanged; the full suite sits at the
pre-existing -18 baseline, so no regressions. A release APK builds, compiling
all 25 consumer files of both models.
On-device runtime pass is still pending — the test phone has been dropping off
adb; the previous modularization step was verified on it end-to-end.
Deleting a task in the app destroyed it outright. delete_task_impl called TaskData::delete(), which purges the record from the replica — the `task purge` equivalent — so the task vanished everywhere: unrecoverable, no audit trail, and invisible to any "deleted" view on any client. Confirmed against the CLI: after deleting in the app, the task could not be found at all, not even as status:deleted. Taskwarrior's own `task delete` is a *soft* delete. Match it: - rust: delete_task_impl now sets Status::Deleted via replica.get_task() instead of purging via get_task_data()/TaskData::delete(). The record survives, still syncs, and can be restored. - rust: update_task_impl no longer forces Status::Pending before applying the caller's map. That unconditional reset meant any update without an explicit "status" silently resurrected a completed or deleted task — which would also have quietly undone the soft delete above. Status is now only written when the caller actually supplies it. With deleted tasks preserved, they need somewhere to be seen, so the status filter becomes three-way instead of a pending/completed boolean: - Query gains a string status filter (pending/completed/deleted) that migrates from the persisted boolean on first read, so an existing profile keeps whatever it was showing. The legacy getPendingFilter() is kept in step for the call sites still reading it. - cycleStatusFilter(includeDeleted:) advances the cycle; "deleted" is only offered on the TaskChampion path, the only one that retains deleted tasks, so other sync modes keep the original two-way toggle. - HomeController exposes statusFilter; the replica list filters on it directly rather than on the boolean; the drawer renders all three labels. - New filterDrawerDeleted string added across all 9 language files. Verified end-to-end on-device against a live server: deleting a task in the app now leaves status=deleted on the server (previously the record was gone), the task is restorable, the drawer cycles Pending -> Completed -> Deleted, and the Deleted view lists it. 4 Rust tests (2 new: soft-delete semantics, and status preservation across an update), flutter analyze 0 errors, Dart suite at the pre-existing -18 baseline.
The burndown feature carried one widget per (period x sync mode):
burn_down_{daily,weekly,monthly} x {base,taskc,replica}. The nine files were
near-identical — same tooltip, same stacked pending/completed series, same
axes, same legend — and differed only in three things: where they fetched
tasks from, which date field they bucketed by, and their caption text.
Replace them with:
- burn_down_data.dart — BurnDownEntry (a task reduced to just date + status)
and bucketBurnDown(), the daily/weekly/monthly grouping. Keeping the chart
input this narrow is what lets one implementation serve every mode: the app
has three task models (built-value Task, TaskForC, TaskForReplica) and each
caller maps its own into this shape.
- views/burn_down_chart.dart — the single chart widget, parameterised by
period plus caption/axis suffixes.
The three report screens now map the tasks they had *already fetched* and
render all three tabs from the shared widget. That also removes a double
fetch: each screen fetched tasks for its empty-state check, then every chart
re-fetched the same list.
Behaviour is deliberately preserved, including each mode's existing choice of
date field (replica buckets by `modified`, taskc/base by `entry`) and the
existing captions, so the charts render exactly as before.
Also documented a PRE-EXISTING quirk found while writing the tests rather than
silently changing it: Utils.getWeekNumbertoInt is ceil(daysSinceJan1 / 7), so
weekly buckets are fixed 7-day windows from Jan 1, not calendar weeks — two
days in the same Mon-Sun week can land in different buckets. Asserted in a
test so it is visible; making the charts follow real calendar weeks would be a
deliberate behaviour change in Utils, not part of this refactor.
13 files -> 5, 2033 lines -> 820. 12 new unit tests for the bucketing.
flutter analyze 0 errors; suite at the pre-existing -18 baseline. Verified
on-device in replica mode: Daily, Weekly and Monthly all render with the same
captions, axes and stacked series as before, and the totals roll up
consistently across the three periods.
Follow-up left alone: ReportsController still holds the now-unused
dailyInfo/weeklyInfo/monthlyInfo maps, sortBurnDown* methods and tooltip
fields. They are interleaved with the live code that populates `allData`
(which the base screen still needs), so removing them is a separate surgical
change best done when the local path can be runtime-tested.
…oller
Follow-up to the burndown unification. With the nine per-mode chart widgets
collapsed into one, the controller's own copy of that logic had no callers
left. Removed:
- sortBurnDownDaily / sortBurnDownWeekLy / sortBurnDownMonthly
- dailyInfo / weeklyInfo / monthlyInfo
- the three Burndown tooltip behaviours
- initDailyReports / initWeeklyReports / initMonthlyReports
- the `storageWidget` field, which was assigned in all three init methods
and never read (every other `storageWidget` in the codebase is an
unrelated local)
plus six imports that went unused with them.
Each init method also re-populated `allData`, but onInit() already does that
in its own Future.delayed, so the base screen keeps its data from a single
remaining source instead of four competing ones. Dropping them also removes
three `Get.find<HomeController>()` calls that would have thrown had
ReportsController ever initialised before HomeController was registered.
Only the externally-referenced members are left: allData, the daily/weekly/
monthly tour keys, initReportsTour, showReportsTour and tabController.
451 lines -> 133. flutter analyze 0 errors; suite at the pre-existing -18
baseline. Verified on-device: the burndown screen renders exactly as before
(same buckets, captions and axes) with no LateInitializationError, which is
what a wrongly-removed field would have produced.
Not touched, as they predate this work and are outside the burndown cleanup:
taskDatabase / isSaved / selectedIndex / fetchTasks also appear unreferenced
outside the controller.
… too
Completes the auto-compile work started for Android, so the native library is
rebuilt from rust/ on every platform's build and can never drift from its
source. This is also the prerequisite for eventually dropping the checked-in
binaries.
- scripts/build_tc_helper_apple.sh — builds for Apple targets. `macos` runs a
plain `cargo build --release`, which is exactly where the desktop loader
looks (ioDirectory in frb_generated.dart). `ios` builds the device and
simulator slices, lipos the simulator archs together and reassembles
ios/tc_helper.xcframework.
- ios/Podfile, macos/Podfile — a `script_phase` running the above before
compile. Hooking CocoaPods rather than editing Runner.xcodeproj/project.pbxproj
deliberately: the pbxproj is a fragile ID-keyed format, and a bad edit there
breaks every iOS build.
- linux/CMakeLists.txt, windows/CMakeLists.txt — a `tc_helper_rust` custom
target that runs `cargo build --release`, wired as a dependency of the app
target.
Every hook is non-fatal by design. If cargo, rustup or full Xcode is missing,
the hook warns and the build continues with whatever library is already
present. A contributor without a Rust toolchain must still be able to build
the app; these only upgrade the build when the toolchain exists. The iOS path
also assembles the new xcframework in a temporary location and swaps it in
only once it is complete, so a failure part-way through cannot leave the
project without a linkable framework.
Verified what this machine allows:
* macOS path run for real — produced rust/target/release/libtc_helper.dylib
at the exact path the frb desktop loader reads.
* The iOS path's guard exercised: with only Xcode CommandLineTools installed
it warns, exits 0, and leaves the existing xcframework untouched.
* Both Podfiles pass `ruby -c` (a malformed Podfile would break pod install).
* The CMake block was configured in an isolated harness, both with cargo
present (creates and wires the tc_helper_rust target) and with it absent
(warns, and configure still succeeds).
* Android rebuilt and packaged successfully — no regression to the platform
that was already working.
Not verified: a full iOS/Linux/Windows app build. This machine has only Xcode
CommandLineTools, so xcodebuild refuses to run and neither iOS nor macOS
Flutter builds are possible here, and Linux/Windows are the wrong OS. The
non-fatal design is what keeps that acceptable: the worst case for an
unverified hook is that it no-ops.
Flutter builds three ABIs (arm64-v8a, armeabi-v7a, x86_64) but cargo-ndk was only invoked for the two ARM ones. The x86_64 APK therefore shipped libflutter.so and libapp.so without libtc_helper.so: it built successfully, installed successfully, and then crashed at RustLib.init(). A build that fails loudly is safe; one that silently emits a broken artifact is not — and this one would have been published the moment split APKs were attached to a release. x86_64 is what the standard Android Studio emulator runs, so this also unblocks emulator-based development for contributors. - build.gradle: add -t x86_64 to cargoBuildTcHelper and the matching rustup target - build-tc-helper.yml: same ABI list, plus a step that fails the run if any ABI is missing a library, so the two lists can't silently drift apart again - commit the x86_64 .so alongside the existing two, keeping the no-Rust fallback complete (all three are removed together by the pending jniLibs purge) Verified: all three split APKs and the universal APK now contain libtc_helper.so.
…he result Only build-tc-helper.yml had a Rust toolchain; the three workflows that actually build shippable APKs (flutterci, build-nightly, nightlydepolyci) had none. They passed only because prebuilt .so files are committed — so CI has never once compiled the Rust library it ships, and deleting those binaries would have made all three emit APKs with no libtc_helper.so at all. The Gradle hook is intentionally non-fatal, so nothing would have gone red: they would simply have published the x86_64 bug on every ABI. This is the prerequisite for purging the committed binaries from the repo. - .github/actions/setup-rust-android: one composite action (NDK, Rust, the three Android targets, cargo-ndk, plus a cargo cache) used by all four workflows, so the target list has a single definition instead of four that can drift - scripts/verify_apk_native_libs.sh: asserts a built APK contains libtc_helper.so for every ABI it should carry, inferring the expected set from the filename so it handles universal and split APKs. Checks the APK rather than jniLibs/, which catches "cargo never ran" and "Gradle didn't package it" alike - every workflow that builds an APK now runs that check, before publishing: build-nightly records a dud as a FAILED nightly instead of a good one, and nightlydepolyci refuses to deploy it to F-Droid - build-tc-helper.yml refactored onto the shared action Verified locally: the checker passes the universal APK and all three splits, and fails an APK with lib/x86_64/libtc_helper.so deliberately removed. The CI steps themselves are NOT yet proven on a runner — fork PRs execute the base branch's workflows, so these cannot run on PR CCExtractor#652.
…comment The first CI run of the new shared action failed, in the one step that existed only to print diagnostics: `cargo-ndk --version` exits 1 with "This binary may only be called via `cargo ndk`". Everything load-bearing had already succeeded — NDK installed, ANDROID_NDK_HOME resolved, all three Android targets added, cargo-ndk built in 34s — so the toolchain setup was fine and a reporting step took the build down with it. - invoke it as `cargo ndk --version` (both call sites); `command -v cargo-ndk` stays, since testing for the file is not invoking it - guard the diagnostics with || true so a future CLI change there can never fail a build; the APK verification is the real gate. (continue-on-error is not available on composite-action steps, hence || true.) - build-tc-helper.yml: drop the "has not yet been run on CI" note. It has run green on ubuntu-latest, so the NDK version and cargo-ndk invocation were already runner-verified, not merely transcribed from a local build.
Owner
Author
|
Both workflows green on 2b06bf1 — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Throwaway PR opened only to execute the workflows in this branch on a runner.
Fork PRs to CCExtractor run the base branch's workflow definitions, so the new
Rust/NDK setup and APK verification steps cannot run on upstream PR CCExtractor#652. This is
a same-repo PR, so they do run here.
Watching for:
Flutter CI— now installs Rust + cargo-ndk and compiles libtc_helper.so forarm64-v8a / armeabi-v7a / x86_64 during the Gradle build, then asserts the APK
actually contains all three.
Build tc_helper (compile from source)— has never run on CI before.Once both are green this PR gets closed. It is the prerequisite for purging the
committed .so binaries from the repo.