Tane dialled its four default relays at launch, before anyone had asked
for anything — an F-Droid reviewer spotted it, and they were right. The
seed book needs no network at all, so the app should not have one until
the person joins the sharing side.
- SocialSettings gains a three-state `sharingEnabled`. `null` means
"never asked", which is what lets `migrateSharingEnabled` keep an
existing install exactly as it was: anyone past the intro was on a
build that connected at launch, so they keep messaging, device sync
and offer alerts. A fresh install starts fully offline.
- bootstrap only starts the shared connection when sharing is on. The
inbox/sync/plantaré/alert listeners are untouched: they react to a
session, and none arrives.
- SharingSwitch is the single place that moves the stored choice, the
live connection and the flag the UI listens to, so they cannot drift.
- Agreeing to the community rules is the opt-in — one consent surface,
reached from the market or from the drawer's invitation.
- SocialConnection.start is now idempotent and gains stop(), so turning
sharing off goes offline immediately instead of at the next launch.
- The social drawer entries stay visible but padlocked while sharing is
off; tapping one explains what wakes up and offers to join. Hiding
them would have kept the tool a secret. "Coming soon" is gone for
good — everything it labelled is built.
Covered by tests for the migration in both directions, start/stop
lifecycle, the gate turning sharing on, the invitation, and the drawer
in all three states (no social layer / off / on).
Forgejo workflows are independent — ci.yml never triggers on tag pushes
(its branches filter skips refs/tags/*), so releases shipped with zero
test gating. Duplicate analyze + test-commons-core + test-app-seeds into
release.yml and make play needs them; gate the fdroid chain on the same
tests via explicit result checks while keeping !cancelled() so it stays
decoupled from play's result. Red suite now blocks every deploy.
F-Droid's APK scanner rejected v0.1.14: 6 com.google.android.play.core.*
(SplitInstall/SplitCompat/tasks) references. They came from Flutter's
io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager
being pinned by our over-broad '-keep class io.flutter.** { *; }' ProGuard
rule — Tane uses no deferred components, so R8 should shrink that manager
away (as it does for immich and other Flutter F-Droid apps, which don't
carry such a keep). Narrow the keep to io.flutter.plugins.**; JNI-critical
embedding classes (FlutterJNI, …) stay via the embedding AAR's own consumer
ProGuard rules — verified the built APK still contains FlutterJNI and no
com.google.android.play.core.* at all. Also exclude the com.google.android.play
group at the dependency level (belt-and-suspenders) + -dontwarn.
Bumps to 0.1.15 (+17); fdroid recipe versionCodes 171/172/173.
Even with the /home/vagrant path fix and v2/v3-only signing, the verify
still failed on a v3 CHUNKED_SHA512 mismatch with an EMPTY file diff. Cause
found by byte-level compare: apksigner (build-tools 35+) re-aligns native
libs to 16KB pages by default (--alignment-preserved defaults to false),
rewriting each .so's ZIP local-header extra field from gradle's 0x0 padding
to the 0xd935 alignment marker. F-Droid's reproducibility check compares
against the raw gradle output (0x0), so our re-aligned reference (0xd935)
differed in the signed bytes though every file was identical. Proven: 273
entries differed only in the extra field, 0 in compressed data. Signing
F-Droid's own unsigned build with --alignment-preserved true yields content
byte-identical to it (0 extra-field diffs, 0 data diffs). So preserve the
build's alignment when signing the reference.
The reproducible-build verify failed even after the /home/vagrant path fix
made every file identical: our reference APK carried a v1 (JAR) signature,
which adds META-INF/{MANIFEST.MF,*.SF,*.RSA} as three extra ZIP ENTRIES.
F-Droid's apksigcopier transplants only the v2/v3 signing block onto its
rebuild, so those 3 v1 entries make the reference's signed content differ
from F-Droid's build — apksigner then reports a v3 CHUNKED_SHA512 digest
mismatch though 'diff -r' shows no file differences. Proven by zipinfo: our
ref had 486 entries vs F-Droid's 483, the delta being exactly the v1 files;
the shared 483 are byte-identical. minSdk is 24 so v1 is never needed.
Sign v2/v3 only so the reference byte-matches F-Droid's rebuild.
Flutter auto-adds android.permission.INTERNET only to the debug/profile
manifests, so every RELEASE build (Play + F-Droid) shipped with no network
permission at all — the whole social layer (market, messaging, trust, sync)
was dead on Android release, while desktop (no permission model) worked. The
market's 'can't reach the servers' was the visible symptom. Adds it to
android/app/src/main/AndroidManifest.xml.
Release prep v0.1.14: pubspec 0.1.13+15 -> 0.1.14+16; fdroid recipe
versionCodes 161/162/163, commit v0.1.14, CurrentVersionCode 163.
v0.1.11 and v0.1.12 bumped pubspec without advancing the fdroid recipe, so the
fdroid_reference job computed versionCodes 131/141 that the recipe (still
121/122/123) didn't define — 'fdroid build' found no such versionCode and the
job failed. The Play deploys were unaffected.
- pubspec: 0.1.12+14 -> 0.1.13+15.
- fdroid recipe: Builds versionName 0.1.13, versionCodes 151/152/153,
commit v0.1.13; CurrentVersion 0.1.13, CurrentVersionCode 153.
Play accepts versionCode 15 (new); fdroid_reference now builds 151/152/153.
Bumps 0.1.11+13 -> 0.1.12+14. Ships the market fixes: auto-recovery when the
shared relay connection comes up (no more stuck 'can't reach the servers' on a
fresh install), per-relay connect timeout, zone prompt before the connection
error, un-clipped Save button, and honest 'community servers' copy in app + site.
fdroid recipe intentionally not bumped here (as with v0.1.11); F-Droid is tracked
in its own reproducible-build MR.
The landing claimed seeds spread 'with no server in the middle' and the About
page said people communicate 'directly with each other', while sharing in fact
travels through community relays. Reworded (en/es/pt, docs sources + generated
about pages via build-legal.sh): what you share travels through community
servers run by people and collectives — many of them, anyone can add one, none
a controllable center.
The setup intro claimed sharing happened 'with no company in the middle'
while the sheet itself lists community servers. Reworded in all repo-authored
locales (en/es/fr/de/pt/pt_BR/ast): what you offer travels through community
servers run by people and collectives rather than a company.
- A new user's first step (set your area) works offline, so that empty state
now wins over 'can't reach the servers'
- The sharing-setup sheet wraps in SafeArea(top: false) so the Save button
isn't clipped by the system navigation bar on edge-to-edge devices
Fresh installs could sit on 'can't reach the servers' forever: the offers
cubit captured the transport once at build time, the shared connection only
retried on a connectivity CHANGE, and a silently-filtered relay could stall
the pool for minutes.
- SocialConnection: retry with backoff after a failed attempt while started
and not knowingly offline (injectable schedule for tests)
- OffersCubit: follow connection.sessions, re-attach the transport and re-run
the last discovery on (re)connect; announce drops via connectionEpoch
- market _init: record the wanted area on the cubit even while offline
- NostrOfferTransport.discoverPage: sort a copy (channel lists may be
unmodifiable)
The forced width:100% on .site-nav below 560px was written back when
the language switcher spelled out every language name and needed the
room. Now that it's a compact pill, the header fits on one line down
to ~360px; let flex-wrap handle the actual overflow case instead of
always forcing a second row.
Even after the dnsmasq DNS cache, the Hetzner vSwitch still blips
mid-transfer. Two spots kept killing ~25min builds: the fdroidserver
tarball 'curl -sL | tar' (an empty stream -> 'gzip: unexpected end of
file') and maven/pub fetches mid-Gradle ('Network is unreachable').
Download fdroidserver to a file with curl --retry (+ -f + validate the
gzip before extract), and wrap fdroid build in a 3-attempt loop gated on
the APK actually appearing (fdroid build exits 0 even on failure).
F-Droid's reproducible-build verification failed: the native .so
(Flutter libapp/libdartjni + tesseract4android libjpeg/leptonica/pngx/
libtesseract) differed byte-for-byte. Root cause: F-Droid builds under
/home/vagrant/build/<appid> (its buildserver path) while our reference
built under /tmp/fdroiddata/build/<appid> — the differing absolute build
path leaks into the native libs (debug info / embedded paths). Build the
reference under /home/vagrant so app + srclib paths match F-Droid's.
Also: add a workflow_dispatch ref_tag input to rebuild+re-upload an
existing release's reference APKs (idempotent asset replace) without
cutting a new tag or re-deploying to Play — so reproducibility fixes can
be iterated on the same v* release.
A tag push started play (AAB → Play) and fdroid_reference_armeabi_v7a at
the same instant; two heavy Android builds on the shared runner
(capacity:2) OOM-killed play's Gradle daemon ('daemon disappeared') deep
in bundleRelease. Chain the fdroid references after play with needs:play
+ if:always() so only one Android build runs at a time. if:always() keeps
the fdroid chain independent of play's result (still runs when play fails
or, on workflow_dispatch, is skipped).
- play job: apply the same two fixes the fdroid recipe uses (strip
flutter_tesseract_ocr's dead-jcenter buildscript; cap Gradle heap to
-Xmx4g + no daemon) so a cold-cache production AAB build can't hit
either blocker. play builds the AAB directly, not via the recipe, so
it was still exposed.
- pubspec: 0.1.9+11 -> 0.1.10+12.
- fdroid recipe: versionName 0.1.10, versionCodes 121/122/123,
commit v0.1.10, CurrentVersion(Code) 0.1.10/123. binary: uses %v so
it resolves to the v0.1.10 release assets the tag build will upload.
The v0.1.10 tag is intentionally NOT cut here: cutting it deploys to
Google Play PRODUCTION (100%) and uploads the 3 signed reference APKs.
The reproducible build OOM-killed the Gradle daemon ('daemon disappeared
unexpectedly') mid flutter-build-apk on the shared runner. The app's
gradle.properties reserves -Xmx8G+4G metaspace (~12G), and the fdroid
build also compiles tesseract4android from source first (its daemon
lingers), so combined peak RAM trips the OS OOM-killer under contention
(the same build succeeded on a quieter host earlier). Scope the fix to
the fdroid build (don't touch the committed 8G the play/local builds
rely on): run tesseract's gradle with --no-daemon so it frees memory
before the app build, and append a lower -Xmx4g + org.gradle.daemon=false
override to gradle.properties in prebuild (last-key-wins; F-Droid runs
the same steps and memory settings don't change output bytes, so it stays
reproducible).
The plugin's android/build.gradle pins its own AGP 7.1.2 and resolves it
via jcenter() (dead since 2021). On a cold Gradle cache the AGP-7.1.2
transitives (apkzlib -> fastutil/json-simple/javawriter) only resolve
from jcenter and fail hard, breaking the reproducible fdroid build.
Warm-cache runs masked it (the one green armeabi-v7a run never hit
jcenter). Strip that buildscript block in prebuild so the plugin
inherits the app's AGP 8.11.1 (settings.gradle.kts) and never touches
jcenter or old AGP at all.
Mirror the es/ pattern: docs/legal/pt/ + docs/o-que-e-tane.md as the
Portuguese source of truth, build-legal.sh extended to generate the
Hugo pt.* pages from them, config.toml gets a [languages.pt] entry,
i18n/pt.json translates the UI chrome. og-pt.png is a placeholder copy
of og-en.png until a proper Portuguese social card is made by hand
(same as the other og-*.png, per DEPLOY.md). Verified locally with the
Docker build: /pt/, /pt/about/, all four /pt/legal/ pages and the
lang-switch link all resolve correctly.
The bare ~/repos/tane.git was renamed to ~/repos/tane-pre-split-2026-07-15.git
(archived pre-filter-repo history) and detached as a remote; origin is now the
forge at git.comunes.org.
Fill the ~74 UI strings pt.i18n.json was missing (saved searches, label
scanning, Plantaré, lot history), then add pt_BR as a proper Brazilian
variant rather than a copy of European Portuguese: tu/teu/tua -> você/
seu/sua with matching verb conjugation, partilhar -> compartilhar, and
Guardar -> Salvar for UI actions (seed-saving keeps "guardar", the
correct term in both variants). Wires AppLocale.ptBr into the language
picker and regenerates slang output.
Device compatibility (regression fix):
- The CAMERA permission (from zxing_barcode_scanner / image_picker) implicitly
required android.hardware.camera, excluding camera-less devices — Play showed
Automotive -96%, Chromebook -86%, TV -25%. Declare camera & location
uses-feature required="false" to keep those devices supported.
- Detect a real camera at runtime (PackageManager.FEATURE_CAMERA_ANY via the
existing MethodChannel), cache it at bootstrap, and hide the QR scan button and
the camera photo-source option when absent, so no broken actions are offered.
Play "app optimization" recommendations:
- Enable R8 (isMinifyEnabled + isShrinkResources) with keep rules for the
OCR (tesseract4android), SQLCipher and notifications JNI/reflection code.
- Bitmap downscaling: cacheWidth/cacheHeight (and ResizeImage for avatars) on
list thumbnails and avatars so photos decode to on-screen size, not full res.
- Edge-to-edge: opt in with transparent system bars in main().
Release flow:
- Tagged builds now publish to the production track at 100% (was internal);
add a manual deploy_internal lane as a QA safety net.
- Document country/region availability as a Play Console setting (not in-repo).
Bound market memory and let large areas page instead of loading everything:
- DiscoveryQuery gains an until cursor; OfferTransport gains discoverPage()
(one-shot, EOSE-bounded, newest-first) alongside the existing live discover()
stream, which now accepts since so it only carries NEW offers going forward.
- NostrOfferTransport.discoverPage sorts by created_at desc and derives the
next cursor from the oldest event seen (not the oldest type-matched one, so
filtering never skips a page).
- OffersCubit: discover() fetches the first page + opens a since=now live sub;
loadNextPage() pages further back; the in-memory list is capped at 400
offers (each can carry a ~40KB inline photo thumbnail, so unbounded growth in
a busy area was a real OOM risk).
- market_screen: infinite scroll via a scroll-position trigger + footer
spinner, instead of a single unbounded ListView.
- Added discoverPage unit tests (commons_core) and cubit pagination tests
(first page/cursor, accumulation, cap, cross-page dedup).
Scale the local inventory to 10k+ varieties:
- Render the list with ListView.builder over a flattened header/item model
instead of building every tile upfront.
- Store a small regenerable JPEG thumbnail per photo (schema v14) and use it
for the 48px list avatar; full bytes stay for offer image hosting. Existing
photos are backfilled lazily at startup. Thumbnail is local-only (excluded
from CRDT sync and backups by the JSON codec).
- Add indexes on varieties(is_deleted,is_draft), attachments(parent_type,
parent_id,kind), lots(variety_id) via @TableIndex.
- Debounce watchInventoryView (~250ms) so a burst of table writes triggers one
reload, not seven.
- cacheWidth/cacheHeight on the list avatar decode.
- Scale test raised 3k -> 10k; migration test v13 -> v14.
One passive line in the batch story after a 'sown today' tap — 'this
species crosses, grow it ~400 m from others' — sourced from the bundled
seed-saving guidance (family default + species override). Selfers get
nothing: the trap is recording isolation religiously for beans where it
never mattered. No input fields, no schema change.
Closes the physical↔digital loop the labels opened: the QR on a printed
envelope (tane://seed, already encoded by seed_label_codec) can now be
scanned from the inventory bar. A known label opens its variety; an
unknown one asks before adding anything, then creates the variety (species
linked by exact scientific name when bundled) with a lot carrying the
label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner,
MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships.
Mobile-only; other platforms don't show the button (OCR precedent).
The post-scan flow is a plain function (handleScannedPayload) so it's
widget-tested without a camera.
The note growers most wish they had two seasons later — did it do well,
was it worth keeping — never had a home. New append-only GardenOutcomes
table (lotId, season year, three-face rating, free note); the question
appears ONCE, inline, right after recording a harvest in the batch story,
and is skippable without a trace. The answer shows as one more story
line. Rides backups/sync like every mutable table (JSON codec, LWW
import, HLC clock absorb). Migration v12→v13 guarded + verified from
every historical version.