Commit graph

43 commits

Author SHA1 Message Date
e7ba305e63 feat: hierarchical subcategory navigation in recipe browser
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Adds a two-level browse tree (domain → category → subcategory) to the
recipe browser, plus an "All" unfiltered option at the top of every
domain.

browser_domains.py:
- Category values now support list[str] (flat) or dict with "keywords"
  and "subcategories" keys — backward compatible with all existing flat
  categories
- Added subcategories to: Italian (Sicilian, Neapolitan, Tuscan, Roman,
  Venetian, Ligurian), Mexican (Oaxacan, Yucatecan, Veracruz, Street
  Food, Mole), Asian (Korean, Japanese, Chinese, Thai, Vietnamese,
  Filipino, Indonesian), Indian (North, South, Bengali, Gujarati),
  Mediterranean (Greek, Turkish, Moroccan, Lebanese, Israeli), American
  (Southern, Cajun/Creole, BBQ, Tex-Mex, New England), European
  (French, Spanish, German, British/Irish, Scandinavian), Latin American
  (Peruvian, Brazilian, Colombian, Cuban, Caribbean), Dinner, Lunch,
  Breakfast, Snack, Dessert, Chicken, Beef, Pork, Fish, Vegetables
- New helpers: category_has_subcategories, get_subcategory_names,
  get_keywords_for_subcategory

store.py:
- get_browser_categories now accepts has_subcategories_by_category and
  includes has_subcategories: bool in each result row
- New get_browser_subcategories method for subcategory count queries

recipes.py endpoints:
- GET /browse/{domain}/{category}/subcategories — returns subcategory
  list with recipe counts (registered before /{subcategory} to avoid
  path collision)
- GET /browse/{domain}/{category} gains optional ?subcategory=X param
  to narrow results within a category
- GET /browse/{domain}/{category}/_all — unfiltered paginated browse
  (landed in previous commit)

api.ts: BrowserCategory adds has_subcategories; new BrowserSubcategory
type; listSubcategories() call; browse() gains subcategory param

RecipeBrowserPanel.vue:
- Category pills show a › indicator when subcategories exist
- Selecting such a category fetches subcategories in the background
  (non-blocking — recipes load immediately at the category level)
- Subcategory row appears below the category list with an
  "All [Category]" pill + one pill per subcategory with count
- Active subcategory highlighted; clicking "All [Category]" resets
  to the full category view
2026-04-18 21:07:06 -07:00
8fd77bd1f2 fix: suppress E2E test sessions from log-based analytics
Add E2E_TEST_USER_ID setting (opt-in via env); session bootstrap logs
at DEBUG instead of INFO for the known test user so test runs don't
inflate session counts.  Still visible with DEBUG=true.
2026-04-18 19:06:37 -07:00
bea61054fa fix: re-fetch inventory item after insert to populate product_name (#99)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-18 16:02:35 -07:00
01aae2eec8 fix: recipe enrichment backfill, main_ingredient browser domain, bug batch
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
CI / Backend (Python) (pull_request) Has been cancelled
CI / Frontend (Vue) (pull_request) Has been cancelled
Recipe corpus (#108):
- Add _MAIN_INGREDIENT_SIGNALS to tag_inferrer.py (Chicken/Beef/Pork/Fish/Pasta/
  Vegetables/Eggs/Legumes/Grains/Cheese) — infers main:* tags from ingredient names
- Update browser_domains.py main_ingredient categories to use main:* tag queries
  instead of raw food terms; recipe_browser_fts now has full 3.19M row coverage
  (was ~1.2K before backfill)

Bug fixes:
- Fix community posts response shape (#96): add total/page/page_size fields
- Fix export endpoint arg types (#92)
- Fix household invite store leak (#93)
- Fix receipts endpoint issues
- Fix saved_recipes endpoint
- Add session endpoint (app/api/endpoints/session.py)

Shopping list:
- Add migration 033_shopping_list.sql
- Add shopping schemas (app/models/schemas/shopping.py)
- Add ShoppingView.vue, ShoppingItemRow.vue, shopping.ts store

Frontend:
- InventoryList, RecipesView, RecipeDetailPanel polish
- App.vue routing updates for shopping view

Docs:
- Add user-facing docs under docs/ (getting-started, user-guide, reference)
- Add screenshots
2026-04-18 15:38:56 -07:00
890216a1f0 fix: wire recipe corpus to cloud per-user DBs via SQLite ATTACH (#102)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Cloud mode: attach shared read-only corpus DB (RECIPE_DB_PATH env var)
as "corpus" schema so per-user SQLite DBs can access 3.19M recipes.
All corpus table references now use self._cp prefix ("corpus." in cloud,
"" in local). FTS5 pseudo-column kept unqualified per SQLite spec.
compose.cloud.yml: bind-mount /Library/Assets/kiwi/kiwi.db read-only.

Also fix batch of audit issues:
- #101: OCR approval used source="receipt_ocr" for inventory_items — use "receipt"
- #89/#100: Shopping confirm-purchase used source="shopping_list" — use "manual"
- #103: Frontend inventory filter sent ?status= but API expects ?item_status=
- #104: InventoryItemUpdate schema missing purchase_date field; store.py allowed set also missing it
- #105: Guest cookie Secure flag tied to CLOUD_MODE instead of X-Forwarded-Proto; broke HTTP direct-port access
2026-04-18 14:21:56 -07:00
e745ce4375 feat: wire meal planner slot editor and meal type picker
Slot click now opens an inline editor panel:
- Pick from saved recipes via dropdown (pre-loaded on mount)
- Or type a custom label
- Clear slot button when a slot is already filled
- Save/Cancel with loading state

Add meal type opens a chip picker showing the types not yet active
(breakfast / lunch / snack minus whatever is already on the plan).
Selecting one calls the new PATCH /meal-plans/{plan_id} endpoint.

Backend:
- PATCH /meal-plans/{plan_id} with UpdatePlanRequest(meal_types)
- store.update_meal_plan_types() UPDATE ... RETURNING *
- 409 on IntegrityError in create_plan (already in place)
2026-04-16 14:23:38 -07:00
dbaf2b6ac8 fix: meal planner week add button crashing on r.name / add duplicate guard
- Fix sqlite3.OperationalError: the recipes table uses `title` not `name`;
  get_plan_slots JOIN was crashing every list_plans call with a 500,
  making the + New week button appear broken (plans were being created
  silently but the selector refresh always failed)
- Add migration 032 to add UNIQUE INDEX on meal_plans(week_start)
  to prevent duplicate plans accumulating while the button was broken
- Raise HTTP 409 on IntegrityError in create_plan so duplicates produce
  a clear error instead of a 500
- Fix mondayOfCurrentWeek to build the date string from local date parts
  instead of toISOString(), which converts through UTC and can produce the
  wrong calendar day for UTC+ timezones
- Add planCreating/planError state to MealPlanView so button shows
  "Creating..." during the request and displays errors inline
2026-04-16 10:46:28 -07:00
c8fdc21c29 feat(export): JSON full-backup download (pantry + saved recipes)
Adds GET /export/json that bundles inventory and saved recipes into a
single timestamped JSON file for data portability. The export envelope
includes schema version and export timestamp so future import logic can
handle version differences.

Frontend: new primary-styled JSON download button in the Export card with
a short description of what is included.

Closes #62
2026-04-16 09:16:33 -07:00
0de6182f48 feat(scan): barcode miss fallback chain — Open Beauty Facts + Open Products Facts
When a barcode is not found in Open Food Facts, the service now tries
Open Beauty Facts and Open Products Facts before giving up. All three
share the same API format; only the host URL differs.

When all databases miss, the scan endpoint sets needs_manual_entry=true
in the result. The frontend detects this, shows a calm informational
message, and switches to manual entry mode automatically.

Also fixes a latent bug where not-found scans showed 'Added: item to
pantry' due to the success condition checking barcodes_found (always 1)
instead of added_to_inventory.

Closes #65
2026-04-16 08:30:49 -07:00
fb18a9c78c feat: partial consumption tracking and waste/disposal logging (#12 #60)
#12 — partial consume:
- POST /inventory/items/{id}/consume now accepts optional {quantity}
  body; decrements by that amount and only marks status=consumed when
  quantity reaches zero (store.partial_consume_item)
- OFFs barcode scan pre-fills sub-unit quantity when product data
  includes a pack size (number_of_units or 'N x ...' quantity string)
- Consume button shows quantity-aware label and opens ActionDialog
  with number input for multi-unit items ('use some or all')
- consumeItem() in api.ts now returns InventoryItem and accepts
  optional quantity param

#60 — disposal logging:
- Migration 031: adds disposal_reason TEXT column to inventory_items
  (status='discarded' was already in the CHECK constraint)
- POST /inventory/items/{id}/discard endpoint with optional DiscardRequest
  body (free text or preset reason)
- Calm framing: 'item not used' not 'wasted'; reason presets avoid
  blame language ('went bad before I could use it', 'too much — had excess')
- Muted discard button (X icon, tertiary color) — not alarming

Shared:
- New ActionDialog.vue component for dialogs with inline inputs
  (quantity stepper or reason dropdown); keeps ConfirmDialog simple
- disposal_reason field added to InventoryItemResponse

Closes #12
Closes #60
2026-04-16 07:28:21 -07:00
64a0abebe3 feat: pantry intel cluster — #61 expiry display, #64 cook log, #66 scaling, #59 open-package tracking
#61: expiry badge now shows relative + calendar date ("5d · Apr 15") with
tooltip "Expires in 5 days (Apr 15)"; traffic-light colors already in place

#64: RecipeDetailPanel.handleCook() calls recipesStore.logCook(); SavedRecipesPanel
shows "Last made: X ago" below each card using cookLog entries

#66: Serving multiplier (1x/2x/3x/4x) in RecipeDetailPanel scales ingredient
quantities using regex; handles integers, decimals, fractions (1/2, 3/4),
mixed numbers (1 1/2), and ranges (2-3); leaves unrecognised strings unchanged

#59: migration 030 adds opened_date column; ExpirationPredictor gains
SHELF_LIFE_AFTER_OPENING table + days_after_opening(); POST /inventory/items/{id}/open
sets opened_date=today and returns computed opened_expiry_date; InventoryList
shows lock-open button for unopened items and an "📂 5d · Apr 15" badge once opened
2026-04-16 06:01:25 -07:00
4423373750 feat: screenshot attachment in feedback form (#82)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
- Backend: new /api/v1/feedback/attach endpoint uploads image to
  Forgejo as an issue asset, then pins it as a comment so the
  screenshot is visible inline on the issue
- Frontend: file input in feedback form (all types, max 5 MB)
  with inline thumbnail preview and remove button
- Attach call is non-fatal: if upload fails after issue creation,
  the issue is still filed and the user sees success
- Screenshot state clears on modal reset

Closes #82
2026-04-15 23:08:02 -07:00
76516abd62 feat: metric/imperial unit preference (#81)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
- Settings: add unit_system key (metric | imperial, default metric)
- Recipe LLM prompts: inject unit instruction into L3 and L4 prompts
  so generated recipes use the user's preferred units throughout
- Frontend: new utils/units.ts converter (mirrors Python units.py)
- Inventory list: display quantities converted to preferred units
- Settings view: metric/imperial toggle with save button
- Settings store: load/save unit_system alongside cooking_equipment

Closes #81
2026-04-15 23:04:29 -07:00
99e9cbb8c1 refactor(kiwi): remove unused LIFETIME_SOURCES import from recipes.py 2026-04-14 15:44:42 -07:00
006582b179 feat(kiwi): add /orch-usage proxy endpoint for frontend budget display 2026-04-14 15:42:58 -07:00
1a6898324c feat(kiwi): merge meal planner feature into main
Adds full meal planning workflow to Kiwi:
- Weekly meal plan creation with configurable meal types (Paid gate)
- Drag-and-assign recipe slots per day
- Prep session generation with sequenced task lists and time estimates
- LLM-assisted full-week plan and timing fill-in (BYOK-unlockable)
- Community feed (local ActivityPub-compat + cloud federation)
- Build Your Own recipe tab with assembly templates
- Save/bookmark any recipe with star rating, notes, and style tags
- Shopping list export from built recipes
- Tab reorder: Saved > Build > Community > Find > Browse
- Auto-redirect from empty Saved tab to Build
- Custom ingredient injection persists in candidate list
- z-index fix: save modal above recipe detail panel
- Route ordering fix: /recipes/saved before /{recipe_id} catch-all
2026-04-14 15:37:57 -07:00
01216b82c3 feat(kiwi): gate L3/L4 recipes behind orch budget check; fallback to L2 on exhaustion 2026-04-14 15:24:57 -07:00
9941227fae chore: merge main into feature/meal-planner
Resolves three conflicts:
- app/api/routes.py: fixed saved_recipes-before-recipes ordering from main;
  meal_plans and community_router from feature branch
- app/db/store.py: meal plan/prep session methods (feature) + community
  pseudonym methods (main) -- both additive
- app/tiers.py: KIWI_BYOK_UNLOCKABLE includes meal_plan_llm,
  meal_plan_llm_timing (feature) and community_fork_adapt (main)
2026-04-14 14:53:52 -07:00
3933136666 fix: save, shopping list, and route ordering for Build Your Own
- Persist built recipes to recipes table on /build so they get real DB IDs
  and can be bookmarked via saved_recipes (FK was pointing at negative IDs)
- Populate missing_ingredients in build_from_selection() from role_overrides
  vs pantry diff -- backend now owns shopping list computation
- Remove client-side cartItems tracking; shopping list derived from
  builtRecipe.missing_ingredients instead
- Fix saved_recipes 422: mount saved_recipes router before recipes router in
  routes.py so /recipes/saved isn't captured by /recipes/{recipe_id}
- Bump SaveRecipeModal z-index to 500 (above detail-overlay at 400)
- Replace "Add to pantry" primary action with "Grocery list" clipboard copy;
  "Add to pantry" demoted to compact secondary button
2026-04-14 14:48:30 -07:00
144d1dc6c4 chore: commit in-progress work -- tag inferrer, imitate endpoint, hall-of-chaos easter egg, migration files, Dockerfile .env defense
- app/services/recipe/tag_inferrer.py: infer tags from recipe ingredient text
- app/db/migrations/022_recipe_generic_flag.sql, 029_inferred_tags.sql: schema migrations
- app/api/endpoints/imitate.py: recipe imitation endpoint stub
- app/api/endpoints/community.py: hall-of-chaos easter egg endpoint
- scripts/pipeline/infer_recipe_tags.py, backfill_keywords.py: pipeline scripts
- scripts/pipeline/build_recipe_index.py: extended index builder
- Dockerfile: explicit .env removal as defense-in-depth
- frontend/src/components/FeedbackButton.vue: feedback UX improvements
- frontend/src/style.css: minor style tweaks
- app/cloud_session.py: cloud session improvements
- tests/api/test_community_endpoints.py: additional test coverage
2026-04-14 13:23:15 -07:00
8c4965123f feat: add GET /templates, GET /template-candidates, POST /build endpoints
Wires the three Build Your Own API routes into the recipes router,
registered before the catch-all /{recipe_id} route to avoid shadowing.
Adds 5 endpoint tests covering template list count/shape, candidate
response structure, successful recipe build, and 404 on unknown template.
2026-04-14 11:45:43 -07:00
86dd9adbcb refactor: use sqlite3.IntegrityError directly for slug collision guard 2026-04-13 11:25:10 -07:00
69e1b70072 fix: community endpoint quality issues — input validation, slot key guard, slug collision, ValueError handling 2026-04-13 11:23:45 -07:00
9ae886aabf fix: community endpoint spec gaps — ld+json content negotiation + premium post tier filter 2026-04-13 11:20:28 -07:00
9c64de2acf feat(community): community API endpoints — browse, publish, fork, delete, RSS, AP
Adds GET /community/posts, GET /community/posts/{slug}, GET /community/feed.rss,
GET /community/local-feed, POST /community/posts, DELETE /community/posts/{slug},
POST /community/posts/{slug}/fork, and POST /community/posts/{slug}/fork-adapt (501 stub).
Wires init_community_store into main.py lifespan. 7 new tests; 115 total passing.
2026-04-13 11:14:18 -07:00
f54127a8cc fix(meal-planner): add GET prep-session endpoint, fix list_plans schema, replace assert with ValueError
- Add GET /{plan_id}/prep-session endpoint so frontend can retrieve existing sessions without creating
- Fix list_plans response_model from list[dict] to list[PlanSummary] with proper _plan_summary() mapping
- Replace assert in store.update_prep_task with ValueError (assert is stripped under python -O)
- Add day_of_week 0-6 validation to upsert_slot endpoint
- Remove MagicMock sqlite artifact files left by pytest (already in .gitignore)
2026-04-12 14:04:53 -07:00
482666907b fix(meal-planner): validate meal_type path param, enforce store whitelist safety, add week_start date validation, make PrepTask frozen
- upsert_slot: raise 422 immediately if meal_type not in VALID_MEAL_TYPES
- update_prep_task: assert whitelist safety contract after dict comprehension
- CreatePlanRequest: week_start typed as date with must_be_monday validator; str() cast at call site
- PrepTask: frozen=True; build_prep_tasks rewired to use (priority, kwargs) tuples so frozen instances are built with correct sequence_order in one pass (no post-construction mutation)
- Move deferred import json to file-level in meal_plans.py
- Fix test dates: "2026-04-14" was a Tuesday; updated request bodies to "2026-04-13" (Monday)
2026-04-12 13:51:50 -07:00
536eedfd6c feat(routes): register meal-plans router at /api/v1/meal-plans
refs kiwi#68
2026-04-12 13:44:08 -07:00
98087120ac feat(api): add /api/v1/meal-plans/ endpoints — CRUD, shopping list, prep session
refs kiwi#68 kiwi#71
2026-04-12 13:44:01 -07:00
793df1b5cf feat: saved recipes, recipe browser, and recipe detail panel
- Saved recipes: save/unsave, star rating, notes, tags, collections (migrations 018-020)
- Recipe browser: domain/category browsing with pantry match badges, pagination
- Recipe detail panel: full directions, ingredient checklist, swap candidates, prep notes
- Grocery links: affiliate links for missing ingredients
- Nutrition filters and display chips on recipe cards
- Bookmark toggle persisted to saved_recipes table
- Tier gates on saved recipes (paid) and collections (premium)
- Browser telemetry for domain/category click tracking
- Cloud compose: CLOUD_DATA_ROOT volume mount for per-user SQLite trees
- manage.sh: cf-orch agent sidecar in local stack
- README: updated feature list and stack description
2026-04-08 14:35:02 -07:00
c064933b14 refactor: use shorter circuitforge_core.api import for feedback router 2026-04-05 21:21:50 -07:00
f3bc796f2c feat: migrate feedback endpoint to circuitforge-core router
Replace hand-rolled feedback.py with make_feedback_router() from
circuitforge_core.api.feedback. Update tests to mount the shared
router on a minimal FastAPI app and mock at the core module level.
2026-04-05 17:57:36 -07:00
6a59c8dfd1 fix: row_factory in _household_store; validate household_id from Heimdall; monkeypatch CLOUD_DATA_ROOT in accept test; add KIWI_BASE_URL to compose.cloud.yml 2026-04-04 22:59:06 -07:00
c7861344b7 feat: add MessageResponse schema; wire response_model on leave + remove-member endpoints 2026-04-04 22:47:39 -07:00
7650747651 feat: household API endpoints (create, status, invite, accept, leave, remove-member) 2026-04-04 22:45:12 -07:00
6791ea22b2 feat: complete feedback button — status probe, requests dep, tests
- feedback.py: add GET /feedback/status endpoint (returns {enabled: bool})
  so frontend can probe on mount instead of optimistic-enable; remove
  unused get_db import
- FeedbackButton.vue: probe /feedback/status on mount, start hidden;
  drop redundant 503-hide path (status probe makes it redundant)
- pyproject.toml: declare requests>=2.31 (used by feedback.py Forgejo calls)
- tests/api/test_feedback.py: 7 tests — status endpoint (no-token, token,
  demo mode), POST 503/403, happy path with mocked Forgejo, 502 on error
2026-04-03 16:52:40 -07:00
61c16af754 feat: in-app feedback FAB + mobile safe-area-inset fixes
- FeedbackButton.vue — floating action button; fires POST /feedback if
  FORGEJO_API_TOKEN is configured, silently hidden otherwise
- feedback.py endpoint — creates Forgejo issues with beta-feedback +
  needs-triage labels; includes version, OS, device info
- App.vue: wire FeedbackButton; use env(safe-area-inset-bottom) for
  bottom-nav clearance (fixes clipped content on iPhone home-bar)
- style.css: overflow-x: hidden prevents stray elements expanding
  mobile viewport
2026-04-03 16:47:50 -07:00
8fec5b6402 chore: inventory endpoint cleanup, expiry predictor, tiers, gitignore test artifacts 2026-04-02 22:12:51 -07:00
33a5cdec37 feat: cloud auth bypass, VRAM leasing, barcode EXIF fix, pipeline improvements
- cloud_session.py: CLOUD_AUTH_BYPASS_IPS with CIDR support; X-Real-IP for
  Docker bridge NAT-aware client IP resolution; local-dev DB path under
  CLOUD_DATA_ROOT for bypass sessions
- compose.cloud.yml: thread CLOUD_AUTH_BYPASS_IPS from shell env; document
  Docker bridge CIDR requirement in .env.example
- nginx.cloud.conf + nginx.conf: client_max_body_size 20m for barcode uploads
- barcode_scanner.py: EXIF orientation correction (PIL ImageOps.exif_transpose)
  before cv2 decode; rotation coverage extended to [90, 180, 270, 45, 135]
  to catch sideways barcodes the 270° case was missing
- llm_recipe.py: CF-core VRAM lease acquire/release wrapping LLMRouter calls
- tasks/runner.py + config.py: COORDINATOR_URL + recipe_llm VRAM budget (4GB)
- recipes.py: per-request Store creation inside asyncio.to_thread worker to
  avoid SQLite check_same_thread violations
- download_datasets.py: HF_PARQUET_FILES strategy for repos without dataset
  builders (lishuyang/recipepairs direct parquet download)
- derive_substitutions.py: use recipepairs_recipes.parquet for ingredient
  lookup; numpy array detection; JSON category parsing
- test_build_flavorgraph_index.py: rewritten for CSV-based index format
- pyproject.toml: add Pillow>=10.0 for EXIF rotation support
2026-04-01 16:06:23 -07:00
9371df1c95 feat: recipe engine Phase 3 — StyleAdapter, LLM levels 3-4, user settings
Task 13: StyleAdapter with 5 cuisine templates (Italian, Latin, East Asian,
Eastern European, Mediterranean). Each template includes weighted method_bias
(sums to 1.0), element-filtered aromatics/depth/structure helpers, and
seasoning/finishing-fat vectors. StyleTemplate is a fully immutable frozen
dataclass with tuple fields.

Task 14: LLMRecipeGenerator for Levels 3 and 4. Level 3 builds a structured
element-scaffold prompt; Level 4 generates a minimal wildcard prompt (<1500
chars). Allergy hard-exclusion wired through RecipeRequest.allergies into
both prompt builders and the generate() call path. Parsed LLM response
(title, ingredients, directions, notes) fully propagated to RecipeSuggestion.

Task 15: User settings key-value store. Migration 012 adds user_settings
table. Store.get_setting / set_setting with upsert. GET/PUT /settings/{key}
endpoints with Pydantic SettingBody, key allowlist, get_session dependency.
RecipeEngine reads cooking_equipment from settings when hard_day_mode=True.

55 tests passing.
2026-03-31 14:15:18 -07:00
ea22dc8b47 fix: recipes endpoint — inject session tier before all gate checks 2026-03-31 12:52:10 -07:00
ce61b5f422 feat: recipe + staple API endpoints with tier gating 2026-03-31 12:49:38 -07:00
8cbde774e5 chore: initial commit — kiwi Phase 2 complete
Pantry tracker app with:
- FastAPI backend + Vue 3 SPA frontend
- SQLite via circuitforge-core (migrations 001-005)
- Inventory CRUD, barcode scan, receipt OCR pipeline
- Expiry prediction (deterministic + LLM fallback)
- CF-core tier system integration
- Cloud session support (menagerie)
2026-03-30 22:20:48 -07:00