diff --git a/routes/adwright.py b/routes/adwright.py index b0f8c56..7b11056 100644 --- a/routes/adwright.py +++ b/routes/adwright.py @@ -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 diff --git a/static/adwright.js b/static/adwright.js index 404ad90..5b409e9 100644 --- a/static/adwright.js +++ b/static/adwright.js @@ -35,6 +35,7 @@ data: { cycles: null, cycleDetail: {}, + metricsByCycle: {}, // {: [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") {