From c087c0362ae343fd39a85a3ead88b91de4ec9809 Mon Sep 17 00:00:00 2001 From: tcpsyn Date: Thu, 9 Apr 2026 23:39:40 -0600 Subject: [PATCH] Cost dashboard: TTS providers endpoint, header link, polish Adds /api/costs/tts endpoint backed by get_tts_providers() in cost_db, surfaces a Costs link in the control panel header, and small polish to the costs page HTML/JS. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 5 +++++ backend/services/cost_db.py | 21 +++++++++++++++++++++ frontend/costs.html | 13 ++++++++++++- frontend/css/style.css | 15 ++++++++++++++- frontend/index.html | 1 + frontend/js/costs.js | 16 +++++++++++++++- 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index 4dd17aa..df60b4d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10354,6 +10354,11 @@ async def get_cost_expensive_calls(period: str = "all", limit: int = 10): return cost_db.get_expensive_calls(period, limit) +@app.get("/api/costs/tts") +async def get_cost_tts_providers(period: str = "all"): + return cost_db.get_tts_providers(period) + + # --- Caller Screening --- SCREENING_PROMPT = """You are a friendly, brief phone screener for "Luke at the Roost" radio show. diff --git a/backend/services/cost_db.py b/backend/services/cost_db.py index 5692d23..37f69ab 100644 --- a/backend/services/cost_db.py +++ b/backend/services/cost_db.py @@ -500,6 +500,27 @@ def get_session_detail(session_id: str) -> dict | None: } +def get_tts_providers(period: str = "all") -> list[dict]: + conn = get_db() + start = _period_filter(period) + if start: + rows = conn.execute( + "SELECT t.provider, COUNT(*) as calls, COALESCE(SUM(t.cost), 0) as cost, " + "COALESCE(SUM(t.char_count), 0) as chars " + "FROM tts_calls t JOIN sessions s ON t.session_id = s.id " + "WHERE s.started_at >= ? GROUP BY t.provider ORDER BY cost DESC", + [start] + ).fetchall() + else: + rows = conn.execute( + "SELECT provider, COUNT(*) as calls, COALESCE(SUM(cost), 0) as cost, " + "COALESCE(SUM(char_count), 0) as chars " + "FROM tts_calls GROUP BY provider ORDER BY cost DESC" + ).fetchall() + return [{"provider": r["provider"], "calls": r["calls"], + "cost": round(r["cost"], 4), "chars": r["chars"]} for r in rows] + + def get_expensive_calls(period: str = "all", limit: int = 20) -> list[dict]: conn = get_db() start = _period_filter(period) diff --git a/frontend/costs.html b/frontend/costs.html index 5cf6466..f28f03b 100644 --- a/frontend/costs.html +++ b/frontend/costs.html @@ -59,7 +59,16 @@ -
+
+
+

TTS Providers

+ + + + + +
ProviderCallsCharactersCost
+

Most Expensive Calls

@@ -69,6 +78,8 @@
+
+

Sessions

diff --git a/frontend/css/style.css b/frontend/css/style.css index efe8b61..4c1d500 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -108,11 +108,24 @@ header button { transition: all 0.2s; } -header button:hover { +header button:hover, .header-link-btn:hover { background: #3a2e1f; border-color: rgba(232, 121, 29, 0.3); } +.header-link-btn { + background: var(--bg-light); + color: var(--text); + border: 1px solid rgba(232, 121, 29, 0.15); + padding: 8px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + font-size: inherit; + font-family: inherit; +} + .theme-bar { display: flex; align-items: center; diff --git a/frontend/index.html b/frontend/index.html index 071f549..7fecafe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -17,6 +17,7 @@ + Costs
diff --git a/frontend/js/costs.js b/frontend/js/costs.js index cef9e78..1b6f71b 100644 --- a/frontend/js/costs.js +++ b/frontend/js/costs.js @@ -50,19 +50,21 @@ function destroyChart(key) { async function loadDashboard() { const p = currentPeriod; try { - const [summary, timeline, models, categories, sessions, expensive] = await Promise.all([ + const [summary, timeline, models, categories, sessions, expensive, tts] = await Promise.all([ fetch(`/api/costs/summary?period=${p}`).then(r => r.json()), fetch(`/api/costs/timeline?period=${p}&group_by=session`).then(r => r.json()), fetch(`/api/costs/models?period=${p}`).then(r => r.json()), fetch(`/api/costs/categories?period=${p}`).then(r => r.json()), fetch(`/api/costs/sessions?period=${p}`).then(r => r.json()), fetch(`/api/costs/expensive?period=${p}&limit=10`).then(r => r.json()), + fetch(`/api/costs/tts?period=${p}`).then(r => r.json()), ]); renderSummary(summary); renderTimeline(timeline); renderModels(models); renderCategories(categories); renderSessionBars(timeline); + renderTtsTable(tts); renderExpensiveTable(expensive); renderSessionsTable(sessions); } catch (e) { @@ -201,6 +203,18 @@ function renderSessionBars(data) { }); } +function renderTtsTable(data) { + const tbody = document.querySelector('#tts-table tbody'); + tbody.innerHTML = data.map(d => ` +
+ + + + + + `).join(''); +} + function renderExpensiveTable(data) { const tbody = document.querySelector('#expensive-table tbody'); tbody.innerHTML = data.map(d => `
${d.provider}${d.calls.toLocaleString()}${d.chars.toLocaleString()}${formatCost(d.cost)}