Compare commits

...

2 Commits

Author SHA1 Message Date
Svrnty
3fa980583d chore(connection-map): regen — adwright.js line shifts after metrics fan-out
Some checks failed
plugin-tests / test (push) Failing after 2s
2026-05-24 17:57:23 -04:00
Svrnty
01825190a3 feat(adwright panel): wire GetCycleMetrics into Overview KPIs
Frontend now drives Impressions / CTR / Spend from real per-variant
metrics (cycle_metric) rather than hardcoded zeros + fake +12% / -0.3%
deltas. Adds state.data.metricsByCycle keyed by cycle id; populated
via _fetchCycleMetrics which hits the panel-update endpoint directly
(no CMO chat dispatch needed — the route calls gRPC live per request).

After every cycles load (refresh-cycles or list-cycles ingest), fans
out one metrics fetch per cycle. _deriveKpis flattens all metrics
arrays, sums impressions/clicks/spend, computes real CTR from
clicks/impressions. Spend bar now reflects $spend / $budget_total
from real values instead of falling back to the $6,000 default floor.

Plugin route _real_payload_for gains a new branch:
adwright_get_cycle_metrics?cycle_id=N → core.get_cycle_metrics_tool(N).

Fake deltas dropped — real deltas need a previous-window snapshot
the backend doesn't supply yet.

Verified live: metricsByCycle.1 populated with backend's [{variant_id:1,
impressions:0, ...}] record. All metric values still zero in this
sandbox because no ads have dispatched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:57:10 -04:00
3 changed files with 46 additions and 9 deletions

View File

@ -56,9 +56,9 @@ _None. Plugin uses only the public API._ ✓
| `static/bte.js` | 360 | `/api/query/assetDtos` | | `static/bte.js` | 360 | `/api/query/assetDtos` |
| `static/bte.js` | 372 | `/api/assets/` | | `static/bte.js` | 372 | `/api/assets/` |
| `static/bte.js` | 481 | `/api/command/rateAsset` | | `static/bte.js` | 481 | `/api/command/rateAsset` |
| `static/adwright.js` | 175 | `/api/profile/switch` | | `static/adwright.js` | 176 | `/api/profile/switch` |
| `static/adwright.js` | 196 | `/api/profile/active` | | `static/adwright.js` | 197 | `/api/profile/active` |
| `static/adwright.js` | 583 | `/api/adwright/provision-creds` | | `static/adwright.js` | 606 | `/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` |

View File

@ -162,6 +162,9 @@ def _real_payload_for(tool: str, qs: dict) -> dict | None:
raw = core.list_recipes_tool(limit=limit) raw = core.list_recipes_tool(limit=limit)
elif tool == "adwright_get_connections_status": elif tool == "adwright_get_connections_status":
raw = core.get_connections_status_tool() raw = core.get_connections_status_tool()
elif tool == "adwright_get_cycle_metrics":
cycle_id = int((qs.get("cycle_id", ["0"])[0] or "0"))
raw = core.get_cycle_metrics_tool(cycle_id=cycle_id)
else: else:
return None return None
return json.loads(raw) if isinstance(raw, str) else raw return json.loads(raw) if isinstance(raw, str) else raw

View File

@ -35,6 +35,7 @@
data: { data: {
cycles: null, cycles: null,
cycleDetail: {}, cycleDetail: {},
metricsByCycle: {}, // {<cycle_id>: [Metric, ...]} — populated via fan-out
segments: null, segments: null,
recipes: null, recipes: null,
connections: null, connections: null,
@ -349,22 +350,44 @@
function _deriveKpis(cycles, recipes) { function _deriveKpis(cycles, recipes) {
const c = cycles || []; const c = cycles || [];
const r = recipes || []; const r = recipes || [];
const spend = c.reduce((s, x) => s + _cycleSpend(x), 0); // Metrics live in cycle_metric (GetCycleMetrics), fanned out per cycle
// into state.data.metricsByCycle. Flatten across all loaded cycles for
// the Overview totals. Stays 0 until ads dispatch + Meta polling fills.
const mby = NS.state.data.metricsByCycle || {};
const allMetrics = c.flatMap((x) => mby[x.id] || []);
const impressions = allMetrics.reduce((s, m) => s + (m.impressions || 0), 0);
const clicks = allMetrics.reduce((s, m) => s + (m.clicks || 0), 0);
const spend = allMetrics.reduce((s, m) => s + (parseFloat(m.spend) || 0), 0);
const budget = c.reduce((s, x) => s + _cycleBudget(x), 0) || 6000; const budget = c.reduce((s, x) => s + _cycleBudget(x), 0) || 6000;
const impressions = c.reduce((s, x) => s + (x.impressions || 0), 0); const ctr = impressions ? (clicks / impressions * 100) : 0;
const ctrs = c.map((x) => x.ctr || 0).filter((v) => v > 0);
const ctr = ctrs.length ? (ctrs.reduce((s, x) => s + x, 0) / ctrs.length) : 0;
return { return {
impressions: impressions ? _abbrev(impressions) : "0", impressions: impressions ? _abbrev(impressions) : "0",
imprDelta: impressions ? "+12%" : "", imprDelta: "", // real delta needs a previous-window snapshot
ctr: ctr ? (ctr.toFixed(2) + "%") : "0%", ctr: ctr ? (ctr.toFixed(2) + "%") : "0%",
ctrDelta: ctr ? "-0.3%" : "", ctrDelta: "", // same — drop fake delta until backend supplies
cycleCount: String(c.length), cycleCount: String(c.length),
recipeCount: String(r.length), recipeCount: String(r.length),
spend: spend, spend: spend,
budget: budget, budget: budget,
}; };
} }
// Fan-out: fetch metrics for one cycle straight from the panel-update
// endpoint (no CMO chat needed — route calls gRPC live per request).
function _fetchCycleMetrics(cycleId) {
if (!cycleId && cycleId !== 0) return;
const url = "/api/adwright/last-panel-update?session_id=" +
encodeURIComponent(_activeSessionId()) +
"&since=0&tool=adwright_get_cycle_metrics&cycle_id=" +
encodeURIComponent(cycleId);
fetch(url)
.then((r) => r.json())
.then((r) => {
if (!r || !r.update || !r.update.payload) return;
NS.state.data.metricsByCycle[cycleId] = r.update.payload.metrics || [];
if (NS.state.activeTab === "overview") _renderTab("overview");
})
.catch(() => { /* best-effort */ });
}
// Cycles — list with click-to-expand ─────────────────────────────────── // Cycles — list with click-to-expand ───────────────────────────────────
function _renderCycles() { function _renderCycles() {
@ -765,6 +788,17 @@
const payload = u.payload || {}; const payload = u.payload || {};
if (tool === "adwright_refresh_cycles" || tool === "adwright_list_cycles") { if (tool === "adwright_refresh_cycles" || tool === "adwright_list_cycles") {
NS.state.data.cycles = payload.cycles || []; NS.state.data.cycles = payload.cycles || [];
// Fan out metrics fetch for each cycle so Overview KPIs (impressions /
// CTR / spend) come from cycle_metric, not from cycle headers (which
// don't carry them).
(payload.cycles || []).forEach((c) => {
if (c && (c.id != null)) _fetchCycleMetrics(c.id);
});
} else if (tool === "adwright_get_cycle_metrics") {
// Direct fetch (no chat) — payload has {metrics:[...]} but no cycle_id;
// the fetch URL carried cycle_id, so this branch is mostly for the rare
// case the polling channel sees a metrics update first. Skip — handled
// by _fetchCycleMetrics promise resolution.
} else if (tool === "adwright_get_cycle") { } else if (tool === "adwright_get_cycle") {
if (payload.id != null) NS.state.data.cycleDetail[payload.id] = payload; if (payload.id != null) NS.state.data.cycleDetail[payload.id] = payload;
} else if (tool === "adwright_list_segments") { } else if (tool === "adwright_list_segments") {