diff --git a/cauldron/db.py b/cauldron/db.py index 4f8c62c..d491696 100644 --- a/cauldron/db.py +++ b/cauldron/db.py @@ -375,6 +375,23 @@ class DB: # --- mealie token ops --------------------------------------------------- + def first_usable_mealie_token(self) -> dict | None: + """Return the freshest (last_validated DESC) per-user encrypted Mealie + token row. Used at boot as a fallback when the system MEALIE_API_TOKEN + env is dead — group admins can list foods, so any user's token works + for the backfill scan.""" + with self.conn() as c, c.cursor() as cur: + cur.execute( + """SELECT authentik_sub, encrypted_token + FROM cauldron_user_mealie_tokens + WHERE last_failure_at IS NULL OR + last_validated > COALESCE(last_failure_at, '1970-01-01') + ORDER BY last_validated DESC, created_at DESC + LIMIT 1""" + ) + row = cur.fetchone() + return dict(row) if row else None + def get_user_mealie_token_blob(self, sub: str) -> bytes | None: with self.conn() as c, c.cursor() as cur: cur.execute( diff --git a/cauldron/server.py b/cauldron/server.py index ba7e174..a5fd2ab 100644 --- a/cauldron/server.py +++ b/cauldron/server.py @@ -71,12 +71,27 @@ def create_app() -> Flask: # One-time backfill: re-key the legacy cauldron_foods (USDA + curated) # densities into cauldron_food_metadata, keyed by Mealie food.id. Runs - # only when the new metadata table is empty. After this lands, the - # legacy parallel-name catalog is no longer authoritative — Step 4 - # cleanup can drop cauldron_foods entirely. + # only when the new metadata table is empty. The system MEALIE_API_TOKEN + # may be expired (Cobb's "Cauldron" token was minted long ago); fall + # back to any stored per-user token since household admins can list + # all foods anyway. + def _resolve_backfill_mealie() -> "Mealie": + try: + system_mealie.who_am_i() + return system_mealie + except Exception: + stash = db.first_usable_mealie_token() + if not stash: + raise RuntimeError("no usable Mealie token (system + user both unavailable)") + return Mealie( + base_url=cfg.mealie_api_url, + api_token=crypto.decrypt(stash["encrypted_token"]), + ) + try: if foods.metadata_count(db) == 0: - stats = foods.backfill_seed_from_legacy(db, system_mealie) + mealie_for_backfill = _resolve_backfill_mealie() + stats = foods.backfill_seed_from_legacy(db, mealie_for_backfill) app.logger.info( "cauldron_food_metadata backfill: matched=%d missed=%d total_mealie=%d", stats.get("matched", 0),