main
dev
chore/365-cut-1.2.1
fix/363-pin-db-path
feat/351-contact-settings-menu
feat/349-window-geometry
feat/355-migration-merge
integration/290-306-335
release/1.1.2-rc.3-allplatforms
fix/309-path-len-decode
chore/298-path-logging
fix/285-ingest-timelog
fix/252-block-outgoing-dm
feat/64-observer-config
feat/batch1-ux
chore/offband-rebrand
v1.2.1
v1.2.0
v1.1.2-rc.3-allplatforms
v1.1.2-rc.3
v1.1.2-rc.2
v1.0.0
observer-g2-rc3
observer-g2-rc4
observer-g2-rc5
observer-g2-rc6
observer-g2-rc7
observer-g2-rc8
observer-g2-rc9
v1.1.0-rc.1
v1.1.0-rc.2
v1.1.0-rc.3
v1.1.2-beta.1
v1.1.2-beta.2
v1.1.2-beta.3
v1.1.2-ble-test
v1.1.2-queuediag
v1.1.2-rc.1
v1.1.2-rc.1-b45-nearbyrep
v1.1.2-rc.1-b46-pathhash
v1.1.2-rc.1-b47-tracewidth
v1.1.2-rc.1-b48-repeatertrace
v1.1.2-rc.1-b49-maptrace
v1.1.2-rc.1-b50-tracefix
v1.1.2-rc.1-b51-originstrip
v1.1.2-rc.1-b52-channelqr
v1.1.2-rc.1-b53-viarepeater
v1.1.2-rc.1-b54-rxfed
v1.1.2-rc.1-b55-topology
v1.1.2-rc.1-b57-routeux
v1.1.2-rc.1-b58-toporeadout
v1.1.2-rc.3-b59-pathlen
${ noResults }
8 Commits (53f87f51e3b71b6f4db5ca8733a4f7e415fe339e)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
53f87f51e3 |
build(#335): commit the drift generated code so CI can analyze/build
CI failed on every job: offband_database.g.dart (drift's build_runner output) was gitignored and no workflow runs build_runner, so analyze and all six platform builds hit "Target of URI hasn't been generated". drift is the first code in this repo needing codegen, which is why dev's CI never had to. The workflows are not matrixed - six separate build jobs plus analyze, web deploy and release-signed, each with its own `flutter pub get`. Adding a codegen step to all of them is wide and easy to miss one. Instead the generated file is committed (drift supports this): one file, works across every job and workflow with no CI edits, and reproducible. drift_dev is pinned (2.34.0) alongside the already-pinned drift, so a future regeneration produces the same file rather than drifting from the committed copy. Regenerated against the pinned versions before committing. Follow-up worth having: a CI check that regenerates and diffs, so a schema change without regen fails loudly rather than shipping a stale .g.dart. |
23 hours ago |
|
|
3ff272ad8c |
fix(#343): close the data-loss holes found by Gemini review
Adversarial review (standards#145) of the storage/migration surface found four real defects, all data-integrity. All confirmed against the code and fixed; no false positives. BLOCKER - concurrent save race. saveChannelMessages / saveMessages are read-modify-write with an await gap, so two saves to the same key raced and the second clobbered the first, silently losing messages (e.g. a message and its delivery ack arriving together). BlobStore now provides synchronized(key, ...), a per-key operation chain; every RMW - merge-save, remove, and the load-path legacy migration - runs through it. Different keys stay concurrent. New test fires two concurrent saves and asserts the union survives. BLOCKER - non-atomic DB relocation. The documents->support move copied and deleted each file (.sqlite/-wal/-shm) in turn, so a failure after the main file moved stranded the DB across two locations and corrupted it. Now copies all files, verifies each by size, and only then deletes the sources; on any failure it rolls back the destination and leaves the original intact. MAJOR - load-path legacy migration raced save. The #194 index->PSK adoption did a blind write to the PSK key that could clobber a save that landed first. It now runs under the key lock and MERGES (union) instead of overwriting, so both the adopted history and any fresh message survive. MINOR - channel merge key lacked a sender. Two senders posting identical text at the same timestamp without a messageId would collide and lose one. The key now includes the sender, matching MessageStore. flutter analyze clean, dart format clean, 509 tests pass. Full Gemini log in docs/llm-consultations/. |
23 hours ago |
|
|
2fefb6aa91 |
fix(#343): keep full message history; window only in memory
The persisted store was being overwritten with the windowed in-memory list, so any channel or DM with more than _messageWindowSize (200) messages lost everything older than the newest 200 on the first save after load. Observed live: Public went 232 -> 201 in one session on a build that already had the #333 fix, so this was not the race - it was windowing truncating the store. This is the slow-erosion cause behind the whole 566 -> 231 -> 206 -> 201 history. saveChannelMessages / saveMessages now MERGE into the persisted set instead of overwriting: upsert by message identity so older persisted messages are kept, new ones added, and the in-memory copy wins for edits/reactions/status. If the existing history fails to decode, the save aborts loudly rather than merging into an empty base and truncating (SAFELANE 6). Deletion is now an explicit path - removeChannelMessage / removeMessage - since the app deletes individual messages. Routing delete through the merging save would resurrect them; the connector's deleteChannelMessage / deleteMessage now call the explicit remove. Windowing stays for display and memory; it no longer dictates what is stored. Tests: 250-message history survives a 200-window save; new message appends; edit is captured not duplicated; delete does not resurrect. 508 pass, analyze and format clean. Cost: a save now re-reads and re-encodes the channel/contact history. Cheap with drift's per-key writes (#335); a future append-only schema removes even that. |
1 day ago |
|
|
fa470c1442 |
fix(#335): store the drift DB in app-support, not OneDrive-synced Documents
drift_flutter's native default is getApplicationDocumentsDirectory(), which on Windows resolves to the user's Documents folder - redirected into OneDrive on most machines (verified: drift_flutter 0.3.1 connect.dart). A live SQLite file syncing to OneDrive risks lock contention and corruption, and it is simply the wrong place for app data. The DB now opens in getApplicationSupportDirectory() (%APPDATA% on Windows), the same place SharedPreferences already lives, so all app data sits together. Existing installs already have a DB in the old location, so opening a fresh one there would abandon their migrated history. _appSupportDatabaseDirectory() relocates it once on first run: it moves offband_store.sqlite and its -wal/-shm sidecars from the documents dir to the support dir before drift opens, and never deletes a source without a successful copy. If relocation fails it is logged loudly and a fresh DB is created, with the migration re-running from SharedPreferences rather than silently losing anything. Web is unaffected: path_provider has no web backend and drift ignores databaseDirectory there, using OPFS/IndexedDB. Adds `path` as a direct dependency (was transitive). flutter analyze clean, tests pass. |
1 day ago |
|
|
dd67391a0e |
feat(#335): switch bulk stores over to drift
Wires the migration into startup and points the four bulk stores at drift. SharedPreferences keeps the settings, which is what it is for. Startup runs the migration after prefs are up and BEFORE any store reads, and awaits it. Letting stores race a half-finished migration is precisely the shape of #333, where a storage path silently chose the wrong key and 566 real messages read as empty. Converted: message_store, channel_message_store, contact_store, contact_discovery_store. All four now have ZERO prefs get/set for bulk data. Three safety properties, deliberately built in: 1. readWithPrefsFallback - if a key is somehow not in drift, the prefs copy is still served rather than reading as empty. It logs a WARNING when it fires, because a fallback during normal operation means the migration is incomplete and someone needs to know. Silence here is what made #333 look like data loss. 2. deleteEverywhere / keysWithPrefix span BOTH backends. A clear that only removed the drift row would leave a pre-migration prefs copy to reappear through the fallback, resurrecting deleted history. 3. The legacy-key migrations inside the stores now check both backends and only touch prefs when a legacy key actually exists. This also carries the #306 fix into this branch: the unconditional prefs.remove was still present here, since this branched from dev rather than from the #306 work. Verified: analyze clean, format clean, 504 tests pass, Windows and web both build. Migration rehearsed earlier against a copy of a real 7 MB store: 69 keys, 5.21 MB, zero failures. NOT yet run against live data - that happens on first launch of this build, and the owner should have a prefs backup before that. |
1 day ago |
|
|
a2c7302aad |
feat(#335): migrate bulk data from SharedPreferences into drift
Moves message history, contacts and discovered contacts out of the settings store. Settings stay in SharedPreferences, which is what it is for. Ordering is the whole safety argument: WRITE, VERIFY BY READING BACK, and only then remove the source. #333 was a storage path that chose a key silently and made 566 real messages read as empty; deleting before verifying would make that class of mistake permanent instead of cosmetic. On any failure the source is left intact and the error is logged - never a silent drop (SAFELANE 6). Idempotent by construction: a key already present in drift is not overwritten, so re-running is a no-op. If an older build re-writes a migrated key into prefs, the migrated copy wins and the stale prefs copy is discarded rather than promoted. Rehearsed against a COPY of a real 7 MB store, as the plan required before touching live data: REHEARSAL: 69 migrated, 0 failed, 5.21 MB, 69 bulk keys expected Every key checked for exact length, confirmed removed from prefs, and every settings key confirmed untouched. The live store was never opened. Two things the tests caught that review would not have: 1. getString THROWS on a non-string value rather than returning null, so a non-string under a bulk prefix was counted as a migration FAILURE. It now type-checks with prefs.get() and skips. Alarming falsely is its own bug. 2. The `contacts` prefix was checked against the real store rather than assumed: it matches only the 6 bulk contact blobs, and correctly does NOT match contact_unread_count*. Not yet wired into app startup - that is the switchover, and it is deliberately a separate commit so this can be reviewed on its own. flutter analyze clean, dart format clean, 504 tests pass. |
1 day ago |
|
|
847a28df3e |
feat(#335): prove drift/SQLite-WASM works in a real browser
Step 1 gate, web half. Building is not evidence, so this was run in a browser. Assets: sqlite3.wasm (748 KB) and drift_worker.js (355 KB), both taken from the SAME drift-2.34.2 release so they are built against each other. Validated before use - the wasm has correct WebAssembly magic (0061 736d) and carries SQLite 3.53.3; the worker is genuine compiled Dart JS. Note the filename: the release ships `drift_worker.js`, not the `drift_worker.dart.js` the docs name. The database URI was corrected to match, which would otherwise have been a silent 404 at runtime. Browser result, served over HTTP with COOP/COEP and Content-Type: application/wasm: Using WasmStorageImplementation.opfsLocks PROBE-OK open+write+read PROBE-OK 6MiB round-trip (localStorage cap is 5MiB) drift selected OPFS, the top storage tier, and round-tripped 6 MiB - data the current localStorage-backed web build physically cannot hold. No exceptions. Adds web/_headers for Cloudflare Pages: forces application/wasm on the wasm asset and sets COOP/COEP for cross-origin isolation. Verified to ship into build/web. Without COOP/COEP drift still works, falling back to IndexedDB, which is slower but still unbounded next to localStorage - a performance tier, not a correctness requirement. web_probe/ is a standalone entrypoint that exercises ONLY the drift stack, so a pass or fail is unambiguous without BLE and the rest of the app in the way. It is not part of the app build. Still no user data touched and no store switched over. The migration is the next step. flutter analyze clean, dart format clean, 497 tests pass. |
1 day ago |
|
|
19858698ee |
feat(#335): add drift and prove it opens on the targets buildable here
Step 1 of #335, deliberately before any data migration: prove the store works before trusting it with message history. Adds drift + drift_flutter, a key/value StoredBlobs table mapping 1:1 onto the existing SharedPreferences keys, and verifyReadWrite() as a runtime probe. Key/value rather than relational tables on purpose: it maps directly onto the current store interfaces, so callers do not change and the migration can be verified row-for-row against the old keys. Relational schemas come later, once this is proven and querying is actually wanted. Verified here: - 3 tests pass, including a 6 MiB round-trip - larger than the 5 MiB localStorage cap that makes the web build impossible today - flutter analyze clean - Windows release build succeeds - Web release build compiles Two findings from the gate that a plan-on-paper would have missed: 1. sqlite3_flutter_libs 0.6.0+eol is a DEPRECATED no-op. From sqlite3 v3.x it is unnecessary and drift_flutter already covers it, so it is not a direct dependency here. 2. The web build COMPILES BUT WOULD FAIL AT RUNTIME: drift needs sqlite3.wasm and drift_worker.dart.js shipped in web/, and neither is present. Per drift docs they are downloaded from GitHub releases, version-matched to pubspec.lock (drift 2.34.2, sqlite3 3.5.0), and the server must serve .wasm as Content-Type: application/wasm. Not done here - downloading files needs explicit human approval. Also noted: the drift_dev CLI does not compile at these versions (allSchemaEntities missing from the drift3_preview GeneratedDatabase), so its asset tooling is unavailable. build_runner code generation is unaffected. No user data is touched. No store is switched over. The migration is the next step and remains gated on the web assets question. |
1 day ago |