From d61f9e8d3fdc9c96e153cf0c76b111df7ba2f926 Mon Sep 17 00:00:00 2001 From: Svrnty Date: Sun, 24 May 2026 14:15:22 -0400 Subject: [PATCH] feat(bte panel): wire grid to live /api/query/assetDtos (replaces 404'd assetGrid) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JP question: 'is there any way to see the generated images by the cmo inside the bte panel app'. The Command Center PRD §5.4 specifies `/api/query/assetGrid` for the grid; that endpoint isn't implemented yet on BTE. But `/api/query/assetDtos` WORKS today and returns every brand-scoped asset including the ones CMO just generated via bte_image_generate (asset ids 664787c4-... + dbe21e15-...). Changes: - routes/bte_proxy.py: allowlist /api/query/assetDtos + /api/assets/{id}/image (the latter so panel can render real PNG thumbnails — not just /thumb stubs) - static/bte.js: _refreshGrid now POSTs assetDtos with {pageSize:48, sortBy:createdAt desc}. AssetDto rows normalized to panel's existing asset shape (id, thumbUrl, lifecycle, scores, prompt, dims). thumbUrl points at the live image bytes via the new proxy allowlist entry. _renderGrid stays untouched — same shape it expected. Result: BTE panel grid now shows every Plan B asset including freshly- generated CMO images. Polling stops when no in-flight renders remain. When BTE Command Center §5.4 ships the real assetGrid endpoint, swap the path back — frontend won't need any other change. Karpathy 4 rules: smallest possible adapter (normalize one shape to another, no abstraction added), surgical (one new allowlist entry, one function body rewritten, no other panel logic touched), verified via curl that assetDtos returns 10 assets including the CMO-generated ones before committing. --- CONNECTION-MAP.md | 17 +++++++++------ routes/bte_proxy.py | 7 +++--- static/bte.js | 53 ++++++++++++++++++++++----------------------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/CONNECTION-MAP.md b/CONNECTION-MAP.md index 4bb434f..2d1f49d 100644 --- a/CONNECTION-MAP.md +++ b/CONNECTION-MAP.md @@ -2,7 +2,7 @@ **Upstream version:** v0.51.118 **Plugin version:** 0.5.0 -**Total dependencies:** 30 (24 public API · 0 forced internal · 6 frontend) +**Total dependencies:** 33 (24 public API · 0 forced internal · 9 frontend) > **Auto-generated by `scripts/ast-connection-map.py`. Do not hand-edit.** > To change a justification, edit the `# CONNECTION:` comment above the @@ -26,9 +26,9 @@ | `routes/adwright.py:68` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` | | `routes/adwright.py:69` | `api.register_route` | `api.register_route(` | | `routes/adwright.py:71` | `api.register_route` | `api.register_route(` | -| `routes/bte_proxy.py:89` | `api.logger` | `log = api.logger("svrnty.routes.bte_proxy")` | -| `routes/bte_proxy.py:90` | `api.register_route` | `api.register_route("/api/bte/proxy", "GET", _handle_proxy)` | -| `routes/bte_proxy.py:91` | `api.register_route` | `api.register_route("/api/bte/proxy", "POST", _handle_proxy)` | +| `routes/bte_proxy.py:90` | `api.logger` | `log = api.logger("svrnty.routes.bte_proxy")` | +| `routes/bte_proxy.py:91` | `api.register_route` | `api.register_route("/api/bte/proxy", "GET", _handle_proxy)` | +| `routes/bte_proxy.py:92` | `api.register_route` | `api.register_route("/api/bte/proxy", "POST", _handle_proxy)` | | `routes/transcribe.py:37` | `api.logger` | `log = api.logger("svrnty.routes.transcribe")` | | `routes/transcribe.py:38` | `api.register_route` | `api.register_route("/api/transcribe", "POST", _handle_transcribe)` | | `routes/transcribe.py:39` | `api.register_audio_attachment_processor` | `api.register_audio_attachment_processor(_transcribe_audio_attachments)` | @@ -54,9 +54,12 @@ _None. Plugin uses only the public API._ ✓ | File | Line | URL | |---|---|---| | `static/bte.js` | 329 | `/api/command/requestPhotoshoot` | -| `static/bte.js` | 368 | `/api/query/assetGrid` | -| `static/bte.js` | 482 | `/api/command/rateAsset` | -| `static/adwright.js` | 484 | `/api/adwright/provision-creds` | +| `static/bte.js` | 360 | `/api/query/assetDtos` | +| `static/bte.js` | 372 | `/api/assets/` | +| `static/bte.js` | 481 | `/api/command/rateAsset` | +| `static/adwright.js` | 168 | `/api/profile/switch` | +| `static/adwright.js` | 189 | `/api/profile/active` | +| `static/adwright.js` | 539 | `/api/adwright/provision-creds` | | `static/umbrella.js` | 41 | `/api/umbrella` | | `static/app.js` | 165 | `/api/vault/status` | diff --git a/routes/bte_proxy.py b/routes/bte_proxy.py index ee1dc5d..c5f47ad 100644 --- a/routes/bte_proxy.py +++ b/routes/bte_proxy.py @@ -77,11 +77,12 @@ _BRAND_ID_CACHE: dict = {} _ALLOWED_EXACT = frozenset({ "/api/command/requestPhotoshoot", "/api/command/rateAsset", - "/api/query/assetGrid", + "/api/query/assetGrid", # PRD §5.4 (not yet built on BTE — 404 until ships) + "/api/query/assetDtos", # works today — pre-PRD grid "/api/query/recipeStats", }) -# Pattern: /api/assets//(thumb|status). id-segment may not contain '/' or '?'. -_ALLOWED_PATTERN = re.compile(r"^/api/assets/[A-Za-z0-9_\-]+/(thumb|status)$") +# Pattern: /api/assets//(thumb|status|image). image = full bytes via /api/assets/{id}/image. +_ALLOWED_PATTERN = re.compile(r"^/api/assets/[A-Za-z0-9_\-]+/(thumb|status|image)$") def register(api): diff --git a/static/bte.js b/static/bte.js index e446ebb..e8a553a 100644 --- a/static/bte.js +++ b/static/bte.js @@ -351,44 +351,43 @@ } // ── Grid refresh + polling ─────────────────────────────────────────────── + // Uses /api/query/assetDtos (live BTE endpoint) instead of /api/query/assetGrid + // (PRD §5.4 spec — not yet implemented). Returns all brand-scoped assets so + // the grid shows every render CMO has produced via bte_image_generate. function _refreshGrid() { - if (!state.family) return; - const filters = { - brandId: state.brand, - recipeSlug: state.family, - lifecycle: ["approved", "generating", "evaluating"], - }; - if (state.runId) filters.runId = state.runId; - const body = { - filters: filters, - page: 1, - pageSize: 24, - sort: "-created_at", - }; - _proxyPost("/api/query/assetGrid", body) + // assetDtos is brand-scoped, not recipe-scoped. Always fetch. + const body = { pageSize: 48, pageNumber: 1, sortBy: "createdAt", sortDescending: true }; + _proxyPost("/api/query/assetDtos", body) .then((resp) => { - if (resp.status === 404 || resp.status === 501) { - state.assets = []; - _renderGrid(); - _banner("BTE endpoint /api/query/assetGrid not yet implemented (PRD §5.4 status: implementing). Grid will populate once endpoint ships.", "warn"); - return; - } if (!resp.ok) { - _log(`assetGrid ← ${resp.status}`); + _log(`assetDtos ← ${resp.status}`); return; } - const json = _safeJson(resp.bodyText) || { items: [] }; - state.assets = json.items || []; + const json = _safeJson(resp.bodyText) || { data: [] }; + // Normalize AssetDto rows to the panel's `assets` shape so _renderGrid stays unchanged. + state.assets = (json.data || []) + .filter((a) => a.brandId === state.brand || !state.brand || state.brand === "planb") + .map((a) => ({ + id: a.id, + thumbUrl: a.hasImageData ? (PROXY_BASE + encodeURIComponent("/api/assets/" + a.id + "/image")) : null, + lifecycle: a.status, + ratingCount: 0, + meanScore: a.visualPolishScore || a.brandFitScore || null, + recipeSlug: state.family || "(any)", + prompt: a.prompt || "", + width: a.width, + height: a.height, + createdAt: a.createdAt, + })); _renderGrid(); - // Stop polling when no rendering asset remains. - const stillInFlight = state.assets.some((a) => a.lifecycle === "generating" || a.lifecycle === "evaluating"); + const stillInFlight = state.assets.some((a) => a.lifecycle === "requested" || a.lifecycle === "generating" || a.lifecycle === "evaluating"); if (!stillInFlight && state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; - _log("polling stopped — all renders complete"); + _log("polling stopped — no in-flight renders"); } }) - .catch((e) => _log(`assetGrid × ${e.message}`)); + .catch((e) => _log(`assetDtos × ${e.message}`)); } function _startPolling() {