Commit graph

18 commits

Author SHA1 Message Date
f1aa8f8e66 feat(app): scan a printed seed label back into its record
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.
2026-07-18 12:26:27 +02:00
d0fabb80a9 fix(android): drop geolocator for a native LocationManager channel
Removes the geolocator dependency, which transitively embedded
com.google.android.gms.* (play-services-location) — proprietary classes
that F-Droid's scanner rejects. The optional coarse-location button now
talks to Android's own LocationManager over a small platform channel
(org.comunes.tane/coarse_location): permission prompt, single coarse fix
with a 12s timeout, last-known fallback, null on any failure. Behavior
is unchanged and the CoarseLocationProvider interface is preserved, so
the UI, i18n and widget tests need no changes.

Also sets dependenciesInfo.includeInApk/Bundle = false so the AGP
dependency-metadata block (also flagged by F-Droid) is not embedded.
Neither change affects the Google Play build.
2026-07-17 09:19:05 +02:00
f6967e8cbb feat(profile): Material default avatar + square photo crop
Drop the DiceBear generated avatar (and dicebear_core/dicebear_styles/
flutter_svg): the default is again a Material coloured-initial disc from
the pubkey. Picking a photo now goes through a square crop step
(crop_your_image — pure Flutter, all platforms incl. desktop, Apache-2.0,
only pulls the image package we already had) before the 24 KB thumbnail.
2026-07-12 23:39:56 +02:00
109c85298c feat(profile): default avatars via DiceBear "thumbs" (CC0), drop boring_avatars
Replace the flutter_boring_avatars fallback with a friendly DiceBear
"thumbs" face, generated offline (dicebear_core) and deterministically
from the pubkey, rendered as SVG (flutter_svg). The thumbs style is
CC0 1.0 (public domain) — no attribution, AGPL-clean — unlike Multiavatar
whose custom licence restricts use. Style parsed once and per-pubkey SVG
memoized; the CC0 <metadata> block is stripped to keep flutter_svg quiet.
2026-07-12 21:59:10 +02:00
1c2b3d5e8d feat(profile): generated default avatars (boring-avatars beam) from pubkey
People without a photo or seed illustration now get a deterministic
pattern avatar drawn from their pubkey (flutter_boring_avatars, MIT),
in an earthy seed-tone palette, replacing the coloured-initial disc.
Same pubkey renders the same picture on every device with nothing shared.
2026-07-12 21:59:10 +02:00
5fe0f4540e feat(messages): unread badges + OS notification for private messages
Private messages (NIP-17) arrived in the foreground with no signal: no
unread count, no badge, no OS notification. Hook both into the single
InboxService.ingest choke point.

- UnreadService: per-peer "last read" in the keystore (timestamps+pubkey
  only, no message text), live count via a changes stream, active-peer
  suppression so the open chat never badges or notifies.
- UnreadBadge: reusable, RTL-aware (Flutter Badge) — on the home hamburger,
  the drawer "Chat" item, and per-conversation in the messages list.
- Mark-read on chat open (ChatScreen).
- NotificationService over flutter_local_notifications: generic
  "New message from <name>" (no message text, for privacy), payload = pubkey,
  tap opens /chat/:pubkey. No-op on web/Windows; Android POST_NOTIFICATIONS.
- i18n notifications.newMessageFrom (en/es/pt/ast).

Background/push stays out of scope (foreground-only by design).

Tests: UnreadService, NotificationService (mock plugin), InboxService hooks
(new peer notifies/counts; duplicate, own, and open-chat do not), and a
UnreadBadge widget test. dart analyze clean; commons_core green.
2026-07-10 21:12:00 +02:00
1ac954a09e feat(block2): global 'you're offline' banner (network state)
Feedback: offline, the social layer stops working with no clear reason. A slim
amber strip now shows above every screen when there's no network (connectivity_
plus), so it's obvious WHY sharing is paused — the inventory keeps working.
Wrapped once via the MaterialApp builder; connectivity check is guarded so it
no-ops where the plugin is absent (tests/unsupported platforms). i18n en/es/pt.
Analyzer clean.
2026-07-10 15:18:16 +02:00
cf99b7ff87 Merge branch 'main' into spike/block2-derisking
# Conflicts:
#	apps/app_seeds/lib/app.dart
#	apps/app_seeds/lib/i18n/strings.g.dart
#	apps/app_seeds/lib/main.dart
2026-07-10 10:50:16 +02:00
f50a4737cb 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
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
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
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
f5c36f2369 chore(app): launcher icons, native splash, and web platform
Add flutter_launcher_icons + flutter_native_splash config (green sprout
on the #E7F1E0 canvas, light & dark) and regenerate Android/iOS assets.
Enable the web platform (favicon, manifest, PWA icons). Add
package_info_plus for the version string shown on the About screen.
2026-07-09 11:51:38 +02:00
b23f4f1c77 feat(inventory): external links on the variety detail
Add Wikipedia/forum-style URLs to a variety:
- Repository: addExternalLink/removeExternalLink (soft delete); watchVariety
  loads links and re-emits on external_links changes; ExternalLinkItem model
  (title preferred over URL for display).
- Cubit: addExternalLink/removeExternalLink.
- Detail: a "Links" section with tappable link chips (open via url_launcher,
  http(s):// prepended if missing), delete per chip, and an "Add link" dialog
  (URL + optional title).

Tests: repo add/remove + widget add/remove. 52 green; Linux runs.
2026-07-08 13:38:31 +02:00
7e92410728 refine(quantity): drop handful, reorder scale, potted-plant icon, year dropdown
Feedback from the running app:
- Remove "handful" from the scale (redundant with spoon); keep the enum value.
- Reorder so "packet" (sobre) follows "spoon" (cuchara).
- Use the Material Symbols potted-plant for the plant lot type and plant unit
  (adds material_symbols_icons).
- Harvest year is now a dropdown of recent years instead of free text.

38 tests green; Linux build runs with the new icon font bundled.
2026-07-08 11:03:57 +02:00
c4b80ab4a9 fix(db): drop sqlite3_flutter_libs to stop SQLCipher symbol-clash segfault
drift_flutter pulled sqlite3_flutter_libs, whose bundled plain SQLite exports
the same sqlite3_* symbols as sqlcipher_flutter_libs. With both loaded the app
segfaulted at startup on Linux desktop when opening the encrypted DB (the app
built and the Dart VM started, then crashed natively — "Lost connection").

drift_flutter was unused (we open the DB via our own encrypted executor), so
removing it drops sqlite3_flutter_libs and leaves SQLCipher as the only native
sqlite provider. 37 tests green; Linux build runs without crashing.
2026-07-07 23:21:05 +02:00
040f15a898 feat(block1): inventory walking skeleton + first quick-add slice
Stand up the Tanemaki monorepo and the first end-to-end vertical slice of
Block 1 (offline, encrypted inventory): add a seed → see it in a categorized,
searchable list → it persists → reopen and it's still there.

Architecture (state management like G1nkgo, adapted to Tane's reality):
- flutter_bloc (Cubit-first), but the encrypted Drift DB is the single source
  of truth; cubits stream from repositories (no hydrated_bloc/Hive, which would
  write plaintext at rest).
- get_it composition root; go_router; slang i18n (ES/EN, Weblate-friendly JSON).

Workspace & core:
- pub workspace: packages/commons_core (pure Dart) + apps/app_seeds (Flutter).
- commons_core primitives: UUIDv7 IdGen, Hybrid Logical Clock, Quantity value
  type (+ plant-aware QuantityKind), IdentityService root-seed stub.

Data & security:
- Drift schemaVersion=1 with all 10 Block-1 tables + common CRDT columns
  (HLC updated_at, last_author, tombstones); Movement append-only.
- SQLCipher via an injectable executor that refuses to open a plaintext DB;
  DB key + root seed in the OS keystore (separate secrets).
- Exported drift_schema_v1.json + migration scaffold.

Tests (near-TDD; nothing merges without tests):
- commons_core units (24), Drift migration test, "no plaintext at rest"
  security guard (runs where SQLCipher is present, skips otherwise),
  repository, widget, full quick-add flow, file-reopen persistence, and a
  no-hardcoded-strings i18n guard. GitLab CI: format + analyze + test + coverage.

Follow-on Block-1 stories (item detail/edit, species catalog, germination UI)
and all of Block 2 remain out of scope.
2026-07-07 15:16:14 +02:00