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)
|
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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 => `
|
||||||
|
|||||||
Reference in New Issue
Block a user