feat(bte panel): wire grid to live /api/query/assetDtos (replaces 404'd assetGrid)
Some checks failed
plugin-tests / test (push) Failing after 6s
Some checks failed
plugin-tests / test (push) Failing after 6s
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.
This commit is contained in:
parent
7dcda7669f
commit
d61f9e8d3f
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Upstream version:** v0.51.118
|
**Upstream version:** v0.51.118
|
||||||
**Plugin version:** 0.5.0
|
**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.**
|
> **Auto-generated by `scripts/ast-connection-map.py`. Do not hand-edit.**
|
||||||
> To change a justification, edit the `# CONNECTION:` comment above the
|
> 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:68` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` |
|
||||||
| `routes/adwright.py:69` | `api.register_route` | `api.register_route(` |
|
| `routes/adwright.py:69` | `api.register_route` | `api.register_route(` |
|
||||||
| `routes/adwright.py:71` | `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.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", "GET", _handle_proxy)` |
|
||||||
| `routes/bte_proxy.py:91` | `api.register_route` | `api.register_route("/api/bte/proxy", "POST", _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: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: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)` |
|
| `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 |
|
| File | Line | URL |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `static/bte.js` | 329 | `/api/command/requestPhotoshoot` |
|
| `static/bte.js` | 329 | `/api/command/requestPhotoshoot` |
|
||||||
| `static/bte.js` | 368 | `/api/query/assetGrid` |
|
| `static/bte.js` | 360 | `/api/query/assetDtos` |
|
||||||
| `static/bte.js` | 482 | `/api/command/rateAsset` |
|
| `static/bte.js` | 372 | `/api/assets/` |
|
||||||
| `static/adwright.js` | 484 | `/api/adwright/provision-creds` |
|
| `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/umbrella.js` | 41 | `/api/umbrella` |
|
||||||
| `static/app.js` | 165 | `/api/vault/status` |
|
| `static/app.js` | 165 | `/api/vault/status` |
|
||||||
|
|
||||||
|
|||||||
@ -77,11 +77,12 @@ _BRAND_ID_CACHE: dict = {}
|
|||||||
_ALLOWED_EXACT = frozenset({
|
_ALLOWED_EXACT = frozenset({
|
||||||
"/api/command/requestPhotoshoot",
|
"/api/command/requestPhotoshoot",
|
||||||
"/api/command/rateAsset",
|
"/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",
|
"/api/query/recipeStats",
|
||||||
})
|
})
|
||||||
# Pattern: /api/assets/<uuid-or-id>/(thumb|status). id-segment may not contain '/' or '?'.
|
# Pattern: /api/assets/<uuid-or-id>/(thumb|status|image). image = full bytes via /api/assets/{id}/image.
|
||||||
_ALLOWED_PATTERN = re.compile(r"^/api/assets/[A-Za-z0-9_\-]+/(thumb|status)$")
|
_ALLOWED_PATTERN = re.compile(r"^/api/assets/[A-Za-z0-9_\-]+/(thumb|status|image)$")
|
||||||
|
|
||||||
|
|
||||||
def register(api):
|
def register(api):
|
||||||
|
|||||||
@ -351,44 +351,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid refresh + polling ───────────────────────────────────────────────
|
// ── 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() {
|
function _refreshGrid() {
|
||||||
if (!state.family) return;
|
// assetDtos is brand-scoped, not recipe-scoped. Always fetch.
|
||||||
const filters = {
|
const body = { pageSize: 48, pageNumber: 1, sortBy: "createdAt", sortDescending: true };
|
||||||
brandId: state.brand,
|
_proxyPost("/api/query/assetDtos", body)
|
||||||
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)
|
|
||||||
.then((resp) => {
|
.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) {
|
if (!resp.ok) {
|
||||||
_log(`assetGrid ← ${resp.status}`);
|
_log(`assetDtos ← ${resp.status}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const json = _safeJson(resp.bodyText) || { items: [] };
|
const json = _safeJson(resp.bodyText) || { data: [] };
|
||||||
state.assets = json.items || [];
|
// 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();
|
_renderGrid();
|
||||||
// Stop polling when no rendering asset remains.
|
const stillInFlight = state.assets.some((a) => a.lifecycle === "requested" || a.lifecycle === "generating" || a.lifecycle === "evaluating");
|
||||||
const stillInFlight = state.assets.some((a) => a.lifecycle === "generating" || a.lifecycle === "evaluating");
|
|
||||||
if (!stillInFlight && state.pollTimer) {
|
if (!stillInFlight && state.pollTimer) {
|
||||||
clearInterval(state.pollTimer);
|
clearInterval(state.pollTimer);
|
||||||
state.pollTimer = null;
|
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() {
|
function _startPolling() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user