discover: per-household imports + group-aware UI
Audit-driven cleanup of the multi-household design:
- migration 035: cauldron_households.mealie_group_id (was missing — blocked
any cross-household reasoning)
- migration 036: cauldron_recipe_index.mealie_group_id (additive; current
per-household keying preserved, the column lets future code key by group
without a destructive PK rebuild)
- migration 037: drop dead cauldron_food_mapping (replaced by
cauldron_food_metadata in foundation reset Step 2 / commit f74a627
2026-04-30; zero callers in the codebase)
- migration 038: cauldron_discover_imports(discover_id, household_id,
mealie_slug, imported_by_sub, imported_at) — per-household provenance.
Replaces the global cauldron_discovered_recipes.status='imported' flag
that incorrectly hid the import button from every user once anyone
imported.
Code:
- sync_user_household reads who_am_i()["groupId"] (and nested group.id)
and persists it on cauldron_households.mealie_group_id
- upsert_household accepts mealie_group_id with COALESCE-on-update
- new helpers: record_discover_import, get_discover_imports_for_group,
discover_imported_by_household, get_household
- /api/discover/import: per-household idempotency (returns cached slug if
this household already imported), records to the new join table; no
longer flips global status='imported'
- /api/discover/search: decorates each row with imported_in_my_group +
imported_by_my_household + mealie_slug + imported_by_household_name
- discover.html: card render uses imported_in_my_group; shows
"✓ in your library as <slug>" when this household imported, or
"✓ shared from <Other Household> as <slug>" when another household in
the group imported (recipe is group-shared via Mealie's group-scope
read so re-importing would create a duplicate)
The imported badge now correctly surfaces "imported by another household
in your group" rather than hiding the row entirely.
This commit is contained in:
parent
09d716116a
commit
fb94da7cce
3 changed files with 235 additions and 16 deletions
157
cauldron/db.py
157
cauldron/db.py
|
|
@ -554,6 +554,56 @@ MIGRATIONS = [
|
||||||
INDEX idx_state (state)
|
INDEX idx_state (state)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
""",
|
""",
|
||||||
|
# 035 — Track the Mealie GROUP a household belongs to. Until now cauldron
|
||||||
|
# only stored mealie_household_id, but Mealie's recipe / food / unit /
|
||||||
|
# category / tag / tool tables are GROUP-scoped on read (cauldron audit
|
||||||
|
# 2026-05-01). To reason about "is this discover import already in my
|
||||||
|
# group's library" we need the group id. Backfilled at boot via
|
||||||
|
# sync_user_household reading who_am_i()["groupId"].
|
||||||
|
"""
|
||||||
|
ALTER TABLE cauldron_households
|
||||||
|
ADD COLUMN IF NOT EXISTS mealie_group_id VARCHAR(64) NULL,
|
||||||
|
ADD INDEX IF NOT EXISTS idx_mealie_group_id (mealie_group_id)
|
||||||
|
""",
|
||||||
|
# 036 — Same column on cauldron_recipe_index. The index is per-household
|
||||||
|
# today (2x storage for a 2-household group) but the underlying recipes
|
||||||
|
# are group-shared. Adding this column doesn't change current write
|
||||||
|
# behavior; it lets future code de-duplicate the cache keyed by group.
|
||||||
|
"""
|
||||||
|
ALTER TABLE cauldron_recipe_index
|
||||||
|
ADD COLUMN IF NOT EXISTS mealie_group_id VARCHAR(64) NULL,
|
||||||
|
ADD INDEX IF NOT EXISTS idx_recipe_group_id (mealie_group_id)
|
||||||
|
""",
|
||||||
|
# 037 — Drop the dead cauldron_food_mapping table. Foundation-reset
|
||||||
|
# Step 2 (commit f74a627 2026-04-30) replaced it with cauldron_food_metadata
|
||||||
|
# which is keyed by Mealie's food UUID and is therefore already group-
|
||||||
|
# implicit. cauldron_food_mapping has zero callers in the codebase as of
|
||||||
|
# 2026-05-01.
|
||||||
|
"""
|
||||||
|
DROP TABLE IF EXISTS cauldron_food_mapping
|
||||||
|
""",
|
||||||
|
# 038 — Per-household provenance for /discover imports. Replaces the
|
||||||
|
# global `cauldron_discovered_recipes.status='imported'` flag, which
|
||||||
|
# incorrectly hid the import-button from every household once any user
|
||||||
|
# imported. The new model: a discover row stays at status='enriched'
|
||||||
|
# forever (or 'rejected' if explicitly skipped); imports are recorded
|
||||||
|
# per-household here. The /discover search endpoint joins this against
|
||||||
|
# the user's group's households so the UI can show "✓ already in your
|
||||||
|
# group's library" without forcing every household to re-import.
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS cauldron_discover_imports (
|
||||||
|
discover_id BIGINT NOT NULL,
|
||||||
|
household_id BIGINT NOT NULL,
|
||||||
|
mealie_slug VARCHAR(255) NOT NULL,
|
||||||
|
imported_by_sub VARCHAR(190),
|
||||||
|
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (discover_id, household_id),
|
||||||
|
INDEX idx_household (household_id),
|
||||||
|
INDEX idx_slug (mealie_slug),
|
||||||
|
FOREIGN KEY (discover_id) REFERENCES cauldron_discovered_recipes(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (household_id) REFERENCES cauldron_households(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -690,16 +740,28 @@ class DB:
|
||||||
|
|
||||||
# --- households ---------------------------------------------------------
|
# --- households ---------------------------------------------------------
|
||||||
|
|
||||||
def upsert_household(self, *, mealie_household_id: str, name: str) -> int:
|
def upsert_household(
|
||||||
"""Create or update a household record. Returns local PK (id)."""
|
self,
|
||||||
|
*,
|
||||||
|
mealie_household_id: str,
|
||||||
|
name: str,
|
||||||
|
mealie_group_id: str | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""Create or update a household record. Returns local PK (id).
|
||||||
|
`mealie_group_id` is optional for back-compat with older callers but
|
||||||
|
should be supplied by sync_user_household so cross-household reasoning
|
||||||
|
works."""
|
||||||
with self.conn() as c, c.cursor() as cur:
|
with self.conn() as c, c.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO cauldron_households (mealie_household_id, name)
|
INSERT INTO cauldron_households (mealie_household_id, name, mealie_group_id)
|
||||||
VALUES (%s, %s)
|
VALUES (%s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE name = VALUES(name), id = LAST_INSERT_ID(id)
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
mealie_group_id = COALESCE(VALUES(mealie_group_id), mealie_group_id),
|
||||||
|
id = LAST_INSERT_ID(id)
|
||||||
""",
|
""",
|
||||||
(mealie_household_id, name),
|
(mealie_household_id, name, mealie_group_id),
|
||||||
)
|
)
|
||||||
return cur.lastrowid
|
return cur.lastrowid
|
||||||
|
|
||||||
|
|
@ -723,6 +785,16 @@ class DB:
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
return row["household_id"] if row else None
|
return row["household_id"] if row else None
|
||||||
|
|
||||||
|
def get_household(self, household_id: int) -> dict | None:
|
||||||
|
"""Return a household row by local id, including mealie_group_id."""
|
||||||
|
with self.conn() as c, c.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT id, mealie_household_id, mealie_group_id, name, created_at
|
||||||
|
FROM cauldron_households WHERE id=%s""",
|
||||||
|
(household_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
def list_household_member_subs(self, household_id: int) -> list[str]:
|
def list_household_member_subs(self, household_id: int) -> list[str]:
|
||||||
with self.conn() as c, c.cursor() as cur:
|
with self.conn() as c, c.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
@ -2186,7 +2258,10 @@ class DB:
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_discovered_status(self, discover_id: int, status: str) -> None:
|
def set_discovered_status(self, discover_id: int, status: str) -> None:
|
||||||
"""Move a discovered recipe to 'imported' or 'rejected'."""
|
"""Move a discovered recipe's GLOBAL status. Use for 'rejected' only;
|
||||||
|
per-household 'imported' is now tracked in cauldron_discover_imports
|
||||||
|
(see record_discover_import). Kept for back-compat — the column
|
||||||
|
stays for ENUM compatibility."""
|
||||||
with self.conn() as c, c.cursor() as cur:
|
with self.conn() as c, c.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""UPDATE cauldron_discovered_recipes
|
"""UPDATE cauldron_discovered_recipes
|
||||||
|
|
@ -2195,6 +2270,74 @@ class DB:
|
||||||
(status, discover_id),
|
(status, discover_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def record_discover_import(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
discover_id: int,
|
||||||
|
household_id: int,
|
||||||
|
mealie_slug: str,
|
||||||
|
imported_by_sub: str | None,
|
||||||
|
) -> None:
|
||||||
|
"""Record that a household has imported a discovered recipe into its
|
||||||
|
Mealie library. INSERT IGNORE so a duplicate POST is a no-op rather
|
||||||
|
than a 409 — the user can click import twice and we just keep the
|
||||||
|
first slug we recorded. Composite PK (discover_id, household_id)."""
|
||||||
|
with self.conn() as c, c.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT IGNORE INTO cauldron_discover_imports
|
||||||
|
(discover_id, household_id, mealie_slug, imported_by_sub)
|
||||||
|
VALUES (%s, %s, %s, %s)""",
|
||||||
|
(discover_id, household_id, mealie_slug[:255], imported_by_sub),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_discover_imports_for_group(
|
||||||
|
self, *, mealie_group_id: str
|
||||||
|
) -> dict[int, dict]:
|
||||||
|
"""Return {discover_id: {household_id, household_name, mealie_slug,
|
||||||
|
imported_at}} for every import by ANY household in the given Mealie
|
||||||
|
group. The /discover search endpoint uses this to mark cards as
|
||||||
|
'already in your group's library' since Mealie's group-scope read
|
||||||
|
means a recipe imported by household A is visible to household B."""
|
||||||
|
if not mealie_group_id:
|
||||||
|
return {}
|
||||||
|
with self.conn() as c, c.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT i.discover_id, i.household_id, h.name AS household_name,
|
||||||
|
h.mealie_household_id, i.mealie_slug, i.imported_at
|
||||||
|
FROM cauldron_discover_imports i
|
||||||
|
JOIN cauldron_households h ON h.id = i.household_id
|
||||||
|
WHERE h.mealie_group_id = %s""",
|
||||||
|
(mealie_group_id,),
|
||||||
|
)
|
||||||
|
out: dict[int, dict] = {}
|
||||||
|
for r in cur.fetchall() or []:
|
||||||
|
did = r["discover_id"]
|
||||||
|
row = dict(r)
|
||||||
|
if hasattr(row.get("imported_at"), "isoformat"):
|
||||||
|
row["imported_at"] = row["imported_at"].isoformat()
|
||||||
|
# First-import-wins per discover_id (oldest reaches us first
|
||||||
|
# by PK order, but we don't sort — preserve insertion order)
|
||||||
|
out.setdefault(did, row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def discover_imported_by_household(
|
||||||
|
self, *, discover_id: int, household_id: int
|
||||||
|
) -> dict | None:
|
||||||
|
"""Look up the import record for one (discover, household) pair.
|
||||||
|
Used by /api/discover/import to detect re-imports."""
|
||||||
|
with self.conn() as c, c.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT discover_id, household_id, mealie_slug,
|
||||||
|
imported_by_sub, imported_at
|
||||||
|
FROM cauldron_discover_imports
|
||||||
|
WHERE discover_id=%s AND household_id=%s""",
|
||||||
|
(discover_id, household_id),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and hasattr(row.get("imported_at"), "isoformat"):
|
||||||
|
row["imported_at"] = row["imported_at"].isoformat()
|
||||||
|
return row
|
||||||
|
|
||||||
def get_discovered_recipe(self, discover_id: int) -> dict | None:
|
def get_discovered_recipe(self, discover_id: int) -> dict | None:
|
||||||
with self.conn() as c, c.cursor() as cur:
|
with self.conn() as c, c.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,22 @@ def create_app() -> Flask:
|
||||||
if not h_id_mealie:
|
if not h_id_mealie:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
local_id = db.upsert_household(mealie_household_id=str(h_id_mealie), name=str(h_name))
|
# Mealie's user-self also exposes the parent group id. Capture both
|
||||||
|
# shapes (top-level groupId AND nested group object) so we can reason
|
||||||
|
# about cross-household visibility within the same group.
|
||||||
|
g_id_mealie: str | None = None
|
||||||
|
g = me.get("group")
|
||||||
|
if isinstance(g, dict):
|
||||||
|
g_id_mealie = g.get("id") or g.get("slug")
|
||||||
|
elif isinstance(g, str) and g:
|
||||||
|
g_id_mealie = g
|
||||||
|
g_id_mealie = g_id_mealie or me.get("groupId") or me.get("group_id") or me.get("groupSlug") or me.get("group_slug")
|
||||||
|
|
||||||
|
local_id = db.upsert_household(
|
||||||
|
mealie_household_id=str(h_id_mealie),
|
||||||
|
name=str(h_name),
|
||||||
|
mealie_group_id=str(g_id_mealie) if g_id_mealie else None,
|
||||||
|
)
|
||||||
existing = db.list_household_member_subs(local_id)
|
existing = db.list_household_member_subs(local_id)
|
||||||
role = "admin" if not existing else "member"
|
role = "admin" if not existing else "member"
|
||||||
db.add_household_member(local_id, sub, role=role)
|
db.add_household_member(local_id, sub, role=role)
|
||||||
|
|
@ -1933,6 +1948,19 @@ def create_app() -> Flask:
|
||||||
offset=max(int(args.get("offset") or 0), 0),
|
offset=max(int(args.get("offset") or 0), 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Decorate each row with per-household / per-group import status.
|
||||||
|
# Mealie's group-share semantics mean a recipe imported by household-A
|
||||||
|
# is visible to household-B in the same group via group-scoped read.
|
||||||
|
# So: hide the import button if ANY household in the caller's group
|
||||||
|
# has imported it; show it otherwise.
|
||||||
|
my_hid = current_household_id()
|
||||||
|
my_household = db.get_household(my_hid) if my_hid else None
|
||||||
|
my_group_id = (my_household or {}).get("mealie_group_id")
|
||||||
|
group_imports = (
|
||||||
|
db.get_discover_imports_for_group(mealie_group_id=my_group_id)
|
||||||
|
if my_group_id else {}
|
||||||
|
)
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
meta = r.get("meta_json")
|
meta = r.get("meta_json")
|
||||||
|
|
@ -1948,17 +1976,44 @@ def create_app() -> Flask:
|
||||||
# scraped_json can be heavy — drop it from list responses
|
# scraped_json can be heavy — drop it from list responses
|
||||||
r.pop("scraped_json", None)
|
r.pop("scraped_json", None)
|
||||||
r["meta_json"] = meta
|
r["meta_json"] = meta
|
||||||
|
|
||||||
|
imp = group_imports.get(r["id"])
|
||||||
|
if imp:
|
||||||
|
r["imported_in_my_group"] = True
|
||||||
|
r["imported_by_my_household"] = (imp["household_id"] == my_hid)
|
||||||
|
r["mealie_slug"] = imp["mealie_slug"]
|
||||||
|
r["imported_by_household_name"] = imp["household_name"]
|
||||||
|
r["imported_at"] = imp["imported_at"]
|
||||||
|
else:
|
||||||
|
r["imported_in_my_group"] = False
|
||||||
|
r["imported_by_my_household"] = False
|
||||||
out.append(r)
|
out.append(r)
|
||||||
return jsonify({"recipes": out, "count": len(out)})
|
return jsonify({"recipes": out, "count": len(out)})
|
||||||
|
|
||||||
@app.post("/api/discover/import/<int:discover_id>")
|
@app.post("/api/discover/import/<int:discover_id>")
|
||||||
@require_session
|
@require_session
|
||||||
def discover_import(discover_id: int):
|
def discover_import(discover_id: int):
|
||||||
|
u = session["user"]
|
||||||
row = db.get_discovered_recipe(discover_id)
|
row = db.get_discovered_recipe(discover_id)
|
||||||
if not row:
|
if not row:
|
||||||
return jsonify({"error": "not_found"}), 404
|
return jsonify({"error": "not_found"}), 404
|
||||||
if row.get("status") == "imported":
|
if row.get("status") == "rejected":
|
||||||
return jsonify({"error": "already_imported"}), 409
|
return jsonify({"error": "row_rejected"}), 409
|
||||||
|
hid = current_household_id()
|
||||||
|
if not hid:
|
||||||
|
return jsonify({"error": "no_household"}), 409
|
||||||
|
# Per-household idempotency: if THIS household already imported this
|
||||||
|
# discover row, return the cached slug instead of double-importing
|
||||||
|
# (which Mealie would happily accept and create "(1)" suffixed dup).
|
||||||
|
existing = db.discover_imported_by_household(
|
||||||
|
discover_id=discover_id, household_id=hid
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"slug": existing["mealie_slug"],
|
||||||
|
"already_imported": True,
|
||||||
|
})
|
||||||
client = current_user_mealie()
|
client = current_user_mealie()
|
||||||
if client is None:
|
if client is None:
|
||||||
return jsonify({"error": "mealie_not_connected"}), 409
|
return jsonify({"error": "mealie_not_connected"}), 409
|
||||||
|
|
@ -1966,7 +2021,12 @@ def create_app() -> Flask:
|
||||||
new_slug = client.import_from_url(row["source_url"])
|
new_slug = client.import_from_url(row["source_url"])
|
||||||
except MealieError as e:
|
except MealieError as e:
|
||||||
return jsonify({"error": "mealie_import_failed", "detail": str(e)[:300]}), 502
|
return jsonify({"error": "mealie_import_failed", "detail": str(e)[:300]}), 502
|
||||||
db.set_discovered_status(discover_id, "imported")
|
db.record_discover_import(
|
||||||
|
discover_id=discover_id,
|
||||||
|
household_id=hid,
|
||||||
|
mealie_slug=new_slug,
|
||||||
|
imported_by_sub=u["sub"],
|
||||||
|
)
|
||||||
return jsonify({"ok": True, "slug": new_slug})
|
return jsonify({"ok": True, "slug": new_slug})
|
||||||
|
|
||||||
@app.post("/api/discover/reject/<int:discover_id>")
|
@app.post("/api/discover/reject/<int:discover_id>")
|
||||||
|
|
|
||||||
|
|
@ -211,14 +211,27 @@
|
||||||
const quip = meta && meta.hecate_quip ? meta.hecate_quip : '';
|
const quip = meta && meta.hecate_quip ? meta.hecate_quip : '';
|
||||||
const desc = r.description || (meta && meta.summary) || '';
|
const desc = r.description || (meta && meta.summary) || '';
|
||||||
const imgUrl = r.image_url || '';
|
const imgUrl = r.image_url || '';
|
||||||
const klass = 'dcard ' + (r.status === 'imported' ? 'imported' :
|
// klass: dim imported (already in this group's library) and rejected.
|
||||||
|
// imported_in_my_group is set server-side via the cauldron_discover_imports
|
||||||
|
// join — covers both "I imported it" and "the other household imported it";
|
||||||
|
// since Mealie's group-share read makes the recipe visible to both, hiding
|
||||||
|
// the import button is correct in either case.
|
||||||
|
const klass = 'dcard ' + (r.imported_in_my_group ? 'imported' :
|
||||||
r.status === 'rejected' ? 'rejected' : '');
|
r.status === 'rejected' ? 'rejected' : '');
|
||||||
const imgHtml = imgUrl
|
const imgHtml = imgUrl
|
||||||
? `<div class="img" style="background-image:url('${_esc(imgUrl)}')"></div>`
|
? `<div class="img" style="background-image:url('${_esc(imgUrl)}')"></div>`
|
||||||
: `<div class="img placeholder">🍴</div>`;
|
: `<div class="img placeholder">🍴</div>`;
|
||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
if(r.status === 'imported'){
|
if(r.imported_in_my_group){
|
||||||
actionsHtml = '<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">✓ imported</span>';
|
const slug = r.mealie_slug || '';
|
||||||
|
let badge;
|
||||||
|
if(r.imported_by_my_household){
|
||||||
|
badge = '✓ in your library' + (slug ? ' as <code>' + _esc(slug) + '</code>' : '');
|
||||||
|
} else {
|
||||||
|
const who = r.imported_by_household_name || 'another household';
|
||||||
|
badge = '✓ shared from ' + _esc(who) + (slug ? ' as <code>' + _esc(slug) + '</code>' : '');
|
||||||
|
}
|
||||||
|
actionsHtml = '<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">' + badge + '</span>';
|
||||||
} else if(r.status === 'rejected'){
|
} else if(r.status === 'rejected'){
|
||||||
actionsHtml = '<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">✗ rejected</span>';
|
actionsHtml = '<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">✗ rejected</span>';
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -275,12 +288,15 @@
|
||||||
const r = await fetch('/api/discover/import/' + id, { method:'POST' });
|
const r = await fetch('/api/discover/import/' + id, { method:'POST' });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if(!r.ok) throw new Error(d.error || r.status);
|
if(!r.ok) throw new Error(d.error || r.status);
|
||||||
// Mark card as imported in-place
|
// Mark card as imported-by-this-household in-place. The badge wording
|
||||||
|
// matches what _renderCard would emit on next refresh.
|
||||||
const card = btn.closest('.dcard');
|
const card = btn.closest('.dcard');
|
||||||
if(card){
|
if(card){
|
||||||
card.classList.add('imported');
|
card.classList.add('imported');
|
||||||
|
const slugFrag = d.slug ? ' as <code>' + _esc(d.slug) + '</code>' : '';
|
||||||
|
const note = d.already_imported ? '✓ already in your library' : '✓ in your library';
|
||||||
card.querySelector('.actions').innerHTML =
|
card.querySelector('.actions').innerHTML =
|
||||||
'<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">✓ imported as <code>' + _esc(d.slug) + '</code></span>';
|
'<span class="muted" style="flex:1; text-align:center; font-family:var(--mono); font-size:11px;">' + note + slugFrag + '</span>';
|
||||||
}
|
}
|
||||||
} catch(e){
|
} catch(e){
|
||||||
btn.disabled = false; btn.textContent = '🍳 import';
|
btn.disabled = false; btn.textContent = '🍳 import';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue