// Admin panel — wired to real API
const { useState: aSt, useEffect: aEf, useRef: aRf } = React;
// ───────── Admin Overview ─────────
function AdminOverview() {
const [overview, setOverview] = aSt(null);
const [pulse, setPulse] = aSt({ summary: {}, recent: [] });
aEf(() => {
let stop = false;
(async () => { setOverview(await window.HDR_API.adminOverview().catch(() => null)); })();
const tick = async () => {
if (stop) return;
try { setPulse(await window.HDR_API.adminPulse()); } catch {}
};
tick();
const id = setInterval(tick, 5000);
return () => { stop = true; clearInterval(id); };
}, []);
const u = overview?.users || {};
const us = overview?.usage || {};
const byEp = overview?.byEndpoint || [];
return (
Admin Overview
Platform-wide KPIs and live request pulse. Auto-refreshes every 5s.
Total users>} value={U.num(u.total || 0)} spark={[12,14,16,18,22,26,30]}/>
Requests (30d)>} value={U.num(us.reqs_30d || 0)} spark={[40,55,52,68,72,80,95]}/>
Revenue (30d)>} value={U.rupees(us.revenue_30d || 0, { short: true })} spark={[20,28,32,38,42,48,55]} sparkColor="var(--pos)"/>
Failed (30d)>} value={U.num(us.failed_30d || 0)} spark={[12,11,9,8,7,6,5]} sparkColor="var(--neg)"/>
Live activity
{pulse.summary?.reqs_60min || 0} req · {U.rupees(pulse.summary?.revenue_60min || 0)} · {pulse.summary?.active_users_60min || 0} active (last hour)
{(pulse.recent || []).length === 0 ? (
No recent activity
) : (pulse.recent || []).map(p => (
{p.status === 'success' ? 'OK' : 'FAIL'}
{p.model_alias || '—'}
{p.endpoint}
{p.email || '—'}
{U.rupees(p.cost_paise || 0)}
{p.latency_ms || 0}ms
))}
{Object.entries(overview?.providers || {}).map(([name, ok]) => (
{name}
{ok ? 'configured' : 'missing'}
))}
Status
{overview?.razorpay?.configured ? (overview.razorpay.mode || 'test').toUpperCase() : 'Not configured'}
{byEp.length === 0 ? (
No data yet
) : byEp.map(e => (
))}
);
}
// ───────── Analytics ─────────
function AdminAnalytics() {
const [days, setDays] = aSt(30);
const { data, loading, reload } = useLoad(() => window.HDR_API.adminAnalytics(days), [days]);
const series = (data?.series || []).map(s => ({ label: s.d?.slice(5) || '', v: Number(s.revenue) }));
const totalRev = (data?.series || []).reduce((s, x) => s + Number(x.revenue), 0);
const totalReq = (data?.series || []).reduce((s, x) => s + Number(x.reqs), 0);
const totalFail = (data?.series || []).reduce((s, x) => s + Number(x.failed), 0);
const successRate = totalReq ? (((totalReq - totalFail) / totalReq) * 100).toFixed(1) : '—';
return (
Analytics
Trends, top users, top models, provider performance.
{[7, 30, 90].map(d => (
))}
| User | Requests | Spent |
{(data?.topUsers || []).map(u => (
| {u.email || '—'} |
{u.reqs} |
{U.rupees(u.spent)} |
))}
{(data?.topUsers || []).length === 0 && | No data |
}
| Alias | Requests | Revenue |
{(data?.topModels || []).map(m => (
| {m.model_alias} |
{m.c} |
{U.rupees(m.revenue)} |
))}
{(data?.topModels || []).length === 0 && | No data |
}
| Provider | Total | Success | Failed | Success rate | Avg latency |
{(data?.providerHealth || []).map(p => {
const rate = p.total ? ((p.ok / p.total) * 100).toFixed(1) : '—';
return (
| {p.provider} |
{p.total} |
{p.ok} |
{p.fail} |
= 95 ? 'pos' : 'warn')}>{rate}% |
{Math.round(p.avg_latency || 0)}ms |
);
})}
{(data?.providerHealth || []).length === 0 && | No data |
}
);
}
// ───────── Users ─────────
function AdminUsers() {
const [q, setQ] = aSt('');
const [users, setUsers] = aSt([]);
const [loading, setLoading] = aSt(true);
const toast = useToast();
const timer = aRf(null);
const load = async (qv) => {
setLoading(true);
try { setUsers(await window.HDR_API.adminUsers(qv || '')); }
finally { setLoading(false); }
};
aEf(() => { load(''); }, []);
const onSearch = (v) => {
setQ(v);
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => load(v), 300);
};
const credit = async (u) => {
const inr = parseFloat(prompt(`Credit how much to ${u.email}? (₹)`));
if (!inr || inr <= 0) return;
const note = prompt('Note:', 'Manual top-up') || 'Admin credit';
try {
const r = await window.HDR_API.creditUser(u.id, { amount_inr: inr, note });
if (r.ok) { toast('Credited', 'pos'); load(q); }
else throw new Error(r.error);
} catch (e) { toast('Failed: ' + e.message, 'neg'); }
};
const toggle = async (u) => {
if (!confirm(u.is_active ? 'Suspend this user?' : 'Activate this user?')) return;
await window.HDR_API.toggleUser(u.id);
toast('Updated', 'pos');
load(q);
};
const toggleAdmin = async (u) => {
if (!confirm(u.is_admin ? 'Remove admin role?' : 'Make admin?')) return;
const r = await window.HDR_API.toggleAdmin(u.id);
if (r.error) toast('Failed: ' + r.error, 'neg');
else { toast('Updated', 'pos'); load(q); }
};
return (
| Email | Name | Wallet | Spent | Joined | Status | Actions |
{loading && | Loading… |
}
{!loading && users.length === 0 && | No users |
}
{users.map(u => (
| {u.email} {!!u.is_admin && Admin} |
{u.name || '—'} |
{U.rupees(u.wallet_paise || 0)} |
{U.rupees(u.total_spent_paise || 0)} |
{new Date(u.created_at).toLocaleDateString('en-IN', { day:'2-digit', month:'short', year:'numeric' })} |
{u.is_active ? 'Active' : 'Suspended'} |
|
))}
);
}
// ───────── Models (routing chains) ─────────
function AdminModels() {
const { data: settings, reload } = useLoad(() => window.HDR_API.adminSettings(), []);
const toast = useToast();
if (!settings) return Loading…
;
const types = [
{ key: 'chat_routes', label: 'Chat', type: 'chat', data: settings.chat_routes },
{ key: 'image_routes', label: 'Image', type: 'image', data: settings.image_routes },
{ key: 'tts_routes', label: 'TTS', type: 'tts', data: settings.tts_routes },
{ key: 'stt_routes', label: 'STT', type: 'stt', data: settings.stt_routes },
];
return (
Models
Edit provider chains per alias. Saves instantly, no redeploy.
{types.map(t => )}
);
}
function ModelRoutesCard({ label, type, data, onSaved }) {
const [json, setJson] = aSt(JSON.stringify(data || {}, null, 2));
const toast = useToast();
const save = async () => {
try {
const parsed = JSON.parse(json);
const r = await window.HDR_API.saveRoutes(type, parsed);
if (r.ok) { toast(`${label} routes saved`, 'pos'); onSaved && onSaved(); }
else throw new Error(r.error);
} catch (e) { toast('Invalid JSON: ' + e.message, 'neg'); }
};
return (
{label} routing
{Object.keys(data || {}).length} aliases — edit JSON below
);
}
// ───────── Skills ─────────
function AdminSkills() {
const [open, setOpen] = aSt(false);
const [editing, setEditing] = aSt(null);
const { data: skills, reload } = useLoad(() => window.HDR_API.adminSkills(), []);
const toast = useToast();
const startCreate = () => { setEditing({ slug:'', name:'', description:'', system_prompt:'', category:'', is_active: true, is_global: true }); setOpen(true); };
const startEdit = (s) => { setEditing({ ...s }); setOpen(true); };
const save = async () => {
if (!editing.name || !editing.description || !editing.system_prompt) {
toast('Name, description and system prompt required', 'neg'); return;
}
try {
if (editing.id) await window.HDR_API.updateSkill(editing.id, editing);
else {
const r = await window.HDR_API.createSkill(editing);
if (r.error) throw new Error(r.error);
}
toast('Saved', 'pos'); setOpen(false); reload();
} catch (e) { toast('Failed: ' + e.message, 'neg'); }
};
const removeSkill = async () => {
if (!confirm('Delete this skill?')) return;
await window.HDR_API.deleteSkill(editing.id);
toast('Deleted', 'pos'); setOpen(false); reload();
};
// group by category
const byCat = {};
(skills || []).forEach(s => { (byCat[s.category || 'uncategorized'] ||= []).push(s); });
return (
Skills
Global skills library. Users attach to agents.
{(!skills || skills.length === 0) ? (
No skills yet
Create your first skill — focused expertise areas users can attach.
) : Object.entries(byCat).map(([cat, list]) => (
{cat}
{list.map(s => (
startEdit(s)}>
{s.name}
{s.is_active ? 'Active' : 'Off'}
{s.slug}
{s.description}
))}
))}
setOpen(false)}
title={editing?.id ? `Edit "${editing?.name}"` : 'Create skill'}
size="wide"
foot={<>
{editing?.id && }
>}>
{editing && }
);
}
function SkillEditor({ value, onChange }) {
const upd = (k, v) => onChange({ ...value, [k]: v });
const [busyAi, setBusyAi] = aSt(false);
const toast = useToast();
const aiSuggest = async () => {
if (!value.name) { toast('Enter skill name first', 'neg'); return; }
setBusyAi(true);
try {
const r = await window.HDR_API.aiSuggestSkill(value.name);
if (r.description) onChange({ ...value, description: value.description || r.description, system_prompt: value.system_prompt || r.system_prompt });
toast('AI suggested', 'pos');
} catch (e) { toast('AI suggest failed', 'neg'); }
finally { setBusyAi(false); }
};
return (
Description (AI uses this for relevance routing)
Active
upd('is_active', v)}/>
Available to users
upd('is_global', v)}/>
);
}
// ───────── Tools ─────────
function AdminTools() {
const [open, setOpen] = aSt(false);
const [editing, setEditing] = aSt(null);
const { data: tools, reload } = useLoad(() => window.HDR_API.adminTools(), []);
const toast = useToast();
const startCreate = () => {
setEditing({
slug: '', name: '', description: '', category: '',
method: 'GET', url_template: '',
headers_json: null, body_template: '', timeout_ms: 15000,
params_schema: [{ name: 'query', type: 'string', description: 'Search query', required: true }],
response_path: '',
is_active: true, is_global: true,
});
setOpen(true);
};
const startEdit = (t) => setEditing({ ...t }) || setOpen(true);
const save = async () => {
if (!editing.slug || !editing.name || !editing.description || !editing.url_template) {
toast('Slug, name, description, URL required', 'neg'); return;
}
try {
if (editing.id) await window.HDR_API.updateTool(editing.id, editing);
else { const r = await window.HDR_API.createTool(editing); if (r.error) throw new Error(r.error); }
toast('Saved', 'pos'); setOpen(false); reload();
} catch (e) { toast('Failed: ' + e.message, 'neg'); }
};
const removeTool = async () => {
if (!confirm('Delete this tool?')) return;
await window.HDR_API.deleteTool(editing.id);
toast('Deleted', 'pos'); setOpen(false); reload();
};
const testTool = async () => {
const params = {};
for (const p of editing.params_schema || []) {
const v = prompt(`${p.name} (${p.description})${p.required ? ' *' : ''}:`);
if (v !== null) params[p.name] = v;
}
const r = await window.HDR_API.testTool(editing.id, params);
if (r.ok) alert('✓ Success\n\n' + JSON.stringify(r.data, null, 2).slice(0, 800));
else alert('✗ Failed: ' + (r.error || 'unknown'));
};
return (
Tools
HTTP endpoints AI can call as functions. Define URL templates + auth headers.
{(!tools || tools.length === 0) ? (
No tools yet
Define HTTP endpoints AI can call to fetch live data.
) : (
{tools.map(t => (
startEdit(t)}>
{t.name}
{t.is_active ? 'Active' : 'Off'}
{t.slug}
{t.method}
{t.category && {t.category}}
{t.description}
))}
)}
setOpen(false)}
title={editing?.id ? `Edit "${editing?.name}"` : 'Create tool'}
size="wide"
foot={<>
{editing?.id && }
{editing?.id && }
>}>
{editing && }
);
}
function ToolEditor({ value, onChange }) {
const upd = (k, v) => onChange({ ...value, [k]: v });
const updJsonField = (k, str) => {
try { onChange({ ...value, [k]: JSON.parse(str) }); }
catch { onChange({ ...value, [k]: str }); }
};
return (
Description (AI uses this to decide WHEN to call)
Method
Body template (for POST/PUT)
Params schema (JSON array)
Active
upd('is_active', v)}/>
Global
upd('is_global', v)}/>
);
}
// ───────── Settings ─────────
function AdminSettings() {
const { data: settings, reload } = useLoad(() => window.HDR_API.adminSettings(), []);
const { data: rzp, reload: reloadRzp } = useLoad(() => window.HDR_API.adminRazorpay(), []);
if (!settings || !rzp) return Loading…
;
return (
Settings
Pricing, payments, and platform configuration.
);
}
function PricingCard({ settings, onSaved }) {
const [v, setV] = aSt({
pricing_markup: settings.pricing_markup,
usd_to_paise: settings.usd_to_paise,
signup_bonus_paise: settings.signup_bonus_paise,
min_balance_paise: settings.min_balance_paise,
live_model: settings.live_model || 'gemini-2.5-flash-native-audio-latest',
live_token_multiplier: settings.live_token_multiplier ?? 2.0,
});
const toast = useToast();
const save = async () => {
try {
const r = await window.HDR_API.saveSettings(v);
if (r.ok) { toast('Pricing saved', 'pos'); onSaved && onSaved(); }
else throw new Error(r.error);
} catch (e) { toast('Failed: ' + e.message, 'neg'); }
};
return (
);
}
function RazorpayCard({ rzp, onSaved }) {
const [keyId, setKeyId] = aSt(rzp.key_id || '');
const [secret, setSecret] = aSt('');
const [webhook, setWebhook] = aSt('');
const toast = useToast();
const save = async () => {
const body = { key_id: keyId };
if (secret) body.key_secret = secret;
if (webhook) body.webhook_secret = webhook;
try {
const r = await window.HDR_API.saveRazorpay(body);
if (r.ok) { toast('Razorpay saved', 'pos'); onSaved && onSaved(); setSecret(''); setWebhook(''); }
else throw new Error(r.error);
} catch (e) { toast('Failed: ' + e.message, 'neg'); }
};
const test = async () => {
const r = await window.HDR_API.testRazorpay();
if (r.ok) toast(r.message || 'Razorpay valid', 'pos');
else toast('Test failed: ' + (r.error || ''), 'neg');
};
return (
Razorpay Payments
{rzp.configured ? rzp.mode?.toUpperCase() : 'Not configured'}
Webhook Secret {rzp.has_webhook_secret && set}
setWebhook(e.target.value)} placeholder="For payment events (optional)"/>
Configure in Razorpay dashboard:
{rzp.webhook_url}
);
}
function ProvidersCard({ settings }) {
return (
{Object.entries(settings.providers || {}).map(([name, ok]) => (
{name}
{ok ? 'configured' : 'missing'}
))}
Provider API keys are loaded from .env file. Update them in Hostinger Environment Variables and restart.
);
}