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>
This commit is contained in:
parent
cfd064aad5
commit
01825190a3
@ -162,6 +162,9 @@ def _real_payload_for(tool: str, qs: dict) -> dict | None:
|
||||
raw = core.list_recipes_tool(limit=limit)
|
||||
elif tool == "adwright_get_connections_status":
|
||||
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:
|
||||
return None
|
||||
return json.loads(raw) if isinstance(raw, str) else raw
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
data: {
|
||||
cycles: null,
|
||||
cycleDetail: {},
|
||||
metricsByCycle: {}, // {<cycle_id>: [Metric, ...]} — populated via fan-out
|
||||
segments: null,
|
||||
recipes: null,
|
||||
connections: null,
|
||||
@ -349,22 +350,44 @@
|
||||
function _deriveKpis(cycles, recipes) {
|
||||
const c = cycles || [];
|
||||
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 impressions = c.reduce((s, x) => s + (x.impressions || 0), 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;
|
||||
const ctr = impressions ? (clicks / impressions * 100) : 0;
|
||||
return {
|
||||
impressions: impressions ? _abbrev(impressions) : "0",
|
||||
imprDelta: impressions ? "+12%" : "",
|
||||
imprDelta: "", // real delta needs a previous-window snapshot
|
||||
ctr: ctr ? (ctr.toFixed(2) + "%") : "0%",
|
||||
ctrDelta: ctr ? "-0.3%" : "",
|
||||
ctrDelta: "", // same — drop fake delta until backend supplies
|
||||
cycleCount: String(c.length),
|
||||
recipeCount: String(r.length),
|
||||
spend: spend,
|
||||
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 ───────────────────────────────────
|
||||
function _renderCycles() {
|
||||
@ -765,6 +788,17 @@
|
||||
const payload = u.payload || {};
|
||||
if (tool === "adwright_refresh_cycles" || tool === "adwright_list_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") {
|
||||
if (payload.id != null) NS.state.data.cycleDetail[payload.id] = payload;
|
||||
} else if (tool === "adwright_list_segments") {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user