<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>API Cost Dashboard — sfpermits.ai</title>
<script nonce="{{ csp_nonce }}" src="/static/htmx.min.js"></script>
{% include "fragments/head_obsidian.html" %}
<style nonce="{{ csp_nonce }}">
/* ── Alert banner ── */
.alert-banner {
padding: 12px 16px; border-radius: 8px; margin-bottom: 20px;
font-size: var(--text-sm); border: 1px solid;
}
.alert-warn {
background: rgba(251,191,36,0.1); border-color: var(--signal-amber);
color: var(--signal-amber);
}
.alert-critical {
background: rgba(248,113,113,0.15); border-color: var(--signal-red);
color: var(--signal-red);
}
.alert-ok {
background: rgba(52,211,153,0.08); border-color: var(--signal-green);
color: var(--signal-green);
}
/* ── Kill switch panel ── */
.kill-switch-panel {
background: var(--bg-elevated); border: 1px solid rgba(255,255,255,0.06);
border-radius: var(--card-radius); padding: var(--space-6); margin-bottom: var(--space-6);
display: flex; align-items: center; justify-content: space-between;
gap: var(--space-4); flex-wrap: wrap;
}
.kill-switch-status {
display: flex; align-items: center; gap: 10px;
}
.kill-dot {
width: 12px; height: 12px; border-radius: 50%;
display: inline-block;
}
.kill-dot.active { background: var(--signal-red); box-shadow: 0 0 8px var(--signal-red); }
.kill-dot.inactive { background: var(--signal-green); }
.kill-label { font-size: 1rem; font-weight: 600; color: var(--text-primary); }
.kill-sublabel { font-size: var(--text-sm); color: var(--text-secondary); }
.btn {
padding: 8px 18px; border-radius: 6px; font-size: var(--text-sm);
border: 1px solid; cursor: pointer; font-family: var(--font-body);
font-weight: 500; transition: all 0.15s;
}
.btn-danger {
background: rgba(248,113,113,0.12); border-color: var(--signal-red);
color: var(--signal-red);
}
.btn-danger:hover { background: rgba(248,113,113,0.25); }
.btn-success {
background: rgba(52,211,153,0.12); border-color: var(--signal-green);
color: var(--signal-green);
}
.btn-success:hover { background: rgba(52,211,153,0.25); }
.btn-neutral {
background: var(--bg-elevated); border-color: rgba(255,255,255,0.08);
color: var(--text-secondary);
}
.btn-neutral:hover { border-color: var(--text-secondary); color: var(--text-primary); }
/* ── Thresholds ── */
.threshold-badge {
padding: 2px 9px; border-radius: 12px; font-size: 0.75rem; font-weight: 600;
}
.badge-warn { background: rgba(251,191,36,0.15); color: var(--signal-amber); }
.badge-kill { background: rgba(248,113,113,0.15); color: var(--signal-red); }
/* ── Daily chart bars ── */
.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.bar-date { font-size: 0.75rem; color: var(--text-secondary); width: 80px; flex-shrink: 0; font-family: var(--font-mono); }
.bar-track { flex: 1; background: var(--bg-elevated); border-radius: 4px; height: 16px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; background: var(--signal-cyan); transition: width 0.3s; }
.bar-val { font-size: 0.75rem; color: var(--text-secondary); width: 60px; text-align: right; font-family: var(--font-mono); }
/* ── Tables ── */
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th {
text-align: left; padding: 8px 12px; color: var(--text-secondary);
border-bottom: 1px solid rgba(255,255,255,0.06); font-weight: 600;
font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
font-family: var(--font-body);
}
td { padding: 9px 12px; border-bottom: 1px solid rgba(255,255,255,0.04); color: var(--text-primary); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--bg-elevated); }
/* ── Flash message ── */
#flash-msg {
font-size: var(--text-sm); padding: 8px 14px; border-radius: 6px;
display: none;
}
#flash-msg.show { display: inline-block; }
#flash-msg.success { background: rgba(52,211,153,0.15); color: var(--signal-green); }
#flash-msg.error { background: rgba(248,113,113,0.15); color: var(--signal-red); }
</style>
</head>
<body class="obsidian">
{% include 'fragments/nav.html' %}
<main style="padding: var(--space-10) 0 var(--space-16);">
<div class="obs-container">
<div class="glass-card" style="margin-bottom: var(--space-6);">
<h1 style="font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; color: var(--text-primary); margin-bottom: var(--space-2);">
<span style="font-size:0.65rem;background:var(--bg-elevated);color:var(--text-secondary);padding:3px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.08);vertical-align:middle;margin-right:8px; font-family: var(--font-body);">⚙ Admin</span>API Cost Dashboard
</h1>
<p style="color: var(--text-secondary); font-size: var(--text-sm);">Claude API usage, token spend, and kill switch controls.</p>
<!-- Alert banner -->
{% if summary.kill_switch_active %}
<div class="alert-banner alert-critical" style="margin-top: var(--space-4);">
<strong>Kill switch is ACTIVE.</strong>
AI endpoints are blocked. Today's spend: ${{ "%.4f"|format(summary.today_cost) }}
— threshold: ${{ "%.2f"|format(summary.kill_threshold) }}.
</div>
{% elif summary.today_cost >= summary.warn_threshold %}
<div class="alert-banner alert-warn" style="margin-top: var(--space-4);">
<strong>Cost warning.</strong>
Today's spend ${{ "%.4f"|format(summary.today_cost) }} has reached the
warning threshold of ${{ "%.2f"|format(summary.warn_threshold) }}.
</div>
{% else %}
<div class="alert-banner alert-ok" style="margin-top: var(--space-4);">
Spend normal. Today: ${{ "%.4f"|format(summary.today_cost) }}
of ${{ "%.2f"|format(summary.kill_threshold) }} kill threshold.
</div>
{% endif %}
</div>
<!-- Kill switch panel -->
<div class="glass-card" style="margin-bottom: var(--space-6);">
<div class="kill-switch-panel">
<div class="kill-switch-status">
<span class="kill-dot {% if summary.kill_switch_active %}active{% else %}inactive{% endif %}"></span>
<div>
<div class="kill-label">
Kill switch: <strong>{% if summary.kill_switch_active %}ACTIVE{% else %}Inactive{% endif %}</strong>
</div>
<div class="kill-sublabel">
{% if summary.kill_switch_active %}
AI routes (/ask, /analyze, /analyze-plans) are blocked.
{% else %}
AI routes are operational. Auto-activates at ${{ "%.2f"|format(summary.kill_threshold) }}/day.
{% endif %}
</div>
</div>
</div>
<div>
{% if summary.kill_switch_active %}
<form action="/admin/costs/kill-switch" method="post" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="active" value="0">
<button type="submit" class="btn btn-success" id="ks-btn">
Deactivate Kill Switch
</button>
</form>
{% else %}
<form action="/admin/costs/kill-switch" method="post" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="active" value="1">
<button type="submit" class="btn btn-danger" id="ks-btn"
onclick="return confirm('Activate kill switch? This blocks all AI routes.')">
Activate Kill Switch
</button>
</form>
{% endif %}
</div>
</div>
<!-- Thresholds -->
<div style="display: flex; gap: var(--space-6); flex-wrap: wrap; align-items: center; font-size: var(--text-sm);">
<span style="color: var(--text-secondary);">Thresholds:</span>
<div style="display: flex; gap: var(--space-2); align-items: center;">
<span class="threshold-badge badge-warn">WARN</span>
<span style="color: var(--text-primary);">${{ "%.2f"|format(summary.warn_threshold) }}/day</span>
</div>
<div style="display: flex; gap: var(--space-2); align-items: center;">
<span class="threshold-badge badge-kill">KILL</span>
<span style="color: var(--text-primary);">${{ "%.2f"|format(summary.kill_threshold) }}/day (auto)</span>
</div>
<span style="font-size:0.75rem;color:var(--text-secondary);">
Set via COST_WARN_THRESHOLD / COST_KILL_THRESHOLD env vars.
</span>
</div>
</div>
<!-- Stat cards -->
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-4); margin-bottom: var(--space-6);">
<div class="glass-card">
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--space-1); font-family: var(--font-body);">Today's Spend</div>
<div style="font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; {% if summary.today_cost >= summary.kill_threshold %}color: var(--signal-red);{% elif summary.today_cost >= summary.warn_threshold %}color: var(--signal-amber);{% else %}color: var(--signal-green);{% endif %}">
${{ "%.4f"|format(summary.today_cost) }}
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: var(--space-1);">USD</div>
</div>
<div class="glass-card">
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--space-1); font-family: var(--font-body);">7-Day Total</div>
<div style="font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; color: var(--signal-cyan);">
${{ "%.4f"|format(summary.daily_totals | sum(attribute=1) if summary.daily_totals else 0) }}
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: var(--space-1);">last 7 days</div>
</div>
<div class="glass-card">
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--space-1); font-family: var(--font-body);">Top Endpoint Today</div>
{% if summary.top_endpoints %}
<div style="font-family: var(--font-display); font-size: 1rem; font-weight: 700; color: var(--signal-cyan);">{{ summary.top_endpoints[0][0] }}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: var(--space-1);">${{ "%.4f"|format(summary.top_endpoints[0][1]) }} · {{ summary.top_endpoints[0][2] }} calls</div>
{% else %}
<div style="font-family: var(--font-display); font-size: 1rem; color: var(--text-tertiary);">—</div>
{% endif %}
</div>
<div class="glass-card">
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--space-1); font-family: var(--font-body);">Kill Threshold</div>
<div style="font-family: var(--font-display); font-size: 1.2rem; font-weight: 700; color: var(--signal-cyan);">${{ "%.2f"|format(summary.kill_threshold) }}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: var(--space-1);">per day</div>
</div>
</div>
<!-- 7-Day trend -->
<div class="glass-card" style="margin-bottom: var(--space-6);">
<h2 style="font-family: var(--font-display); font-size: var(--text-xl); font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-4);">7-Day Cost Trend</h2>
{% if summary.daily_totals %}
{% set max_cost = summary.daily_totals | map(attribute=1) | max %}
{% for date_str, cost in summary.daily_totals %}
<div class="bar-row">
<span class="bar-date">{{ date_str }}</span>
<div class="bar-track">
<div class="bar-fill" style="width: {{ ((cost / max_cost * 100) if max_cost > 0 else 0) | int }}%"></div>
</div>
<span class="bar-val">${{ "%.4f"|format(cost) }}</span>
</div>
{% endfor %}
{% else %}
<p style="color:var(--text-secondary);font-size:var(--text-sm);">No API calls recorded in the last 7 days.</p>
{% endif %}
</div>
<!-- Today's endpoints -->
{% if summary.top_endpoints %}
<div class="glass-card" style="margin-bottom: var(--space-6);">
<h2 style="font-family: var(--font-display); font-size: var(--text-xl); font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-4);">Today's Usage by Endpoint</h2>
<div style="overflow-x: auto; -webkit-overflow-scrolling: touch;">
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Cost (USD)</th>
<th>Calls</th>
</tr>
</thead>
<tbody>
{% for endpoint, cost, calls in summary.top_endpoints %}
<tr>
<td><code style="font-size:var(--text-sm);font-family:var(--font-mono);color:var(--signal-cyan);">{{ endpoint }}</code></td>
<td style="font-family: var(--font-mono);">${{ "%.6f"|format(cost) }}</td>
<td style="font-family: var(--font-mono);">{{ calls }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Today's top users -->
{% if summary.top_users %}
<div class="glass-card" style="margin-bottom: var(--space-6);">
<h2 style="font-family: var(--font-display); font-size: var(--text-xl); font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-4);">Today's Usage by User</h2>
<div style="overflow-x: auto; -webkit-overflow-scrolling: touch;">
<table>
<thead>
<tr>
<th>User ID</th>
<th>Cost (USD)</th>
</tr>
</thead>
<tbody>
{% for user_id, cost in summary.top_users %}
<tr>
<td style="font-family: var(--font-mono);">{{ user_id }}</td>
<td style="font-family: var(--font-mono);">${{ "%.6f"|format(cost) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if summary.error is defined %}
<div class="alert-banner alert-warn" style="margin-top:var(--space-6);">
<strong>Dashboard error:</strong> {{ summary.error }} — some data may be missing.
</div>
{% endif %}
<!-- Back link -->
<div style="margin-top: var(--space-8);">
<a href="/admin/ops" style="color: var(--text-secondary); font-size: var(--text-sm); text-decoration: none;">
← Back to Admin Ops
</a>
</div>
</div>
</main>
</body>
</html>