Adds a QrView (painted from the pure-Dart 'barcode' matrix — already transitive
via pdf, no native plugin) and shows a QR of your npub on the profile identity
card, so you can hand your identity over with a scan. Analyzer clean.
Closes the profile loop — peers now appear by name, not a raw key.
- ProfileCache: keystore-backed cache of peers' published display names (works
offline) + shortPubkey() fallback.
- Chat: the app bar shows the peer's name (cached first, then freshened from
their kind:0 over the session); falls back to a short key.
- Inbox: each conversation shows the cached name, and missing names are resolved
over one session and cached.
- Wired via DI + TaneApp(profileCache).
Analyzer clean; ProfileCache covered by a plain test.
Two UX reports:
- Market/Messages (and Profile/Chat) had no back button — they were opened with
context.go (replaces the stack). Switched the drawer entries, home Market card,
offer 'Message' link and inbox taps to context.push, so the AppBar shows a back
arrow to return.
- The market's upload icon (tooltip-only) was unclear. Replaced it with a labelled
FloatingActionButton.extended 'Share my seeds' (campaign icon); the config gear
stays in the AppBar.
Analyzer clean.
Drawer 'Profile' is now live.
- commons_core: ProfileTransport + UserProfile + NostrProfileTransport (kind:0
publish/fetch), exposed as SocialSession.profile. Round-trip tested against the
in-process relay (fast dart test).
- app: ProfileStore (local name/about, keystore) + ProfileScreen — shows your
shareable npub with copy, edits display name + 'about', saves locally and (when
online) publishes kind:0 so peers can recognise you instead of a raw key.
- Drawer 'Profile' -> /profile (gated like Market/Chat).
- i18n en/es/pt. ProfileStore + profile transport covered by plain/dart tests.
Analyzer clean (both packages).
Chat was only reachable via an offer's 'Message' button, and the drawer 'Chat'
sat on 'coming soon' — so messaging/trust were effectively hidden.
- MessageStore keeps a conversation index (the keystore has no key enumeration)
and exposes conversations() — peers newest-active-first with their last line.
- ChatListScreen: the messages inbox (tap a conversation → /chat/:pubkey).
- Drawer 'Chat' is now a live destination -> /messages (gated like Market).
- i18n en/es/pt. conversations() covered by a plain test (no widget hang).
Analyzer clean.
Messages were in-memory only — gone on leaving the conversation.
- MessageStore: keystore-backed (no plaintext), a capped per-peer JSON list
(last 200). Simple; the encrypted Drift DB is the eventual home at scale.
- MessagesCubit: loads saved history on start (subscribes FIRST, then loads, so
a message arriving during the load isn't dropped) and persists every sent and
received message. Wired via DI + TaneApp(messageStore).
Tests (plain 'test', no hang risk): MessageStore round-trip/per-peer/cap, and a
cubit test that reopens a fresh cubit and sees the saved conversation. Analyzer
clean; run 'flutter test' locally to confirm.
Even with fixed pumps, driving discovery through testWidgets hangs here (the
transient 'searching' spinner never lets pumpAndSettle settle). Discovery logic
is already covered by offers_cubit_test (plain test + pumpEventQueue). Keep only
the offline/setup widget tests, which have no live stream. Reported by the user.
A network-wide 'N people vouch' count invites spam and means little. Now the
trust banner computes whether the peer is within YOUR circle — you vouch for
them, or someone you vouch for does (friend-of-a-friend) — via the pure
WebOfTrust rule (seeds={you}, threshold 1, distance 2). When known, the banner
turns green with 'In your circle' (else it falls back to the count).
The full member policy (threshold/distance/bootstrap) stays open (network-trust
§2); this is a deliberately loose 'people near you' heuristic.
Analyzer clean; trust cubit test adds a friend-of-a-friend vs stranger case
(run flutter test locally to confirm — the widget suite is too slow to run here).
The 'searching' state shows a CircularProgressIndicator, so pumpAndSettle never
settled and the widget test hung ~10 min. Pump a couple of fixed frames instead
(the buffered offers deliver on a microtask), and close the fake discover stream
so it's finite. Reported by the user.
Three feedback fixes:
- Servers are fully hidden now — relays are managed automatically (default
community servers; the pool skips dead ones). Sharing setup shows only 'your
area'; no jargon-y URL field. (3 default relays for redundancy.)
- Drawer 'Market' is a live destination (was stuck on 'coming soon'), gated on
the social layer being wired like the home card.
- My own listings in the market carry a 'You' badge (and no 'message yourself'
button), so it's clear which offers are mine.
Analyzer clean. Note: could not run the flutter widget suite here (10+ min
compile after the geolocator native dep in this sandbox) — run 'flutter test'
locally to confirm; the You-badge test was simplified to be robust.
Feedback: the blank 'web address' field was unusable — nobody knows what to put,
so the market never worked at first launch.
- SocialSettings ships default public relays (nos.lol, relay.damus.io), applied
automatically for new users so the market works day one. An explicitly empty
choice still turns the network off (privacy). Exposure is minimal: offers are
opt-in and carry only a coarse geohash.
- Sharing setup now shows the servers as a short PICK-LIST (checkboxes, pre-
selected) instead of a blank field; a community's own server goes under a small
'add another' escape hatch. Intro reworded — no need to know a community.
- Tests updated (defaults) and pinned offline (setRelayUrls([])) so widget tests
don't hit the network. 22 green, analyzer clean.
Reported: sheet overflowed 7.7px with the keyboard up, and 'use my location'
seemed to do nothing.
- Config sheet is now scrollable (SingleChildScrollView) — no keyboard overflow.
- 'Use my location' shows an inline spinner + inline error (the old snackbar was
hidden BEHIND the bottom sheet, so failures were invisible). Message is now
actionable ('check location is on and the permission is granted').
- Provider hardening: request permission first; on permanent denial open app
settings; if location services are off open location settings; time-limit the
fix and fall back to last-known position so it doesn't hang indoors.
Analyzer clean; market tests green.
- TrustCubit: reads how many vouch for the peer + whether YOU do; toggleVouch
certifies/revokes over TrustTransport; never vouches for self; offline-safe.
Kept simple — the full known-member rule (threshold+distance) and its
bootstrap set are still-open policy (network-trust §2); a live count + your
vouch is the honest first step.
- Chat screen now opens ONE session carrying both messaging AND trust (the
shared-connection shape), and shows a slim vouch banner: 'Vouched for by N' +
an 'I know this person' / 'You vouch for them' toggle.
- i18n en/es/pt (human words: 'I know this person', not 'certify').
Tests: 4 trust cubit + messaging + chat, 10 green; analyzer clean.
- MessagesCubit: 1:1 conversation over MessageTransport. Filters the shared
inbox to the peer; tags our own sends with our pubkey (NIP-17 wraps don't come
back to the sender) so the UI can align bubbles; degrades gracefully offline.
- ChatScreen (mockup 10): message bubbles (mine right / peer left) + a composer;
opens a session lazily, shows 'set up sharing' when there's no relay.
- Entry point: a 'Message' button on each offer card (hidden on your own
listings) → /chat/:pubkey.
- Routing + i18n en/es/pt.
Tests: 5 cubit (send/receive/order/filter/offline) + chat offline + market, green.
Sender-side offline delivery hardening.
- OfferOutbox: a keystore-backed (no plaintext) queue of lot ids to publish
later. Stores ids, not offers, so on flush each offer is rebuilt from the
lot's CURRENT state — an offline edit is respected, a deleted lot drops out.
- flushOutbox(): on reconnect, republishes queued lots and clears them (no-op
offline; drops ids whose lot is gone).
- Market screen: sharing offline parks the lots ('we'll share these when you're
connected'); opening the market online auto-flushes the queue first.
- Wired via DI + TaneApp(outbox); i18n en/es/pt.
Tests: 4 outbox + 3 flush + market UI, 20 green. Analyzer clean for new code.
Relay-strategy hardening (network-trust §3): don't hang on one relay.
- NostrChannel interface: what the transports need (sign, publish, subscribe,
reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool.
- RelayPool: connects to all configured relays (skips unreachable ones, throws
only when none are reachable), publishes to every live relay (accepted if any
accepts), merges discovery deduped by event id, and survives a relay dropping.
- Transports now depend on NostrChannel, not a concrete connection.
- SocialService.openSession takes ALL configured relays (was relays.first);
createOffersCubit passes the whole list.
commons_core 63 tests green (3 new pool tests over two in-process relays);
app_seeds social/market 18 green.
Makes the coarse area human, per feedback that a typed code is jargon.
- CoarseLocationProvider interface + geolocator-backed impl (BSD-3): low
accuracy only, returns null on any denial/error (degrades quietly). Behind an
interface so it's fakeable and the plugin stays at the composition root.
- Config sheet: a 'Use my approximate location' button (shown only when a
provider is wired) fills the area from the device position, reduced to a
5-char geohash via commons_core's Geohash — a precise fix never leaves the
device. 'Couldn't get your location' on failure.
- Threaded via TaneApp(location); main wires GeolocatorCoarseLocation.
- Platform: ACCESS_COARSE_LOCATION (Android) + NSLocationWhenInUseUsageDescription
(iOS), both scoped to the coarse-area use.
- i18n en/es/pt. Widget test fills the area from a fake location. 8 tests green.
The other half of the market: share your marked lots as offers.
- VarietyRepository.shareableLots(): non-private lots WITH their id (stable,
updatable offer id) + the variety label. Private lots never leave the device.
- OffersCubit.publishLots(): maps each via OfferMapper and publishes, tagged
with the coarse area + signed by the derived key; counts acceptances; no-op
offline or with no area.
- Market screen: a 'share my seeds' action (online only) — publishes, then
re-discovers to show them; 'mark some seeds first' when nothing's shared.
- i18n en/es/pt.
14 tests green (repo query, cubit publish, market UI); analyzer clean for new code.
A silent safety net against in-app data loss or DB corruption, with zero
friction. Driven off the app lifecycle (startup + when hidden), it writes a
sealed .tanemaki copy into the app's private storage once a week, keeping the
newest 3 (rotating). Reuses the manual backup's encryption via the extracted
ExportImportService.buildSealedBackup(); never throws, so a failed copy can't
crash startup or backgrounding. Not registered on web (no file storage).
Settings shows a quiet reassurance line with the date of the last automatic
copy (localised via intl), hidden where no automatic backup exists.
The market config leaked network jargon ('community servers / one address per
line'), confusing for the 10-80 audience (violates the human-words rule):
- Lead with a plain intro: what sharing does + a community you trust runs a
shared meeting point, no company in the middle.
- Bury the technical web address under an 'advanced' expander (most people won't
have one; 'leave empty if you don't have one yet').
- Human labels: 'Your area' (not 'Area code'), 'Web address' under advanced.
Market tests green.
Wires the 'coming soon' market card to a real discovery screen (mockups 04/05):
- MarketScreen: opens an OfferTransport lazily; local-first, so it degrades to a
human 'set up sharing' prompt when no relay/area is set or the net is down.
- Discovers offers by the saved coarse area, lists them with human reciprocity
labels (gift/swap/for sale), 'near you' (never exact location), price only for
sales. Config sheet to set area + community servers (no bundled public relays).
- Threaded via TaneApp(social, socialSettings) — null keeps the card 'coming
soon' (so Block 1 tests pass unchanged); main wires it live.
- i18n en/es/pt (market.*), reuses share.* for type labels.
3 market widget tests + home tests green; app_seeds analyzes clean.
Persistence + wiring the offers screen needs, per the product decisions:
- SocialSettings (keystore-backed, no plaintext): coarse area geohash +
configurable relay URLs, both empty by default (no bundled public relays —
don't leak metadata to third parties). Registered in DI.
- createOffersCubit(): opens a SocialSession on the first configured relay when
online, else a null-transport cubit that degrades gracefully. OffersCubit now
closes the owning session on dispose.
4 settings tests green; offers/social suites still green.
The testable core of the offers UI slice, transport-agnostic:
- OfferMapper: pure app-sharing-intent (OfferStatus) -> network Offer/OfferType
mapping; keeps price only for sales, refuses to publish private lots.
- OffersCubit: discover(geohash) streams results; publish() reports the verdict;
depends on the OfferTransport interface (fake in tests) and degrades
gracefully offline (null transport = social layer unavailable, never throws).
8 tests green (fake in-memory transport, no relay). Screen/routing/i18n come
next — they need two product decisions (coarse-area capture, relay list).
Third slice: bridge Block 1's root seed to the Block 2 transport foundation.
- SocialService: derives the secp256k1 Nostr identity from the SAME root seed
the recovery QR backs up (no extra backup), exposes npub/pubkey, and opens a
SocialSession = one shared connection carrying all three transports.
- injector.dart: registers SocialService in get_it. Local-first — derivation is
cheap + offline; no relay is contacted at startup. Replaces the old
"nodeId slice of the seed is the social pubkey" placeholder; keeps the CRDT
author id distinct (unifying it would rewrite authorship — a later decision).
- Test: deterministic identity from the seed hex, matches commons_core, rejects
malformed hex. 5 tests green; app_seeds analyzes clean (0 errors).
Typing in the edit sheet ran heavy DB work on every keystroke: a SQL
LIKE search on the species field, and a full species-catalog load plus
O(n^2) label matching on the name field. That made typing laggy.
Debounce both lookups (250ms) so they only run once the grower pauses,
and drop stale results with a per-field query counter so an earlier
response can't flicker an outdated suggestion back in.
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
The variety edit sheet grew with v8 fields (organic, needs-reproduction,
crop calendar) and its Save button, sitting at the bottom of the
SingleChildScrollView, dropped below the fold — the species-linking
widget test could not reach it (tap landed off-screen, species never
linked). Move the fields into a Flexible scroll area and pin the
Cancel/Save row as a fixed footer so Save is always visible and tappable.
Full suite green (233 tests).
The slang aggregator fell out of tracking through merge resolutions while its
per-locale siblings (strings_en/es/pt.g.dart) and database.g.dart stayed
tracked, so it kept showing as an untracked file on main. Track it too for
consistency with the repo's commit-generated-code convention.
The repository clock started fresh at Hlc.zero on every launch and was
never re-seeded from the database. After a restart, if the device wall
clock lagged behind stamps already stored (e.g. rows imported from a
peer whose clock ran ahead), a local edit was stamped *older* than the
row it edited and silently lost last-writer-wins — so lot changes made
on one device did not update when the backup was imported on another.
Re-seed the clock lazily from the newest stored `updatedAt` before the
first local stamp of the session, mirroring what importInventory already
does with the incoming max. _stamp() is now async.
Tests: add a lot-level LWW re-import test (the variety case was covered,
the lot case was not) and a restart-regression test that reproduces the
lagging-wall-clock scenario.
Grow the bundled species catalog from 14 hand-curated entries to ~1200
edible/cultivated species, internationalized in 13 languages (es, en, fr,
de, it, pt, ca, gl, eu, ar, zh, ja, ru — Latin + Arabic RTL + CJK + Cyrillic).
- Add a reproducible generator (tool/gen_species_catalog.dart) that queries
Wikidata (CC0, no attribution burden) in two phases, filters out
non-vernacular noise (author citations, ranks, initials) and applies a
relevance floor, then merges hand-curated, authoritative core-crop data
(tool/curated_overrides.json: names, family, viability_years). GBIF is used
only as an identifier. The generated species.json (v3) is committed.
- Carry wikidata_qid and gbif_key through the parse/seed pipeline; the columns
already existed, so no DB migration.
- Rewrite seedBundled to one read + one batch (was a SELECT per species on
every startup — a real cost at ~1200 rows) and keep it idempotent with
backfill of the new reference fields.
- Move species search filtering to SQL (LIKE) so a large catalog is not pulled
into memory on every keystroke.
- Cover the generator transform, the generated asset, and the new fields with
tests.
- Android release signing reads a gitignored key.properties, falling back
to debug keys so contributors can still build --release.
- CHANGELOG.md (human, newest-first) + Fastlane/F-Droid metadata (en/es)
with descriptions derived from docs/intro.md.
- docs/release.md: the by-hand build + manual smoke checklist (no CI yet).
- inventory_scale_test guards the list query against N+1 at 3000 varieties.
- Add rtl_smoke_test covering inventory, detail (lot sheet) and settings
under Directionality.rtl, with an Arabic label and vernacular name.
- Ship pt.i18n.json (Portuguese) as proof the locale pipeline scales beyond
es/en; wire its language tile in Settings.
- docs/i18n.md documents the Weblate-ready flow for adding locales.
Add a read-only "Learn more" section on the variety detail that surfaces
verified external references for the linked species, built deterministically
from bundled identifiers — no titles bundled, no network, offline-first:
- GBIF taxon page from the backbone key
- Wikipedia via Wikidata Special:GoToLinkedPage (locale-aware, RTL-safe,
falls back to the Wikidata item when a language lacks the article)
- Wikispecies from the scientific name
Enrich the bundled catalog (species.json v3) with gbif_key + wikidata_qid for
all 14 species, cross-checked (each QID carries GBIF ID P846 = the key).
Schema already had the columns; seedBundled now persists and backfills them.
The grower's own manual links stay but are demoted (progressive disclosure):
the section only shows when links exist and "Add link" is a low-emphasis
button, since hand-adding a link is the rarer power move.
Pure-Dart link builder is unit-tested; repo persistence/backfill and the
detail-screen section are covered too.