D11 - merge origin/Micron-Assets: the Micron asset picker, adapted to R2

Integrates Cody Schaefer's 7ef1fcd (written against pre-R2 main) per Nick's
instruction of Aug 20. The catalog lookup arrives whole: read-only /api/assets
backed by server/assets_db.py (one SELECT, env-only MICRON_DB_URL, 503-not-500
when broken, driver errors logged not propagated), the searchable picker with
CSV import and Excel column paste, catalog rows badged and locked to the DB's
casing, manual rows visibly unvouched, and graceful absent/unreachable states.

Three conflicts, resolved as unions of both sides' intent; the adaptations and
their reasons are recorded in docs/waves/decisions-2026-08-20.md:
- renderPackage: Cody's two-column asset table inside T9.1's sectioned
  add('assets', ...) frame, so the CR-006 toggle keeps governing the export.
- bootData: initAssetPicker() joins the R2 loads instead of replacing them.
- The asset card: his picker UI, plus role=status on the source note (C1).
- Six imported alert() calls converted to the creator's idioms: file errors
  through toast(msg,'alert') as the drawings uploader does; the instructional
  and summary messages through the T7.9 kit, which gains the one-button
  wpAlertDialog shape (BL-024's console conversions will want it too).

New probe: assets_check (27) - read-only structurally, unconfigured/broken as
first-class states, no credential echo, search ranking, casing canonicalisation,
import fallback + dedup, kit-not-native summary. One sections_check pin
re-pointed with the reason in code: normaliseAsset now stamps legacy rows
source:'manual' on load, so the CR-016 check compares content, not bytes.

Battery after merge: assets_check 27/27, creator_dialogs_check 20/20,
sections_check ALL PASS, export_check 20/20, helptip_check 13/13,
mobile_check 24/24, icon_check 5/5, color_check 5/5, form_structure_check
50/51 (the one red is BL-022, unchanged, deliberate).

Item: D11 (new scope, new id per the working rules). Out-of-scope note in
completion.md amended - 'no integration code exists' was true when written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:45:32 -07:00
14 changed files with 978 additions and 31 deletions

View File

@@ -26,7 +26,7 @@ from sqlalchemy import select, delete, func
from sqlalchemy.orm import Session
from .db import Base, engine, get_db
from . import models, auth, notify
from . import models, auth, notify, assets_db
# Schema management:
# • Local dev (SQLite) auto-creates tables for a zero-config run.
@@ -3392,6 +3392,29 @@ def list_comments(
return [c.to_dict() for c in rows]
# ── Micron asset catalog (read-only lookup) ────────────────────────────────────
# Backs the asset picker in the work package creator. This is a *lookup*, not a
# resource this app owns: there is no POST, and nothing here ever writes to the
# Micron database. It is deliberately not project-scoped by the app's own access
# rules — the catalog is reference data, and any signed-in user who can build a
# work package needs to be able to name the assets it covers. Authentication is
# still required (the auth_gate middleware covers every /api/ path).
@app.get("/api/assets")
def list_assets(_user: models.User = Depends(auth.get_current_user)):
"""The whole catalog, fetched once when the creator loads. Searching happens
in the browser — there is no per-keystroke endpoint by design."""
if not assets_db.configured():
# Not an error — the suite is designed to run without Micron wired up.
# The picker reads this and switches to manual entry.
return {"configured": False, "assets": [], "detail": assets_db.status()["detail"]}
try:
return {"configured": True, "assets": assets_db.load()}
except assets_db.AssetSourceError as exc:
# 503, not 500: the suite is healthy, its upstream lookup is not. The
# picker degrades to manual entry rather than blocking the package.
raise HTTPException(status_code=503, detail=str(exc))
# ── Local dev convenience: serve the static site from this app ──────────────────
# In production NGINX serves html/ and only proxies /api/ here, so this app never
# receives "/" requests, and the api Docker image doesn't even include html/ — so