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.
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.
Publishes the Flutter web client. offband.org stays the Hugo marketing
site in OffbandMesh/offband-site, a separate Pages project, untouched.
push to main -> --branch=main -> production -> offband.app
push to dev -> --branch=dev -> dev tier -> dev.offband.app
Direct upload only. Cloudflare's dashboard "Connect to Git" must not be
enabled for this project; it would create a second git-integrated
project racing this workflow. Matches how offband-site deploys.
Because direct-upload projects cannot configure production branch
controls in the Cloudflare dashboard, the "never auto-deploy arbitrary
branches to prod" requirement from #122 is enforced by the workflow's
trigger list instead. That is the safety mechanism, and it is better
placed here than in dashboard state: reviewable and version controlled.
Builds via build_pipe rather than a plain `flutter build web`, because
build_pipe appends ?v=<version> to the bootstrap and manifest files.
Without it, browsers serve stale bundles after a deploy.
Two fixes folded in:
- pubspec.yaml build_pipe build_command did not pass
--dart-define-from-file, so a deployed site would have shipped a
dead GIF picker even though #331 wired the key into build.yml. The
deploy path and the CI path used different build commands.
- Retired the inherited zjs81 Workers deploy: removed deploy.yml and
wrangler.toml (which targeted THEIR account's "meshcore" project)
and dropped the now-dead `deploy` script from package.json. A root
wrangler.toml carrying Workers config can also interfere with
`wrangler pages deploy`.
Verified locally by running the real build with the bench toolchain:
build_pipe accepts the new flag, cache busting is applied
(flutter_bootstrap.js?v=e2019703...), output is 46 files with a 10.2 MB
largest asset (Pages limits are 25 MiB per asset and 20000 files), and
the Giphy key is confirmed compiled into main.dart.js.
Not verifiable locally: whether the repo's CLOUDFLARE_API_TOKEN carries
Pages:Edit scope. The workstation token is DNS-only and 403s on the
Pages API. A 403 on first deploy would indicate wrong scope; it fails
safely.
Part of epic #312, plan #321 (Phase 4).
Owner asked whether the web assets must be re-downloaded on every drift bump.
They do not - the version can be pinned - but doing that safely needed three
changes, because the failure mode is silent.
1. drift and drift_flutter are pinned EXACTLY (2.34.2 / 0.3.1), no caret. The
committed web assets are built for a specific drift release, so a caret
would let pub resolve a newer drift than the shipped binaries and break the
web build with no signal at compile time.
2. pubspec.lock is now committed (removed from .gitignore). It was ignored,
which for an application means resolution can differ between machines and
CI. That undermines the pin: the assets are matched against the RESOLVED
version, so resolution has to be reproducible. Owner-authorized, and it is
a repo-wide change rather than a storage-only one.
3. New test asserting the shipped assets match the resolved drift version,
plus that both files exist and the wasm really starts with the WebAssembly
magic bytes. Verified in both directions: it passes when matched and fails
loudly with re-download instructions when skewed.
Without (3) a stale asset compiles, deploys, and only fails in the user's
browser - the exact silent-failure class SAFELANE 6 forbids and that produced
#333 earlier.
Upgrading drift is now a deliberate step: bump the pin, re-download both
assets, update web/drift_assets.version. The test fails until you do.
flutter analyze clean, tests pass.
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.
Cut v1.1.2-rc.3 (rc.2+59 -> rc.3+60). versionCode 60 clears 59, the build
currently live in closed testing, keeping the Play line strictly increasing.
Carries 21 commits since v1.1.2-rc.2: per-channel notify levels (epic #259,
hardware-tested), firmware block offload (#176-#180), block gating (#252),
USB VID gate (#248), reply/mention/GIF UX (#235/#238), path-len decode
(#224), and flutter_local_notifications 20 -> 22 (#242).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves flutter_local_notifications_windows to 3.1.1, which fixes the
VS 18.6 / MSVC 14.51 experimental-coroutine build error (upstream #2777 /
#2785) that breaks the Windows build. No SDK bump needed (Flutter 3.44 /
Dart 3.12 satisfy the new plugin's 3.38.1 / 3.10.0 mins).
analyze clean (the 2-major bump did not change our notification API usage);
full suite 406 green.
Part of #230 (unblocks the windows CI job, PR #228).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cut v1.1.2-rc.2 (rc.1+46 -> rc.2+59). versionCode 59 clears the
b45-b58 GitHub test-build tags for a clean monotonic line ahead of
the first Google Play closed-test upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per meshcore-firmware/docs/trace-path-format.md: a TRACE route must exclude
the origin (implicit at both ends). If route[0] is our own node's prefix it
matches no forwarding node (we are the sender) -> the packet never advances
-> RESP_CODE_SENT then a silent timeout (the doc's #1, most-common cause).
buildPath now drops a leading self hop before assembling the round trip.
Bumps build to +51 for the b51 test binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App Debug Log showed every failing trace sending an empty path ->
"Firmware responded with error code: 1". Cause: repeaters with a flood
route (path stored as all-zeros) were treated as routed; buildPath
collapsed the all-zero path to empty; the firmware rejects an empty trace.
- buildPath: an empty OR all-zero path now traces the target's own width
prefix (never returns an empty payload).
- _doPathTrace: guard - never send an empty path; surface "not available".
- Repeater menu: constant "Path Trace" label (no more Ping/Path-Trace
flip-flop); a flood path (all-zero) counts as no route -> direct ping.
Bumps build to +50 for the b50 test binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The map "Current path" panel joined _pathTrace byte-by-byte, so once the
builder started capturing the full configured-width prefix per tap (#150),
a single 2-byte node read as "84,f4" — looking like two hops. Format via
PathHelper.formatPathHex at the configured width so it reads "84F4".
Bumps build to +49 for the b49 test binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repeater contact menu hardcoded "Ping" and always sent a target-only
(direct) trace, so a multi-hop repeater offered a "Ping" that could never
return. Mirror the room behavior: a repeater with a known route is
labelled "Path Trace" and traced along that route (round-trip); only a
direct neighbour (no path) shows "Ping" and pings directly.
Bumps build to +48 for the b48 test binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The trace was width-1 end to end (flags=0, a 1-byte path, and a
1-byte-per-hop parse + render), so on a 2-/3-byte path-hash mesh it
returned 1-byte hops (`6A`, not `6Axx`) and failed to route where a
1-byte prefix is ambiguous ("Path trace not available").
- Send: path_sz = floor(log2(width)); build width-byte hops — the
repeater/room "Ping" prefixes (contacts_screen), the round-trip
buildPath (now mirrors by hop, not byte), and the map trace builder.
- Parse: read the flag byte for the hop width, group the hash bytes into
packed width-byte hops, key contacts by the full prefix, 1 SNR per hop.
- Render: list panel + markers + map points iterate packed hops, labelled
at the configured width.
PathHelper gains tracePathSz / traceHopBytes / packPrefix / tracePathHops
/ buildTraceRoundTrip, all unit-tested (incl. width-2 hop mirroring).
Bumps build to +47 for the b47 test binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three routing-match sites keyed/compared nodes by only the first
public-key byte, which collides when two nodes share a first byte on a
2-/3-byte path-hash mesh:
- DirectRepeater.matchesPathStart() replaces the 1-byte
`pubkeyFirstByte == pathBytes.first` direct-repeater check in
chat_screen and path_management_dialog (x3 each).
- map _computeGuessedLocations packs the full configured-width prefix as
the repeaterByHash key and looks up the contact-side hop at the same
width — was keying publicKey[0] but reading pathBytes.last, a silent
miss at width>1 that stopped location-guessing on multi-byte meshes.
- map _filterContactsBySettings overlap detection compares the
configured-width prefix via _samePubkeyPrefix (was publicKey.first).
Adds DirectRepeater.matchesPathStart unit tests. Bumps build to +46 for
the combined path-hash sweep test binary (b46: #151+#154+#155+#156).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DirectRepeater stored only pubkeyFirstByte (1 byte of the last hop), so the SNR
indicator + Nearby Repeaters dialog showed a 1-byte prefix and matched the wrong
repeater on 2-/3-byte path-hash meshes. Capture the last hop's full configured-
width prefix and use it for display, contact-match, and dedup.
- DirectRepeater.pubkeyPrefix (Uint8List) + prefixHex getter; pubkeyFirstByte
retained for the path-management / chat matching sites (those are #156).
- _updateDirectRepeater captures path[len-width..] (or the pubkey prefix when no
path); dedup by listEquals(pubkeyPrefix).
- snr_indicator: status-bar + dialog render prefixHex; matcher keys on the full
width prefix so the correct repeater name resolves.
Bumps build to +45. analyze clean; 6 DirectRepeater tests (2 new); Gemini: ship.
Closes#151
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bumps pubspec 1.1.2-beta.4+43 -> 1.1.2-rc.1+44, adds the 1.1.2-rc.1
CHANGELOG section (reply-UX #131/#106, device-info #134, GPS query #135,
BLE flood fix#140, path-hash #115, 0xC1 gate #144; known issues #86/#91),
and updates the README banner from public beta to release candidate.
Folds the #86 known-issue note into the rc.1 changelog, superseding #99.
Closes#146
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fork-only 0xC1 GPS command was sent to any connected radio — both the
60s self-location poll (on gps=1) and the Settings GPS status query. Stock /
non-Offband MeshCore firmware can't answer it, so gate both behind a
capability check.
- firmwareSupportsOffbandGps(offbandCaps): presence of the Offband-fork
offband_caps byte (device-info v14+), which stock MeshCore never emits.
- requestOffbandGps() hard-returns when unsupported — 0xC1 never goes out.
- _reconcileGpsPolling() centralizes the poll decision (support AND gps=1)
across all three triggers + the device-info reply, so frame order is moot.
- Settings hides the GPS status section on unsupported firmware.
Interim presence-gate; a dedicated OFFBAND_CAP_GPS bit can follow once
firmware defines one. Unit-tested (3 new); Gemini review: ship.
Bumps build number to +43 (test build b43).
Closes#144
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packet Path mis-identified repeaters: a 2-byte path hash like 6A3D was
sliced into two 1-byte hops (6A + 3D), so the first byte collided with an
unrelated repeater (Freemasons 6A..) and the second matched nothing
("Unknown Repeater").
- meshcore_protocol: realHopCount() converts the firmware path BYTE length
to true hops via the device hash width.
- channel_message_path_screen: slice/count/match at the device
pathHashByteWidth (not the unreliable per-message high-bits); bucket and
match on the full w-byte hash (mirrors _pathMatchesContact), not hash[0].
- channel_chat_screen: same width fix on the per-message via-line + hop badge.
- settings: show the build number alongside the version name.
- bump 1.1.2-beta.4+36.
- test: realHopCount regression coverage.
Gemini review (llm-consult): fix-then-ship; 2 findings clarification-only
(_formatHash is canonical hex used identically both sides; pathHashByteWidth
is static per-connection).
Part of epic #112.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pubspec 1.1.2+30 -> 1.1.2-beta.1+31: the app self-identifies as the public beta
heading toward 1.1.2 final.
- scripts/package-windows.ps1 (#96): builds the distributable zip and BUNDLES the
VC++ runtime DLLs (msvcp140 / vcruntime140*) that Flutter's Windows build omits,
so the zip launches on a clean machine without the redistributable installed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Was 1.1.1+18. Tags already referenced 1.1.2 (v1.1.2-ble-test/-queuediag) and the
rc8/rc9 APKs shipped +28/+29 via --build-number, so the pubspec build number had
drifted. +30 is the next build after the +29 that shipped. Tag v1.1.2 at release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Patch increment for the #37 compact-tile feature (the bump that was owed
inside PR #47). Build +18 stays ahead of rc.3 (+17) so a 1.1.1 build
installs over the sideloaded RC.
Replace inherited zjs81 9.1.0+14 with Offband's own line. Milestone catch-up: v1.0.0=rebrand (c002f90), v1.1.0=settings shell. Build number 14->15 (monotonic). Tags applied separately on dev.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the Offband rebrand (chore/offband-rebrand, 98a7728) on top of
dev, which already carries Batch-1 UX (#2) + emoji :shortcode: (#5) via
fast-forward of feat/batch1-ux.
pubspec version conflict resolved to 9.1.0+14 (emoji MINOR over rebrand
PATCH 9.0.1; build kept at the higher +14).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The maintainer's keyboard can't type emoji, so the composer now accepts
:shortcode: input with an inline autocomplete:
- Type `:` to open a bottom-anchored picker; arrows/Tab/click select, Esc
dismisses. Best/exact match sits at the bottom, nearest the caret.
- Enter completes an unambiguous emoji (single OR exact match) then sends,
so `:rofl` + Enter ships the glyph, not the literal text. Ambiguous
matches still send raw; Tab picks one deliberately.
- A completed `:name:` (closing colon) auto-converts in place.
- 1913 gemoji shortcodes bundled (lib/helpers/emoji_shortcodes.dart, MIT).
- Byte-count aware via the existing ByteCountedTextField; shares the
MentionAutocompleteField with @-mention input (both composers wired).
Version 9.0.0+12 -> 9.1.0+14 (MINOR feature; build kept monotonic after
the rebrand's +13 on the parallel chore/offband-rebrand branch).
Grid/tap picker deferred (#5 follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freeze permanent applicationId/bundle id to app.offband.meshcore
(three-segment so app.offband.* reserves the brand family; a future
app.offband.meshtastic won't collide).
Display name "Offband Meshcore" across: Android android:label, iOS
CFBundleDisplayName, Windows + Linux window titles, MaterialApp title,
Windows VERSIONINFO (ProductName/FileDescription/CompanyName), pubspec
description, GPX creator. iOS/macOS bundle ids + Linux APPLICATION_ID
set to app.offband.meshcore.
Version 9.0.0+12 -> 9.0.1+13 (PATCH milestone + build).
Deferred (unchanged): internal Dart package name (meshcore_open imports),
Android namespace + MainActivity package, exe/bundle filenames, docs prose
(README/privacy/documentation still say MeshCore Open - separate sweep).
MIT attribution to zjs81/MeshCore preserved (LICENSE untouched).
Verified: flutter analyze clean; Android debug APK built green;
aapt2 dump packagename = app.offband.meshcore; Windows compiled clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Introduced translation functionality in chat screen, allowing users to translate messages before sending.
- Added MessageTranslationButton to the input bar for enabling/disabling translation.
- Implemented translation service to handle incoming and outgoing text translations using llama models.
- Enhanced message storage to include original and translated text, language codes, and translation status.
- Created UI components for displaying translated messages and managing translation options.
- Added translation model management, including downloading and storing models locally.
- Updated app settings to manage translation preferences and model selections.
- Implemented radio statistics features in multiple screens including chat, channels, and settings.
- Added localization for new strings in Swedish, Ukrainian, and Chinese.
- Introduced a setting to jump to the oldest unread message in chat and channels.
- Enhanced path management and display for contacts and messages.
- Updated app settings to include new boolean for jumping to the oldest unread message.
- Improved battery indicator and radio stats display in the app bar.
- Removed unused wakelock_plus dependency and updated plugin registrations.
Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN
entry, with Completer-based flow control, explicit retry loop, and named
timeout constants.
- LinuxBlePairingService: pair-and-trust with up to 2 retries
- LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors
- Conditional import stub for web builds (dart.library.io gate)
- Scanner screen: PIN dialog integration for Linux pairing flow
- MeshCoreConnector: Linux pairing/recovery/reconnect wiring
- l10n: 4 new pairing keys across all 14 locales
- 12 unit tests (pairing service + error classifier)
Train a linear regression model on actual message delivery times to
predict tighter timeouts, replacing worst-case physics estimates.
Features: path length, message bytes, seconds since last RX, flood mode.
Global model with per-contact blending after 10+ observations per contact.
Falls back to existing physics formula when model has insufficient data.