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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+12
-1
@@ -59,7 +59,16 @@
|
||||
<canvas id="session-chart"></canvas>
|
||||
</div>
|
||||
</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">
|
||||
<h3>Most Expensive Calls</h3>
|
||||
<table id="expensive-table">
|
||||
@@ -69,6 +78,8 @@
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tables-section">
|
||||
<div class="table-container">
|
||||
<h3>Sessions</h3>
|
||||
<table id="sessions-table">
|
||||
|
||||
+14
-1
@@ -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;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<button id="export-session-btn">Export</button>
|
||||
<button id="preflight-btn" class="preflight-btn">Preflight</button>
|
||||
<button id="settings-btn">Settings</button>
|
||||
<a href="/costs" class="header-link-btn" title="Cost Dashboard">Costs</a>
|
||||
</div>
|
||||
<div class="theme-bar">
|
||||
<label for="show-theme-input" class="theme-label">Theme:</label>
|
||||
|
||||
+15
-1
@@ -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 => `
|
||||
<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) {
|
||||
const tbody = document.querySelector('#expensive-table tbody');
|
||||
tbody.innerHTML = data.map(d => `
|
||||
|
||||
Reference in New Issue
Block a user