Add dd0c/route Dashboard UI: React + Vite + Tailwind SPA

- Layout with sidebar navigation (Dashboard, Rules, Keys, Settings)
- Dashboard page: stat cards, cost savings area chart (Recharts), model usage table
- Rules page: routing rules CRUD with modal editor, strategy/complexity/model matching
- Keys page: API key generation, copy-once reveal, revocation, quick-start code snippet
- Settings page: org info, provider config, danger zone
- API client (SWR + fetch wrapper) with full TypeScript types
- dd0c dark theme: indigo primary, cyan accent, dark surfaces
- Vite proxy config for local dev against API on :3000
This commit is contained in:
2026-03-01 02:36:32 +00:00
parent 0fe25b8aa6
commit a486373d93
18 changed files with 882 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import useSWR from 'swr';
import { api, type AnalyticsSummary, type TimeseriesPoint, type ModelBreakdown } from '../lib/api';
import CostChart from '../components/CostChart';
import StatCard from '../components/StatCard';
import ModelTable from '../components/ModelTable';
export default function Dashboard() {
const { data: summary } = useSWR('summary', api.getSummary);
const { data: timeseries } = useSWR('timeseries', () => api.getTimeseries('hour'));
const { data: models } = useSWR('models', api.getModels);
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold">Dashboard</h2>
{/* Stat cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Total Requests"
value={summary?.total_requests?.toLocaleString() ?? '—'}
sub="Last 7 days"
/>
<StatCard
label="Cost Saved"
value={summary ? `$${summary.total_cost_saved.toFixed(2)}` : '—'}
sub={summary ? `${summary.savings_pct.toFixed(1)}% savings` : ''}
accent="success"
/>
<StatCard
label="Avg Latency"
value={summary ? `${summary.avg_latency_ms}ms` : '—'}
sub={summary ? `P99: ${summary.p99_latency_ms}ms` : ''}
/>
<StatCard
label="Routed"
value={summary ? `${((summary.routing_decisions.cheapest / Math.max(summary.total_requests, 1)) * 100).toFixed(0)}%` : '—'}
sub="Requests downgraded"
accent="primary"
/>
</div>
{/* Cost savings chart */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4">
<h3 className="text-sm font-medium text-dd0c-muted mb-4">Cost Savings Over Time</h3>
<CostChart data={timeseries ?? []} />
</div>
{/* Model breakdown */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4">
<h3 className="text-sm font-medium text-dd0c-muted mb-4">Model Usage</h3>
<ModelTable data={models ?? []} />
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import { useState } from 'react';
import useSWR, { mutate } from 'swr';
import { api, type ApiKey } from '../lib/api';
export default function Keys() {
const { data: keys } = useSWR('keys', api.listKeys);
const [newKeyName, setNewKeyName] = useState('');
const [createdKey, setCreatedKey] = useState<string | null>(null);
const create = async () => {
if (!newKeyName.trim()) return;
const result = await api.createKey(newKeyName.trim());
setCreatedKey(result.key);
setNewKeyName('');
mutate('keys');
};
const revoke = async (id: string, name: string) => {
if (!confirm(`Revoke key "${name}"? This cannot be undone.`)) return;
await api.revokeKey(id);
mutate('keys');
};
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold">API Keys</h2>
{/* Create key */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4">
<h3 className="text-sm font-medium text-dd0c-muted mb-3">Create New Key</h3>
<div className="flex gap-2">
<input
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Key name (e.g. Production, Staging)"
className="flex-1 bg-dd0c-bg border border-dd0c-border rounded px-3 py-1.5 text-sm"
onKeyDown={(e) => e.key === 'Enter' && create()}
/>
<button
onClick={create}
className="px-4 py-1.5 bg-dd0c-primary text-white text-sm rounded-md hover:bg-dd0c-primary/80"
>
Generate
</button>
</div>
{createdKey && (
<div className="mt-3 p-3 bg-dd0c-success/10 border border-dd0c-success/30 rounded-md">
<p className="text-xs text-dd0c-success mb-1">Copy this key now it won't be shown again.</p>
<code className="text-sm font-mono text-dd0c-text break-all select-all">{createdKey}</code>
<button
onClick={() => { navigator.clipboard.writeText(createdKey); }}
className="ml-2 text-xs text-dd0c-muted hover:text-dd0c-text"
>
📋 Copy
</button>
</div>
)}
</div>
{/* Keys table */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg overflow-hidden">
<table className="w-full text-sm" role="table">
<thead>
<tr className="text-dd0c-muted text-xs uppercase tracking-wide border-b border-dd0c-border">
<th className="text-left py-2 px-3">Name</th>
<th className="text-left py-2 px-3">Key</th>
<th className="text-left py-2 px-3">Scopes</th>
<th className="text-left py-2 px-3">Last Used</th>
<th className="text-left py-2 px-3">Created</th>
<th className="text-right py-2 px-3">Actions</th>
</tr>
</thead>
<tbody>
{(keys ?? []).map((key) => (
<tr key={key.id} className="border-b border-dd0c-border/50 hover:bg-dd0c-border/20">
<td className="py-2 px-3">{key.name}</td>
<td className="py-2 px-3 font-mono text-dd0c-muted">{key.key_prefix}••••••••</td>
<td className="py-2 px-3">
{key.scopes.map((s) => (
<span key={s} className="px-1.5 py-0.5 rounded text-xs bg-dd0c-border text-dd0c-muted mr-1">
{s}
</span>
))}
</td>
<td className="py-2 px-3 text-dd0c-muted text-xs">
{key.last_used_at ? new Date(key.last_used_at).toLocaleDateString() : 'Never'}
</td>
<td className="py-2 px-3 text-dd0c-muted text-xs">
{new Date(key.created_at).toLocaleDateString()}
</td>
<td className="py-2 px-3 text-right">
<button
onClick={() => revoke(key.id, key.name)}
className="text-dd0c-danger/60 hover:text-dd0c-danger text-xs"
>
Revoke
</button>
</td>
</tr>
))}
{(keys ?? []).length === 0 && (
<tr>
<td colSpan={6} className="py-8 text-center text-dd0c-muted">
No API keys. Generate one to start routing.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Usage instructions */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4">
<h3 className="text-sm font-medium text-dd0c-muted mb-2">Quick Start</h3>
<pre className="text-xs font-mono text-dd0c-text bg-dd0c-bg p-3 rounded overflow-x-auto">
{`# Replace your OpenAI base URL
export OPENAI_BASE_URL=https://route.dd0c.dev/v1
export OPENAI_API_KEY=dd0c_your_key_here
# Your existing code works unchanged
from openai import OpenAI
client = OpenAI() # Uses dd0c proxy automatically`}
</pre>
</div>
</div>
);
}

View File

@@ -0,0 +1,208 @@
import { useState } from 'react';
import useSWR, { mutate } from 'swr';
import { api, type RoutingRule } from '../lib/api';
const STRATEGIES = ['passthrough', 'cheapest', 'quality-first', 'cascading'];
const COMPLEXITIES = ['low', 'medium', 'high'];
export default function Rules() {
const { data: rules } = useSWR('rules', api.listRules);
const [editing, setEditing] = useState<RoutingRule | null>(null);
const [isNew, setIsNew] = useState(false);
const blank: RoutingRule = {
priority: (rules?.length ?? 0) + 1,
name: '',
strategy: 'cheapest',
enabled: true,
};
const save = async () => {
if (!editing) return;
if (isNew) {
await api.createRule(editing);
} else if (editing.id) {
await api.updateRule(editing.id, editing);
}
setEditing(null);
setIsNew(false);
mutate('rules');
};
const remove = async (id: string) => {
if (!confirm('Delete this rule?')) return;
await api.deleteRule(id);
mutate('rules');
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Routing Rules</h2>
<button
onClick={() => { setEditing(blank); setIsNew(true); }}
className="px-3 py-1.5 bg-dd0c-primary text-white text-sm rounded-md hover:bg-dd0c-primary/80"
>
+ Add Rule
</button>
</div>
{/* Rules table */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg overflow-hidden">
<table className="w-full text-sm" role="table">
<thead>
<tr className="text-dd0c-muted text-xs uppercase tracking-wide border-b border-dd0c-border">
<th className="text-left py-2 px-3">Priority</th>
<th className="text-left py-2 px-3">Name</th>
<th className="text-left py-2 px-3">Match</th>
<th className="text-left py-2 px-3">Strategy</th>
<th className="text-left py-2 px-3">Target</th>
<th className="text-center py-2 px-3">Enabled</th>
<th className="text-right py-2 px-3">Actions</th>
</tr>
</thead>
<tbody>
{(rules ?? []).map((rule) => (
<tr key={rule.id} className="border-b border-dd0c-border/50 hover:bg-dd0c-border/20">
<td className="py-2 px-3 font-mono">{rule.priority}</td>
<td className="py-2 px-3">{rule.name}</td>
<td className="py-2 px-3 text-dd0c-muted text-xs font-mono">
{[
rule.match_model && `model=${rule.match_model}`,
rule.match_feature && `feature=${rule.match_feature}`,
rule.match_team && `team=${rule.match_team}`,
rule.match_complexity && `complexity=${rule.match_complexity}`,
].filter(Boolean).join(', ') || '*'}
</td>
<td className="py-2 px-3">
<span className="px-2 py-0.5 rounded text-xs bg-dd0c-primary/20 text-dd0c-primary">
{rule.strategy}
</span>
</td>
<td className="py-2 px-3 font-mono text-dd0c-accent text-xs">
{rule.target_model || '—'}
</td>
<td className="py-2 px-3 text-center">
{rule.enabled ? '✅' : '⏸️'}
</td>
<td className="py-2 px-3 text-right space-x-2">
<button
onClick={() => { setEditing(rule); setIsNew(false); }}
className="text-dd0c-muted hover:text-dd0c-text text-xs"
>
Edit
</button>
<button
onClick={() => rule.id && remove(rule.id)}
className="text-dd0c-danger/60 hover:text-dd0c-danger text-xs"
>
Delete
</button>
</td>
</tr>
))}
{(rules ?? []).length === 0 && (
<tr>
<td colSpan={7} className="py-8 text-center text-dd0c-muted">
No routing rules yet. Add one to start saving.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Edit modal */}
{editing && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" role="dialog" aria-modal="true">
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-6 w-full max-w-lg space-y-4">
<h3 className="text-lg font-semibold">{isNew ? 'New Rule' : 'Edit Rule'}</h3>
<div className="grid grid-cols-2 gap-3">
<label className="block">
<span className="text-xs text-dd0c-muted">Name</span>
<input
value={editing.name}
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
/>
</label>
<label className="block">
<span className="text-xs text-dd0c-muted">Priority</span>
<input
type="number"
value={editing.priority}
onChange={(e) => setEditing({ ...editing, priority: parseInt(e.target.value) || 0 })}
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
/>
</label>
<label className="block">
<span className="text-xs text-dd0c-muted">Match Model</span>
<input
value={editing.match_model ?? ''}
onChange={(e) => setEditing({ ...editing, match_model: e.target.value || undefined })}
placeholder="gpt-4o"
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
/>
</label>
<label className="block">
<span className="text-xs text-dd0c-muted">Match Complexity</span>
<select
value={editing.match_complexity ?? ''}
onChange={(e) => setEditing({ ...editing, match_complexity: e.target.value || undefined })}
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
>
<option value="">Any</option>
{COMPLEXITIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</label>
<label className="block">
<span className="text-xs text-dd0c-muted">Strategy</span>
<select
value={editing.strategy}
onChange={(e) => setEditing({ ...editing, strategy: e.target.value })}
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
>
{STRATEGIES.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
</label>
<label className="block">
<span className="text-xs text-dd0c-muted">Target Model</span>
<input
value={editing.target_model ?? ''}
onChange={(e) => setEditing({ ...editing, target_model: e.target.value || undefined })}
placeholder="gpt-4o-mini"
className="mt-1 w-full bg-dd0c-bg border border-dd0c-border rounded px-2 py-1.5 text-sm"
/>
</label>
</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={editing.enabled}
onChange={(e) => setEditing({ ...editing, enabled: e.target.checked })}
/>
<span className="text-sm">Enabled</span>
</label>
<div className="flex justify-end gap-2 pt-2">
<button
onClick={() => { setEditing(null); setIsNew(false); }}
className="px-3 py-1.5 text-sm text-dd0c-muted hover:text-dd0c-text"
>
Cancel
</button>
<button
onClick={save}
className="px-3 py-1.5 bg-dd0c-primary text-white text-sm rounded-md hover:bg-dd0c-primary/80"
>
{isNew ? 'Create' : 'Save'}
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,73 @@
import useSWR from 'swr';
import { api } from '../lib/api';
export default function Settings() {
const { data: org } = useSWR('org', api.getOrg);
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold">Settings</h2>
{/* Org info */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4 space-y-3">
<h3 className="text-sm font-medium text-dd0c-muted">Organization</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-xs text-dd0c-muted">Name</p>
<p>{org?.name ?? '—'}</p>
</div>
<div>
<p className="text-xs text-dd0c-muted">Slug</p>
<p className="font-mono">{org?.slug ?? '—'}</p>
</div>
<div>
<p className="text-xs text-dd0c-muted">Tier</p>
<p>
<span className="px-2 py-0.5 rounded text-xs bg-dd0c-primary/20 text-dd0c-primary uppercase">
{org?.tier ?? '—'}
</span>
</p>
</div>
<div>
<p className="text-xs text-dd0c-muted">ID</p>
<p className="font-mono text-xs text-dd0c-muted">{org?.id ?? '—'}</p>
</div>
</div>
</div>
{/* Provider configs */}
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4 space-y-3">
<h3 className="text-sm font-medium text-dd0c-muted">Providers</h3>
<p className="text-xs text-dd0c-muted">
Configure your LLM provider API keys. Keys are encrypted at rest.
</p>
<div className="space-y-2">
<ProviderRow name="OpenAI" provider="openai" />
<ProviderRow name="Anthropic" provider="anthropic" />
</div>
</div>
{/* Danger zone */}
<div className="bg-dd0c-surface border border-dd0c-danger/30 rounded-lg p-4 space-y-3">
<h3 className="text-sm font-medium text-dd0c-danger">Danger Zone</h3>
<button className="px-3 py-1.5 border border-dd0c-danger/50 text-dd0c-danger text-sm rounded-md hover:bg-dd0c-danger/10">
Delete Organization
</button>
</div>
</div>
);
}
function ProviderRow({ name, provider }: { name: string; provider: string }) {
return (
<div className="flex items-center justify-between p-3 bg-dd0c-bg rounded-md">
<div>
<p className="text-sm font-medium">{name}</p>
<p className="text-xs text-dd0c-muted">{provider}</p>
</div>
<button className="px-3 py-1 text-xs border border-dd0c-border rounded hover:bg-dd0c-border/50 text-dd0c-muted hover:text-dd0c-text">
Configure
</button>
</div>
);
}