Build task embeds GIT_HASH via --dart-define; new deploy-linux-to-server task
packages a tar.gz and updates latest.json on the server. The account list screen
shows a MaterialBanner when a newer Linux build is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ORDER BY receivedAt DESC to the searchAddresses query so the first
unique occurrence of each address comes from the newest email. Contacts
from recent conversations float to the top of the suggestions list.
Add a unit test verifying the sort order.
Fixes#83
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_RawAutocompleteState.dispose() removes _updateOptionsViewVisibility
from the external FocusNode but forgets to remove _onFocusChange. When
the state is recreated with the same FocusNode both listeners accumulate,
and the second hide() call hits the _zOrderIndex != null assertion in
overlay.dart:1681. This is a Flutter framework bug, not a test deficiency.
Restore the filter with a comment pointing to the root cause so it can
be removed when we upgrade past the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When focus leaves the To field while the address DB query is in flight,
the optionsBuilder Future completes AFTER RawAutocomplete has already
called hide() on the overlay. The completion triggers a second hide()
call, hitting the _zOrderIndex != null assertion in overlay.dart.
Fix: check focusNode.hasFocus after the await; return [] if focus left,
which prevents RawAutocomplete from calling show()/hide() on a closed
overlay.
Also fixes#81 partially: after undo(), push an inverse UndoAction so
the undo log retains a record and the user can re-apply the operation.
Fixes#79
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /builds/ page returned 404 because website/content/builds/ was fully
gitignored — Hugo had no content to generate the section landing page.
Fix:
- Narrow .gitignore to only ignore year-subdirectories (YYYY/) so that
_index.md can be committed as a static fallback.
- Add website/content/builds/_index.md with section description.
- Enhance generate_build_history.py to fetch and display commit datetime
alongside title, and render _index.md as a flat list of all builds
(newest-day first) so the section landing page is useful immediately.
Fixes#82
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A plain pump() between enterText(To) and enterText(Subject) does not
prevent the _zOrderIndex assertion: hide() is called twice synchronously
during the focus-dispatch triggered by the second enterText().
Fix: explicitly call primaryFocus?.unfocus() after the To field, then
pump(300ms) so RawAutocomplete's OverlayPortal closes via a single
FocusNode notification. By the time Subject takes focus the overlay is
already hidden — no second hide() fires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RawAutocomplete's OverlayPortalController.hide() was called twice:
once when focus left the To field and again when ComposeScreen was popped,
triggering the _zOrderIndex assertion in overlay.dart.
Fix by:
1. pump() after entering the To field so the overlay has a frame to close
before the Subject field takes focus.
2. unfocus() + pump() before tapping Send so the overlay is already hidden
when the screen pops, preventing a second hide() on unmount.
Remove the _zOrderIndex string-filter from FlutterError.onError — the
root cause is fixed rather than suppressed.
Fixes#79
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After showDialog and after the two repo awaits (getEmail/deleteEmail),
the widget may have been disposed — calling ref.read on a disposed
ConsumerStatefulElement throws "Cannot use 'ref' after the widget was
disposed." Add if (!mounted) return; at both points.
Fixes#80
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSH secrets (SSH_USER, SSH_HOST, SSH_PRIVATE_KEY) are not yet configured
as repository secrets. Mark the four SSH-dependent steps continue-on-error
so the Play Store deploy job succeeds while those secrets are pending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The deploy-apk-to-server task depends on build-android which signs the
APK — it needs the keystore password or the packageRelease Gradle task
fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_verifyErrorWidgetBuilderUnset is called from _runTestBody after testBody()
returns, but addTearDown callbacks run after _runTestBody — so teardown is
too late for this check. Restore ErrorWidget.builder inline, right after
app.main() sets it, so the binding sees the original value when it verifies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.main() also sets ErrorWidget.builder to its CrashScreen handler.
The test binding's _verifyErrorWidgetBuilderUnset check fires when
ErrorWidget.builder != its pre-test value after the test completes.
Save and restore ErrorWidget.builder alongside FlutterError.onError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OverlayPortalController.hide() asserts _zOrderIndex != null before
clearing it. In headless tests without navigation animations, rapid
screen dismissal can trigger hide() twice (once on focus loss, once on
widget unmount) — a Flutter framework race that overlay.dart itself
notes should not happen during rebuilds. Filter it alongside the
existing DEFUNCT/DISPOSED suppressions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.main() synchronously sets FlutterError.onError to its crash-screen
handler, overwriting the filter the test had registered first. The test
binding's _runTest finally-block checks FlutterError.onError != _recordError
and fires assertion '_pendingExceptionDetails != null', which prevents the
integration test framework from calling exit() — causing the process to hang
for the full 360-second timeout.
Fix: capture the binding's error recorder (bindingError) before app.main(),
call app.main() first, then install the DEFUNCT/DISPOSED filter pointing at
bindingError, and restore to bindingError in teardown. This keeps the crash
handler from interfering with the test binding's error tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Future.any([Future.delayed(N), stopSignal.future]) left unfired Timers
alive after stop() fired the signal — pending Timers kept the Dart event
loop running and prevented the process from exiting, causing the E2E
integration test to time out (exit 124) instead of exiting cleanly.
Replace all four occurrences with an explicit Timer that completes the
stop-signal and is cancelled in a finally block, so the Dart isolate can
exit as soon as the sync loops are stopped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scripts/generate_build_history.py: SSH into server, list APKs under
public_html/builds/YYYY/MM/DD/, fetch commit titles from Codeberg API,
and write Hugo content pages to website/content/builds/
- Taskfile: add deploy-apk-to-server and generate-build-history tasks;
add --exclude='*.apk' to website-deploy rsync so APKs survive redeploy
- CI: after Play Store deploy, set up SSH key, scp APK, generate history,
then deploy website
- .gitignore: exclude website/content/builds/ (generated at deploy time)
- website/hugo.toml: add Builds nav item
Closes#73
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a sync failure banner appears in the email list screen, a new
'View log' button navigates directly to the account's sync log screen
so the user can see the full error details.
Also creates issue #75 for the first-snooze-in-new-account failure.
Closes#13
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds RawAutocomplete<EmailAddress> to the To and Cc fields in the
compose screen. As the user types (minimum 2 chars), suggestions are
fetched from the local DB by searching from/to/cc columns of cached
emails. Selecting a suggestion appends it to any existing addresses
already in the field (comma-separated).
New repository method searchAddresses() returns deduplicated
EmailAddress objects matching the query string.
Closes#11
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue reports now include:
- App version (from package_info_plus)
- OS name and version (non-personal, from dart:io Platform)
- Error and stack trace wrapped in triple-backtick code blocks
so Codeberg renders them as preformatted text
Closes#59
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
actions/checkout defaults to fetch-depth: 1 (shallow clone).
generate-changelog runs git log -n 50, so only one entry appeared
in the built app. Fetching 50 commits gives a complete changelog.
Closes#64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The user should set State/Ready manually when the issue is ready
to be worked on, not automatically on creation.
Closes#74
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add --no-warn-dirty to all nix develop calls to suppress Git dirty-tree warnings
- Switch integration test reporter from expanded to compact (per-test names suppressed on success)
- Show only summary line on integration test success, matching unit/widget test behavior
Closes#8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSingle() throws 'Bad state: No element' when the email row is gone
(race condition in batch operations or already deleted). Switch to
getSingleOrNull() and return early so batch moves/flags/deletes on
stale IDs fail silently instead of crashing.
Closes#58
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A partial BODY.PEEK[n] fetch omits the section's MIME headers, so
enough_mail's decodeContentBinary() has no Content-Transfer-Encoding
and returns the raw base64 string instead of the decoded bytes.
Fetching BODY.PEEK[] gives enough_mail the full MIME structure and
getPart(fetchPartId) correctly decodes the attachment.
Also adds an integration test that creates an email with a binary
attachment, syncs it, and asserts the downloaded bytes match the
original — this test failed before the fix.
Closes#70
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mobile platforms provide OS-level back navigation (swipe gesture),
so the redundant AppBar back button only clutters the toolbar.
Desktop keeps it since there is no system back gesture.
Closes#69
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The (YY-20)mmddHHMM formula generates ~605M for 2026, which is lower
than existing epoch-second deployments (~1.747B). Google Play rejects
version code regressions at commit time (403 Forbidden).
Blocked — see issue #63 for context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Makes the InProgress-first rule harder to skip by including the exact
command to run, so there is no ambiguity about how or when to do it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces epoch seconds with a compact date-based integer so the Play
Store version code is interpretable by humans while staying below the
2 100 000 000 upper bound until ~2040.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
date +%y%m%d%H%M for 2026-05-14 17:17 = 2605141717 which exceeds
Android's 2100000000 versionCode cap, aborting the build.
Epoch seconds (~1.75B today) stay under the cap and remain unique.
Human-readable build-name (yymmddhhmm) is unchanged for issue #63.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workflow_run is not supported by Forgejo Actions — release.yml never
fired after CI passed. Port the deploy-playstore job into ci.yml with
needs: check + if: main, matching the pattern already used by build-linux.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The U7 onboarding view replaced "No accounts yet." with "Welcome to
SharedInbox", causing the E2E test to spin for the full timeout budget
(pumping slowly in headless CI) before failing. Fix the finder and
bump per-attempt timeout from 240s → 360s and CI job ceiling from
20 min → 30 min to give the full account-add → send → verify flow
room to complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xvfb-run catches SIGTERM from `timeout`, kills its children, and exits 0,
making a timed-out test indistinguishable from a pass (CI #168 false positive).
Running Xvfb ourselves captures fvm flutter test's real exit code so timeouts
(exit 124) are correctly treated as failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous failed CI runs leave orphan sharedinbox/flutter processes that hold
onto Xvfb display resources, causing the next run's GTK app to hang during
initialisation (never connects back to the flutter test runner, no output
for 9+ min until timeout fires).
Fix:
- Kill stale sharedinbox/flutter processes before launching xvfb-run
- Retry the xvfb-run call once (4-min timeout per attempt) so a transient
display-init hang doesn't permanently fail the job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splitting into separate steps breaks the Dart compilation cache that task
check builds up via parallelism. Without the shared cache, flutter test
integration_test/ -d linux rebuilds cold (9+ min instead of ~24s).
Keep the single 'nix develop --command task check' step which runs
analyze+build-linux+test in parallel (Task deps) and warms the cache
before the E2E test. Add timeout-minutes: 20 as a job-level safety net.
The xvfb-run timeout 600 (already in integration_ui_test.sh) still
prevents infinite hangs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sequential CI steps leave the runner under heavier load than the parallel
task check approach, so the E2E test can legitimately take 4-5 min.
Raise timeout 300→600 in integration_ui_test.sh and step timeout 6→12 min.
Job-level ceiling raised to 30 min to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The assets/ directory is created by generate-changelog. Splitting CI into
separate steps meant analyze ran before any step created it, causing a
pubspec.yaml asset_directory_does_not_exist warning that fails the check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split single 'Run Full Check Suite' step into named steps so per-step
timing is visible in the CI UI
- Add timeout-minutes: 20 to the overall job and timeout-minutes: 6 to
the UI E2E step — previously a stuck xvfb-run could hang for 23+ min
- Add 'timeout 300' to xvfb-run in integration_ui_test.sh so the E2E
test exits with a clear error instead of hanging indefinitely
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Select All button to AppBar during selection mode (#15)
- Replace Unix timestamp build number with yymmdd-hhmm format (#63)
- Gate release.yml on CI workflow success via workflow_run event
- Update golden for email_list_selection to reflect new Select All button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
httplib2 raises RedirectMissingLocation on Google Play's resumable upload
redirects, causing every deploy since run #77 to fail. Replace google-api-python-client
+ google-auth-httplib2 with a direct requests-based implementation using
AuthorizedSession; drop httplib2 from flake.nix entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>