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) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 23:39:40 -06:00
co-authored by Claude Opus 4.6
parent 1b988a0303
commit c087c0362a
6 changed files with 68 additions and 3 deletions
+5
View File
@@ -10354,6 +10354,11 @@ async def get_cost_expensive_calls(period: str = "all", limit: int = 10):
return cost_db.get_expensive_calls(period, limit) 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 --- # --- Caller Screening ---
SCREENING_PROMPT = """You are a friendly, brief phone screener for "Luke at the Roost" radio show. SCREENING_PROMPT = """You are a friendly, brief phone screener for "Luke at the Roost" radio show.
+21
View File
@@ -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]: def get_expensive_calls(period: str = "all", limit: int = 20) -> list[dict]:
conn = get_db() conn = get_db()
start = _period_filter(period) start = _period_filter(period)
+12 -1
View File
@@ -59,7 +59,16 @@
<canvas id="session-chart"></canvas> <canvas id="session-chart"></canvas>
</div> </div>
</section> </section>
<section class="tables-section"> <section class="chart-row">
<div class="table-container">
<h3>TTS Providers</h3>
<table id="tts-table">
<thead>
<tr><th>Provider</th><th>Calls</th><th>Characters</th><th>Cost</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-container"> <div class="table-container">
<h3>Most Expensive Calls</h3> <h3>Most Expensive Calls</h3>
<table id="expensive-table"> <table id="expensive-table">
@@ -69,6 +78,8 @@
<tbody></tbody> <tbody></tbody>
</table> </table>
</div> </div>
</section>
<section class="tables-section">
<div class="table-container"> <div class="table-container">
<h3>Sessions</h3> <h3>Sessions</h3>
<table id="sessions-table"> <table id="sessions-table">
+14 -1
View File
@@ -108,11 +108,24 @@ header button {
transition: all 0.2s; transition: all 0.2s;
} }
header button:hover { header button:hover, .header-link-btn:hover {
background: #3a2e1f; background: #3a2e1f;
border-color: rgba(232, 121, 29, 0.3); 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 { .theme-bar {
display: flex; display: flex;
align-items: center; align-items: center;
+1
View File
@@ -17,6 +17,7 @@
<button id="export-session-btn">Export</button> <button id="export-session-btn">Export</button>
<button id="preflight-btn" class="preflight-btn">Preflight</button> <button id="preflight-btn" class="preflight-btn">Preflight</button>
<button id="settings-btn">Settings</button> <button id="settings-btn">Settings</button>
<a href="/costs" class="header-link-btn" title="Cost Dashboard">Costs</a>
</div> </div>
<div class="theme-bar"> <div class="theme-bar">
<label for="show-theme-input" class="theme-label">Theme:</label> <label for="show-theme-input" class="theme-label">Theme:</label>
+15 -1
View File
@@ -50,19 +50,21 @@ function destroyChart(key) {
async function loadDashboard() { async function loadDashboard() {
const p = currentPeriod; const p = currentPeriod;
try { 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/summary?period=${p}`).then(r => r.json()),
fetch(`/api/costs/timeline?period=${p}&group_by=session`).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/models?period=${p}`).then(r => r.json()),
fetch(`/api/costs/categories?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/sessions?period=${p}`).then(r => r.json()),
fetch(`/api/costs/expensive?period=${p}&limit=10`).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); renderSummary(summary);
renderTimeline(timeline); renderTimeline(timeline);
renderModels(models); renderModels(models);
renderCategories(categories); renderCategories(categories);
renderSessionBars(timeline); renderSessionBars(timeline);
renderTtsTable(tts);
renderExpensiveTable(expensive); renderExpensiveTable(expensive);
renderSessionsTable(sessions); renderSessionsTable(sessions);
} catch (e) { } catch (e) {
@@ -201,6 +203,18 @@ function renderSessionBars(data) {
}); });
} }
function renderTtsTable(data) {
const tbody = document.querySelector('#tts-table tbody');
tbody.innerHTML = data.map(d => `
<tr>
<td>${d.provider}</td>
<td>${d.calls.toLocaleString()}</td>
<td>${d.chars.toLocaleString()}</td>
<td>${formatCost(d.cost)}</td>
</tr>
`).join('');
}
function renderExpensiveTable(data) { function renderExpensiveTable(data) {
const tbody = document.querySelector('#expensive-table tbody'); const tbody = document.querySelector('#expensive-table tbody');
tbody.innerHTML = data.map(d => ` tbody.innerHTML = data.map(d => `