// screens-admin.jsx — Users, Tokens, Containers, Cluster (graph), Settings
function _normalizeNode(n, x, y) {
return {
id: n.id || n.hostname || n.name || 'node',
role: n.role || 'worker',
status: n.status || (n.online ? 'online' : 'offline'),
ip: n.ip || n.ip_address || '—',
version: n.version || '—',
uptime: n.uptime || '—',
cpu: n.cpu || n.cpu_percent || 0,
mem: n.mem || n.memory_used || n.memory_used_gb || 0,
memTotal: n.memTotal || n.mem_total || n.memory_total || n.memory_total_gb || 0,
gpus: n.gpus || (n.gpu_count ? Array(n.gpu_count).fill('GPU') : []),
devices: n.devices || n.capture_devices || [],
x, y,
};
}
function Users() {
const { USERS } = window.ZAMPP_DATA;
const [tab, setTab] = React.useState("users");
return (
Users & Groups
Export
Invite user
setTab("users")}>Users · {USERS.length}
setTab("groups")}>Groups
setTab("policies")}>Policies
User
Role
Groups
Last active
{USERS.length === 0 && (
No users found
)}
{USERS.map(u => (
{u.role}
{(u.groups || []).map(g => {g} )}
{u.lastSeen}
))}
);
}
function Tokens() {
const [burned, setBurned] = React.useState(14340);
const [rate, setRate] = React.useState(2.4);
const [showCalc, setShowCalc] = React.useState(false);
React.useEffect(() => {
const i = setInterval(() => {
setBurned(b => b + Math.floor(Math.random() * 8) + 1);
setRate(r => Math.max(0.8, Math.min(8, r + (Math.random() - 0.5) * 0.4)));
}, 800);
return () => clearInterval(i);
}, []);
const burnSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 100 + i * 8 + Math.sin(i * 0.7) * 12), []);
const competitorSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 200 + i * 22 + Math.cos(i * 0.5) * 30), []);
const yourCostSpark = React.useMemo(() => Array.from({ length: 40 }, () => 1), []);
const [events, setEvents] = React.useState([
{ t: "21:14:02", action: "preview thumbnail generated", cost: 4 },
{ t: "21:14:01", action: "user clicked play", cost: 12 },
{ t: "21:13:58", action: "API health check", cost: 8 },
{ t: "21:13:54", action: "asset metadata read", cost: 2 },
{ t: "21:13:51", action: "session token refreshed", cost: 18 },
{ t: "21:13:47", action: "scrubbed timeline 1 frame", cost: 6 },
{ t: "21:13:42", action: "took a deep breath near the API", cost: 24 },
]);
React.useEffect(() => {
const actions = [
"preview thumbnail generated", "user clicked play", "API health check",
"scrubbed timeline 1 frame", "asset metadata read", "session token refreshed",
"checked job queue", "rendered a tooltip", "loaded sidebar icon",
"blinked", "made eye contact with the cluster", "opened a modal (twice)",
"asset list pagination request", "thought about a comment", "moved cursor near 'Save'",
];
const i = setInterval(() => {
const now = new Date();
const t = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
const a = actions[Math.floor(Math.random() * actions.length)];
const c = Math.floor(Math.random() * 28) + 1;
setEvents(ev => [{ t, action: a, cost: c }, ...ev].slice(0, 12));
}, 1600);
return () => clearInterval(i);
}, []);
const tiers = [
{ name: "Starter", desc: "For \"evaluation only\" — definitely not production", price: "$2,400", per: "/ month", tokens: "100k tokens", popular: false, color: "#6B7280" },
{ name: "Broadcast", desc: "Most teams. Most pain.", price: "$28,000", per: "/ month", tokens: "1.5M tokens", popular: true, color: "#5B7CFA" },
{ name: "Enterprise", desc: "If you have to ask, you can't afford it (but you'll ask anyway)", price: "$call us", per: "and bring lawyers", tokens: "∞ tokens", popular: false, color: "#B57CFA" },
];
return (
Tokens
Token-metered pricing parody · You actually pay $0.00
SATIRE
setShowCalc(!showCalc)}> Cost calculator
TOKENS BURNED THIS SESSION
🔥
{burned.toLocaleString()}
↑ {rate.toFixed(1)}k/sec
burning since you logged in
WHAT YOU ACTUALLY PAY
$0
.00
Dragonflight is self-hosted. The tokens above are imaginary.
Imagine them as a stress test for your sanity.
HOURLY BURN — DRAGONFLIGHT vs. THE OTHER GUYS
i < 20 ? 1 : 1), color: "#2DD4A8" },
]}
/>
Competitor: $1,247/hr and rising
Dragonflight: $0.00/hr forever
LIVE BILLING EVENTS
{events.map((e, i) => (
{e.t}
{e.action}
+{e.cost} tk
))}
PRICING TIERS WE DIDN'T COPY
{tiers.map(t => (
{t.popular &&
MOST PAIN }
{t.name}
{t.desc}
{t.price}
{t.per}
{t.tokens}
Not for sale
))}
{showCalc &&
setShowCalc(false)} />}
Disclaimer: No actual tokens were billed in the making of this page. Wild Dragon's broadcast platform
is self-hosted infrastructure. Any resemblance to per-API-call broadcast token economies, living or dead, is satirical
and protected as commentary. If you came here looking for actual API tokens, that page no longer exists — service
credentials are managed through the cluster's own JWT issuer (see Settings → AMPP Integration ).
);
}
function ChartLine({ series }) {
const w = 600, h = 140;
return (
{series.map((s, si) => {
const max = Math.max(...series.flatMap(x => x.data), 1);
const pts = s.data.map((d, i) => {
const x = (i / (s.data.length - 1)) * w;
const y = h - (d / max) * (h - 10) - 4;
return `${x},${y}`;
}).join(" ");
const area = `0,${h} ${pts} ${w},${h}`;
return (
);
})}
);
}
function CostCalculator({ onClose }) {
const [users, setUsers] = React.useState(12);
const [assets, setAssets] = React.useState(500);
const [clicks, setClicks] = React.useState(2000);
const cost = users * 240 + assets * 8 + clicks * 0.12;
return (
e.stopPropagation()}>
Token Cost Calculator
What it would cost on AMPP-style pricing
You would be paying
${cost.toLocaleString("en-US", { maximumFractionDigits: 0 })}/ month
Your actual Dragonflight cost: $0.00. You're welcome.
);
}
function CalcSlider({ label, value, onChange, min, max, step = 1, unit }) {
return (
);
}
function Containers() {
const [containers, setContainers] = React.useState(null);
function load() {
setContainers(null);
window.ZAMPP_API.fetch('/cluster/containers')
.then(data => setContainers(Array.isArray(data) ? data : (data.containers || [])))
.catch(() => setContainers([]));
}
React.useEffect(() => { load(); }, []);
const running = (containers || []).filter(c => c.state === 'running').length;
return (
Containers
Docker Compose services across the cluster
{containers !== null && containers.length > 0 && (
{running} / {containers.length} running
)}
Refresh
{containers === null && (
Loading…
)}
{containers !== null && containers.length === 0 && (
🐳
No container data available
Container metrics endpoint not yet wired
)}
{containers !== null && containers.length > 0 && (
Container
Image
State
CPU
Memory
Ports
{containers.map(c => (
{c.image}
RUNNING
{c.healthy && healthy }
{(c.cpu || 0).toFixed(1)}%
{c.mem} MB
{c.ports}
Logs
Restart
))}
)}
);
}
function Cluster() {
const rawNodes = window.ZAMPP_DATA.NODES;
const nodesArr = Array.isArray(rawNodes) ? rawNodes : (rawNodes?.nodes || []);
const NODES = React.useMemo(() => {
if (!nodesArr.length) return [];
const primaryRaw = nodesArr.find(n => n.role === 'primary') || nodesArr[0];
const others = nodesArr.filter(n => n !== primaryRaw);
const primary = _normalizeNode(primaryRaw, 0.5, 0.46);
const positioned = others.map((n, i) => {
const angle = others.length <= 1
? Math.PI / 2
: (i / others.length) * 2 * Math.PI - Math.PI / 2;
return _normalizeNode(n, 0.5 + 0.32 * Math.cos(angle), 0.46 + 0.35 * Math.sin(angle));
});
return [primary, ...positioned];
}, []);
const [hovered, setHovered] = React.useState(null);
const [selected, setSelected] = React.useState(NODES[0] || null);
const W = 720, H = 460;
if (!NODES.length) {
return (
No cluster nodes available
);
}
const primary = NODES.find(n => n.role === 'primary') || NODES[0];
const edges = NODES.filter(n => n.id !== primary.id).map(n => ({
from: primary,
to: n,
alive: n.status === 'online',
}));
const sel = selected || NODES[0];
return (
Cluster
{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online
Live
Refresh
Add node
Avg CPU
{Math.round(NODES.reduce((a, n) => a + (n.cpu || 0), 0) / NODES.length)} %
GPUs
{NODES.reduce((a, n) => a + n.gpus.length, 0)}
Avg Memory
{Math.round(NODES.reduce((a, n) => a + (n.mem || 0), 0) / NODES.length)} GB
{edges.map((e, i) => {
const x1 = e.from.x * W, y1 = e.from.y * H;
const x2 = e.to.x * W, y2 = e.to.y * H;
return (
{e.alive && (
)}
);
})}
{NODES.map(n => {
const cx = n.x * W, cy = n.y * H;
const isSelected = sel && sel.id === n.id;
const color = n.status === "online" ? "var(--success)" : "var(--text-4)";
return (
setHovered(n.id)}
onMouseLeave={() => setHovered(null)}
onClick={() => setSelected(n)}>
{n.status === "online" && (
)}
{n.role === "primary" && }
{n.role !== "primary" && {n.role[0].toUpperCase()} }
{n.id}
{n.ip}
);
})}
{sel && (
{sel.id}
{sel.role}
{sel.status}} />
{sel.cpu}%
} />
{sel.memTotal > 0 && (
{sel.mem} / {sel.memTotal} GB
} />
)}
{sel.gpus.length > 0 && (
GPUs ({sel.gpus.length})
{sel.gpus.map((g, i) => (
{g}
))}
)}
{sel.devices && sel.devices.length > 0 && (
Capture devices
{sel.devices.map((d, i) => (
{d}
))}
)}
Logs
Drain
{sel.role !== "primary" && Remove }
)}
);
}
function DetailRow({ k, v, mono }) {
return (
{k}
{v}
);
}
function Settings() {
const [s3, setS3] = React.useState({ s3_endpoint: '', s3_bucket: '', s3_access_key: '', s3_secret_key: '', s3_region: 'us-east-1' });
const [s3Loading, setS3Loading] = React.useState(true);
const [s3Saving, setS3Saving] = React.useState(false);
const [s3Testing, setS3Testing] = React.useState(false);
const [s3Msg, setS3Msg] = React.useState(null);
const [secretExists, setSecretExists] = React.useState(false);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/s3')
.then(data => {
setS3({ s3_endpoint: data.s3_endpoint || '', s3_bucket: data.s3_bucket || '', s3_access_key: data.s3_access_key || '', s3_secret_key: '', s3_region: data.s3_region || 'us-east-1' });
setSecretExists(!!data.s3_secret_key_exists);
setS3Loading(false);
})
.catch(() => setS3Loading(false));
}, []);
const saveS3 = () => {
setS3Saving(true); setS3Msg(null);
window.ZAMPP_API.fetch('/settings/s3', { method: 'PUT', body: JSON.stringify(s3) })
.then(() => { setS3Saving(false); setS3Msg({ ok: true, text: 'Saved and applied.' }); if (s3.s3_secret_key) setSecretExists(true); })
.catch(e => { setS3Saving(false); setS3Msg({ ok: false, text: e.message }); });
};
const testS3 = () => {
setS3Testing(true); setS3Msg(null);
window.ZAMPP_API.fetch('/settings/s3/test', { method: 'POST', body: JSON.stringify(s3) })
.then(r => { setS3Testing(false); setS3Msg({ ok: r.ok !== false, text: r.message || 'Connection OK' }); })
.catch(e => { setS3Testing(false); setS3Msg({ ok: false, text: e.message }); });
};
return (
Settings
System configuration · changes apply without restart
{[
{ id: 'storage', label: 'S3 / Object storage', icon: 'hdd' },
{ id: 'gpu', label: 'GPU / Transcoding', icon: 'gpu' },
{ id: 'sdi', label: 'SDI capture', icon: 'video' },
{ id: 'ampp', label: 'AMPP integration', icon: 'link' },
].map((s, i) => (
{s.label}
))}
);
}
function SField({ label, children }) {
return (
{label}
{children}
);
}
function SettingsCard({ icon, title, sub, tag, children }) {
return (
);
}
Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });