diff --git a/services/web-ui/public/screens-admin.jsx b/services/web-ui/public/screens-admin.jsx
new file mode 100644
index 0000000..c7fb1ba
--- /dev/null
+++ b/services/web-ui/public/screens-admin.jsx
@@ -0,0 +1,661 @@
+// screens-admin.jsx — Users, Tokens, Containers, Cluster (graph), Settings
+
+const { USERS, TOKENS_LIST, CONTAINERS, NODES } = window.ZAMPP_DATA;
+
+/* ========== Users & Groups ========== */
+function Users() {
+ const [tab, setTab] = React.useState("users");
+ return (
+
+
+
Users & Groups
+
+
Export
+
Invite user
+
+
+
+ setTab("users")}>Users · {USERS.length}
+ setTab("groups")}>Groups · 4
+ setTab("policies")}>Policies · 7
+
+
+
+
User
+
Role
+
Groups
+
Last active
+
+
+ {USERS.map(u => (
+
+
+
{u.avatar}
+
+
{u.display}
+
@{u.username}
+
+
+
{u.role}
+
+ {u.groups.map(g => {g} )}
+
+
{u.last}
+
+
+ ))}
+
+
+
+ );
+}
+
+/* ========== Tokens (parody) ========== */
+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 (
+
+
+ {label}
+ {value.toLocaleString()}{unit}
+
+
onChange(Number(e.target.value))}
+ style={{ width: "100%", accentColor: "var(--accent)" }}
+ />
+
+ );
+}
+
+/* ========== Containers ========== */
+function Containers() {
+ return (
+
+
+
Containers
+
Docker Compose services across the cluster
+
+
+
+ {CONTAINERS.filter(c => c.state === "running").length} / {CONTAINERS.length} running
+
+
Refresh
+
+
+
+
+
Container
+
Image
+
State
+
CPU
+
Memory
+
Ports
+
+
+ {CONTAINERS.map(c => (
+
+
+
{c.name}
+
up {c.uptime}
+
+
{c.image}
+
+ RUNNING
+ {c.healthy && healthy }
+
+
+
+
+
{c.cpu.toFixed(1)}%
+
+
+
{c.mem} MB
+
{c.ports}
+
+ Logs
+ Restart
+
+
+ ))}
+
+
+
+ );
+}
+
+/* ========== Cluster (topology graph) ========== */
+function Cluster() {
+ const [hovered, setHovered] = React.useState(null);
+ const [selected, setSelected] = React.useState(NODES[0]);
+ const W = 720, H = 460;
+
+ const primary = NODES.find(n => n.role === "primary");
+ const edges = NODES.filter(n => n.id !== primary.id).map(n => ({
+ from: primary,
+ to: n,
+ alive: n.status === "online",
+ }));
+
+ return (
+
+
+
Cluster
+
{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online
+
+
Live
+
Refresh
+
Add node
+
+
+
+
+
Nodes
+
{NODES.length}
+
across 2 regions
+
+
+
Total CPU
+
320 cores
+
14% utilized
+
+
+
GPUs
+
5
+
3 idle · 2 transcoding
+
+
+
Memory
+
243 GB
+
15% utilized
+
+
+
+
+
+
+
Topology
+
+ Graph
+ Map
+ List
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 isHovered = hovered === n.id;
+ const isSelected = selected.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}
+
+ );
+ })}
+
+
+
+
+
+
+ {selected.id}
+ {selected.role}
+
+
+
{selected.status}} />
+
+
+
+
+
+ {selected.cpu}%
+
+ } />
+
+
+ {selected.mem} / {selected.memTotal} GB
+
+ } />
+ {selected.gpus.length > 0 && (
+
+
GPUs ({selected.gpus.length})
+ {selected.gpus.map((g, i) => (
+
+
+ {g}
+
+ ))}
+
+ )}
+ {selected.devices && (
+
+
Capture devices
+ {selected.devices.map((d, i) => (
+
+ {d}
+
+ ))}
+
+ )}
+
+ Logs
+ Drain
+ {selected.role !== "primary" && Remove }
+
+
+
+
+
+
+ );
+}
+
+function DetailRow({ k, v, mono }) {
+ return (
+
+ {k}
+ {v}
+
+ );
+}
+
+/* ========== Settings ========== */
+function Settings() {
+ 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" },
+ { id: "branding", label: "Branding", icon: "image" },
+ { id: "logs", label: "Logs & telemetry", icon: "list" },
+ ].map((s, i) => (
+
+ {s.label}
+
+ ))}
+
+
+
connected}
+ >
+
+
+
+
+
+
+ Show} />
+
+ Save & apply
+ Test connection
+
+
+
3 GPUs available}
+ >
+
+ Enable GPU-accelerated transcoding
+
+
+
+
+
+
+
+
+
+
+ Save GPU settings
+
+
+
setup needed}
+ >
+
+
+
+ Connect
+
+
+
+
+
+
+ );
+}
+
+function SettingsCard({ icon, title, sub, tag, children }) {
+ return (
+
+ );
+}
+
+function Field({ label, value, mono, select, placeholder, right }) {
+ return (
+
+
{label}
+
+ {select ? (
+
+ {value || placeholder}
+
+
+ ) : (
+
+ )}
+ {right}
+
+
+ );
+}
+
+Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });