Commit graph

234 commits

Author SHA1 Message Date
ee78b0ed62 feat(block2): relay pool — publish to many relays, discover deduped, tolerate failures
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.
2026-07-10 10:35:39 +02:00
15bd689a99 Solve lint issue 2026-07-10 10:34:43 +02:00
bf0e770aae feat(block2): 'use my location' — set the sharing area without typing a code
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.
2026-07-10 10:25:44 +02:00
6195bece4a feat(block2): publish my seeds to the network (completes publish->discover)
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.
2026-07-10 10:18:30 +02:00
39adc423b9 feat(backup): add automatic weekly encrypted backups
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.
2026-07-10 10:17:36 +02:00
2d4e14e0da feat(block2): geohash encoder in commons_core (for 'use my location')
Pure Dart lat/lon -> low-precision geohash (public-domain algorithm), the piece
the coarse-area capture needs so people never type a code. Precision-controlled
(5 chars ~ ±2.4 km). Tested against the canonical u4pruydqqvj vector + prefix/
proximity properties. 5 tests green.
2026-07-10 03:18:30 +02:00
6d2d0517cc fix(block2): plain-language sharing setup — no relay/server jargon
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.
2026-07-10 03:16:39 +02:00
0e7eb22c26 chore(i18n): regenerate strings for market.* after merging main 2026-07-10 03:07:59 +02:00
12f48ea318 Merge branch 'main' into spike/block2-derisking 2026-07-10 03:06:30 +02:00
2c358ec33a feat(block2): market screen — discover seeds near you (offers UI)
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.
2026-07-10 03:05:12 +02:00
801a846d6e feat(block2): social settings + offers cubit factory (area, relays)
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.
2026-07-10 02:54:06 +02:00
a59adcca6b feat(block2): offers logic layer — OffersCubit + OfferMapper (no UI yet)
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).
2026-07-10 02:49:06 +02:00
51f9338433 feat(block2): wire the social identity into app_seeds (DI, no UI)
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).
2026-07-10 02:44:37 +02:00
2b58fe40ce feat(block2): Nostr transport backend in commons_core (all 3 contracts)
Second production slice: the concrete Nostr backend on the shared connection,
using the vetted nostr package's crypto (NIP-44 spec-vector tested, gift wrap).

- NostrConnection: one dart:io WebSocket + identity + EVENT/OK, REQ/EOSE via
  the library's Event/Filter/Request. Pure Dart, no Flutter.
- NostrOfferTransport (NIP-99 kind 30402) + Nip99Codec: geohash coarsened to a
  NIP-52 prefix ladder on the wire; Offer stays inventory/location-agnostic.
- NostrMessageTransport (NIP-17): send/inbox via the library's DirectMessage
  gift-wrap onion.
- NostrTrustTransport (custom addressable kind 30777): certify/revoke/discover.
- MiniRelay test support + integration tests: offers, messaging (metadata
  privacy), WoT membership + revoke, and trust-filters-offers — all over ONE
  shared connection, hermetic in-process relay.

Renamed the core offer lifecycle enum OfferStatus -> OfferLifecycle to avoid
colliding with app_seeds' sharing-intent OfferStatus (no Block 1 code touched;
app_seeds analyzes clean, 0 errors). commons_core: 55 tests green.
2026-07-10 02:38:50 +02:00
33e0cffd30 feat(about): add Comunes Association copyleft notice à la Ğ1nkgo 2026-07-10 02:36:17 +02:00
913e058122 perf(ui): debounce species/name lookups in the edit sheet
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.
2026-07-10 02:32:02 +02:00
edb480b21b feat(block2): commons_core social foundation — derivation, types, interfaces, WoT
First production slice of Block 2 (transport foundation), on the vetted
pure-Dart nostr package (LGPL-3.0, AGPL-compatible, Flutter-free):

- NostrKeyDerivation: seed -> secp256k1 Nostr identity via domain-separated
  HKDF (reuses cryptography's Hkdf like BackupBox), one-way, versioned. Wraps
  nostr's Keys for signing. Tested (reproducible, avalanche, one-way, integrates
  with IdentityService seed).
- Agnostic value types: Offer/OfferType/OfferStatus/DiscoveryQuery/PublishResult,
  Certification.
- Transport interfaces: OfferTransport, MessageTransport (+PrivateMessage),
  TrustTransport — the three sibling contracts.
- WebOfTrust: pure Duniter membership rule (threshold + distance, fixpoint).
  Promoted from the spike; TDD (7 cases).

No network yet (that's the next slice). commons_core: 50 tests green, analyzer
clean. Block 1 APIs unchanged.
2026-07-10 02:31:02 +02:00
05a9d45a82 docs(block2): lift the gate — open the social round
The de-risking spike validated the whole social happy path, so Block 2 moves
from "do NOT start" to STARTED. Records the build order (transport foundation in
commons_core first, on the vetted pure-Dart nostr lib) and marks open-decisions
D.3/D.7 as resolved by the spike.
2026-07-10 02:26:16 +02:00
b840b83c42 feat(species): auto-classify variety species from its label
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.
2026-07-10 02:23:32 +02:00
0e6ef13d00 fix(ui): pin edit-sheet Save button as a fixed footer
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).
2026-07-10 02:19:07 +02:00
cc88f3d688 spike(block2): de-risk web of trust — the last big unknown
Adds TrustTransport as the third interface on the shared NostrConnection,
completing the social-layer happy path in the spike:

- WebOfTrust: pure, Flutter-free Duniter membership rule (N certs from members +
  within distance D of bootstrap referents), iterated to a fixpoint. The
  commons_core-worthy piece; TDD'd (threshold, distance cutoff, transitive
  growth, expired/revoked/self exclusion).
- NostrTrustTransport: certifications as a custom addressable kind (30777,
  keyed by issuer+subject) — certify renews, revoke replaces, certs expire.
- Trust filters spam (network-trust §2): seeds certify Alice; Eve stays
  uncertified; Bob discovers both offers and annotates authors — Alice known,
  Eve unknown — all over ONE shared connection.

Findings updated: WoT risk High -> Medium (policy/bootstrap, not feasibility);
overall scope Medium-High -> Medium — all three transport contracts now proven.
30 tests green, analyzer clean, Block 1 untouched.
2026-07-10 02:17:35 +02:00
2cafc7fc12 spike(block2): de-risk NIP-17 messaging + validate shared-connection shape
Extends the Block 2 spike to attack the highest-risk unbuilt piece (private
messaging) and to prove the Q2 architecture recommendation with running code:

- NostrConnection: one shared socket+key+sign+REQ/EOSE lifecycle. Refactored
  NostrOfferTransport onto it; added NostrMessageTransport — two thin contracts
  on ONE connection (the 'one connection, three interfaces' shape).
- NIP-44 v2 encryption (secp256k1 ECDH -> ChaCha20 + HMAC, length-hiding pad).
- NIP-17/NIP-59 gift-wrap: rumor(14) -> seal(13) -> wrap(1059, ephemeral key).
  Alice->Bob DM round-trips through the relay: encrypted, sender authenticated,
  and metadata-private (relay/eavesdropper sees only ciphertext, an ephemeral
  author and the recipient p-tag — Alice stays hidden).

Findings updated: NIP-17 risk drops Medium-High -> Medium (hardening, not
feasibility); next de-risking target is the web of trust. NIP-44 not yet
vector-verified (noted). 20 tests green, analyzer clean, Block 1 untouched.
2026-07-10 02:11:54 +02:00
733c118c3b chore(i18n): track generated strings.g.dart
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.
2026-07-10 02:09:01 +02:00
2ea8d67cb4 fix(sync): re-seed HLC clock from stored rows so local edits win LWW
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.
2026-07-10 02:06:01 +02:00
46ba85dad0 Merge branch 'claude/cool-swanson-81a747': expand species catalog to ~1200 edible species
# Conflicts:
#	apps/app_seeds/assets/catalog/species.json
#	apps/app_seeds/lib/data/species_repository.dart
2026-07-10 02:04:51 +02:00
959fa2551c feat(species): expand catalog to ~1200 edible species from Wikidata
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.
2026-07-10 02:00:47 +02:00
49b872b405 spike(block2): de-risk social layer — key derivation, OfferTransport, NIP-99 round-trip
Throwaway research spike (spike/ branch, outside pub workspace, no prod deps
touched). Validates the open Block 2 decisions before committing a funded round:

1. Deterministic one-way secp256k1 (Nostr) key derived from the Ğ1 root seed
   via HKDF — user still backs up ONE thing. Reproducible + signs (BIP340).
2. OfferTransport abstraction over Nostr NIP-99 (kind 30402); Offer stays
   inventory/location-agnostic, geohash coarsened on the wire (tested).
3. publish->discover-by-geohash proven end-to-end against an in-process
   hermetic mini-relay (~34ms round-trip).

Findings + risks + recommendation in docs/design/spike-block2-findings.md.
Block 1 suite untouched. 14 tests green, analyzer clean.
2026-07-10 01:57:25 +02:00
6eb1517ffb Merge branch 'main' into claude/trusting-shannon-ff64dd
# Conflicts:
#	apps/app_seeds/lib/i18n/strings.g.dart
2026-07-09 23:56:45 +02:00
4acf227e30 chore(release): beta packaging — signing, changelog, store metadata, scale test
- 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.
2026-07-09 23:10:01 +02:00
5d2b41a110 i18n: RTL smoke tests, Portuguese locale and a translation guide
- 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.
2026-07-09 23:06:49 +02:00
fde0ea6eab feat(species): derived verified reference links (GBIF/Wikipedia/Wikispecies)
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.
2026-07-09 22:44:12 +02:00
d6781870d9 feat(backup): sealed backups + printable recovery sheet
'Save a backup' now writes a sealed .tanemaki file (AES-256-GCM under a
key HKDF-derived from the root seed; format TANEBK v1, open and stable).
Restoring on the same identity is silent; a copy sealed by another
identity asks for the printed recovery code (TANE1 base32, typo-tolerant)
and adopts that seed — recovering your bank recovers you. Legacy plain
exports still restore. Settings gains a recovery sheet (QR + code PDF).
BackupBox/RecoveryCode live in commons_core, pure Dart, TDD.
2026-07-09 22:32:13 +02:00
ba87bf2719 feat(inventory): local sharing — per-lot share terms, filter and printable catalog
The local half of the original Fase 2 (no network, no Offer entity yet):
- Lot sheet gains a 'do you share it?' choice (gift/swap first, sale last,
  never the default); declaring plenty of abundance reveals it with a nudge.
- Lot lines and list tiles badge shared batches; an 'I share' filter and
  a printable PDF catalog (paper bridge for fairs) round out the view.
- Catalog PDF embeds DejaVu (Latin-ext/Cyrillic/Greek); fonts are a
  per-locale resource like the OCR packs.
2026-07-09 22:19:55 +02:00
e3ec855630 style: dart format reflow (no behavior change) 2026-07-09 22:19:43 +02:00
cf5d302cc1 test(state): cover InventoryCubit and VarietyDetailCubit; refresh block1 status doc 2026-07-09 22:03:36 +02:00
a96d82add9 test(ui): scroll to the save button in the taller edit sheet; fix doc lint 2026-07-09 21:58:43 +02:00
383eb3ba37 ci: manual Windows desktop build job (allow_failure) 2026-07-09 21:48:14 +02:00
3db8ca558c docs: public intro narrative and in-app onboarding copy 2026-07-09 21:48:14 +02:00
1f6a3282c1 docs(agents): codify international-by-design and human-words golden rules 2026-07-09 21:48:14 +02:00
7f9f5c065b fix(inventory): merge draft + list watch into one subscription
InventoryCubit subscribed to watchInventory() and watchDrafts()
separately — two overlapping StreamGroups (both watching attachments)
re-emitted in a tight loop that starved the event loop and hung widget
tests' pumpAndSettle for 10+ minutes (home/inventory/quick-add all
timed out). Replace both with a single watchInventoryView() stream that
loads the catalogued list and the draft tray from one StreamGroup.

Full widget suite no longer hangs: 165 pass. One unrelated failure
remains in the v8 variety edit sheet (Save button pushed off-screen by
new fields).
2026-07-09 21:33:48 +02:00
6809dc6143 feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
Lower the bulk-digitization cliff with two more routes on top of the
already-landed CSV import and "save and add another":

- Photo-first drafts (capture now, catalogue later): burst-capture
  photos (camera or multi-gallery) into unnamed draft varieties, shown
  in a "to catalogue" tray, hidden from the main list until named.
  Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft,
  the triage sheet and the inventory banner.
- On-device OCR label suggestion (Tesseract, offline, no Google): a
  "Suggest name from photo" button in the naming dialog behind a
  LabelTextExtractor interface (Tesseract on Android/iOS, no-op
  elsewhere). Reads the largest print via hOCR bounding boxes, drops
  boilerplate/low-confidence noise, preprocesses (grayscale, contrast,
  upscale) and sweeps rotations (0-315 deg) so tilted packets still
  read. Bundles tessdata_fast eng+spa; validated on-device against real
  packets. The photo is written to a temp file deleted immediately in a
  finally block (the plugin needs a path) - a bounded, documented
  exception to no-plaintext-at-rest.

This commit also carries the co-developed schema evolution v5 to v8 that
shares these files (organic flag, species viability years, crop
calendar, lot provenance/abundance/preservation format, condition
checks) plus their exports/migrations and i18n.

Tests: CSV/draft/OCR unit + widget + migration green in isolation.
Note: the full widget suite currently hangs (>10 min) - under investigation.
2026-07-09 21:23:46 +02:00
12a2ee2d64 test(ui): scroll to the About tile now that Backup pushes it below the fold 2026-07-09 14:49:15 +02:00
d3711e39b0 feat(quick-add): save & add another for rapid manual entry
The quick-add sheet no longer closes on every save: a new 'Save & add
another' action saves the seed, clears the form (keeping the chosen lot
type), refocuses the name field and shows an 'N added' running count, so
a whole shelf can be entered without reopening the sheet each time.
Plain Save still closes it. Adds a dedicated label field with its own
controller, submitAndAddAnother() on the cubit, and cubit + widget tests.
2026-07-09 14:45:48 +02:00
89b61bc9e4 feat(inventory): additive CSV import for bulk digitization
Import a spreadsheet list (the same 21-column schema the CSV export
already produces) as new varieties/lots. Tolerant reader: columns
matched by header name in any order, only variety_label required,
unknown enums fall back to a safe default, rows sharing variety_id
collapse into one variety with several lots. Species re-linked by
scientific name. Additive by design (fresh ids + HLC), so it never
overwrites existing rows; JSON stays the canonical id-reconciling import.

Adds a hand-rolled RFC 4180 parser (no new dependency), an importCsv
path in VarietyRepository/ExportImportService, an 'Import from CSV'
tile in the backup section, and en/es strings.

Note: i18n .g.dart also carries pre-existing About/intro strings from
the working tree (regenerated by slang).
2026-07-09 14:41:28 +02:00
21072633b5 fix(ui): darken seedMuted and app-bar green to meet WCAG AA contrast
seedMuted (hints, card subtitles, search placeholder) was #8A9880 —
2.62:1 on the canvas and 3.05:1 on white, failing AA for the 13px text
it colours. Darkened to #617057: 5.30:1 on white, 4.56:1 on canvas,
4.88:1 on the field fill, same muted green-grey hue.

The new contrast test also caught the app-bar green: white titles on
#3E8C38 were 4.19:1, so it is nudged to #3A8434 (4.64:1) — a barely
visible change.

test/ui/theme_contrast_test.dart pins the WCAG relative-luminance math
and asserts every text/background pair the palette actually uses.
2026-07-09 12:50:26 +02:00
2812c99280 feat(inventory): CSV/JSON export and JSON import with LWW reconciliation
Interchange export/import for Phase 1 (data-model §7):

- JSON: canonical, versioned envelope (formatVersion 1) with all sync
  metadata verbatim, photos embedded as base64, tombstones excluded.
  Species are re-resolved on import by scientific name (catalog ids are
  per-install). Reader tolerates unknown fields/enum values (§5.2) and
  rejects newer format versions with a clear error.
- CSV: export-only spreadsheet flatten, one row per lot, RFC 4180
  escaping, never any photo bytes.
- Import merges by UUIDv7 id in one transaction: insert-if-unknown
  preserving original stamps, last-writer-wins by packed HLC for known
  mutable rows, append-only movements; afterwards the local clock
  receiveEvent()s the newest imported stamp so it never runs behind.
- Settings gains a Backup section (export CSV/JSON, import JSON with
  confirmation); file dialogs behind a FileService interface backed by
  file_picker (MIT).
- Tests: codec round-trip and tolerance, reconciler LWW, repository
  export→import round-trip (fresh DB, idempotent re-import, newer-local
  wins, clock monotonicity, species re-resolution), backup widget flows.
2026-07-09 12:46:53 +02:00
136ed701a7 fix(linux): set GTK window icon from bundled app icon 2026-07-09 12:39:29 +02:00
c9e93f7e27 feat(ui): About in drawer; higher-contrast app icon & splash
Add an "About" entry to the navigation drawer (below Settings) so the
About screen is reachable without opening Settings first.

Recolour the launcher icon and native splash to a white sprout on brand
green (#2F7D34) — the previous dark-green-on-light-green had almost no
contrast. Regenerate Android, iOS and web icons and the light/dark splash.
2026-07-09 12:18:37 +02:00
9558115c1e feat(inventory): filter by category and lot form + a11y polish
Add filter chips above the inventory list, one per category in use and
one per lot form actually held; results apply search ∧ category ∧ form.
An active filter that empties the list shows a distinct 'no matches'
state and a clear-filters button. VarietyListItem now carries the set of
lot forms it holds (one aggregate query), and the stream re-emits on lot
changes.

Accessibility: category headers use the text theme so they honour the
system font scale; the list avatar is excluded from semantics (the tile
title already announces the name); the edit action uses the green action
colour (≥3:1 on the canvas, up from 2.62:1). Add category/form/clear
filter tests and a tap-target-size guideline test.
2026-07-09 12:00:34 +02:00
42c16c0e3f feat(ui): Material 3 redesign, cover-photo viewer, About screen
Apply a Material 3 seed-green palette and rounded components across the
home, inventory, quick-add, quantity picker and variety-detail screens.
The full-screen photo viewer can now set any photo as the cover (via
attachment sort order) and delete after confirmation. Lot forms and
packaging surface as choice chips.

Extract About out of Settings into its own screen (app version via
package_info_plus, website link). Add es/en strings for the new lot
types, presentation, home tagline and About. Update Seedees v2 mockups.
2026-07-09 11:51:59 +02:00