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

+
+ + +
+
+
+ + + +
+
+
+
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 + +
+
+
+
+
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}
+ +
+ ))} +
+
+
+ + {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 +
+ +
+
+
+
+
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}
+
+ + +
+
+ ))} +
+
+
+ ); +} + +/* ========== 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
+ + +
+
+
+
+
Nodes
+
{NODES.length}
+
across 2 regions
+
+
+
Total CPU
+
320 cores
+
14% utilized
+
+
+
GPUs
+
5
+
3 idle · 2 transcoding
+
+
+
Memory
+
243 GB
+
15% utilized
+
+
+ +
+
+
+ Topology +
+ + + +
+
+ + + + + + + + + + + + {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} +
+ ))} +
+ )} +
+ + + {selected.role !== "primary" && } +
+
+
+
+
+
+ ); +} + +function DetailRow({ k, v, mono }) { + return ( +
+ {k} + {v} +
+ ); +} + +/* ========== Settings ========== */ +function Settings() { + return ( +
+
+

Settings

+ System configuration · changes apply without restart +
+
+
+ +
+ connected} + > + +
+ + +
+ + Show} /> +
+ + +
+
+ 3 GPUs available} + > + +
+ + +
+
+ + +
+
+ +
+
+ setup needed} + > + + +
+ +
+
+
+
+
+
+ ); +} + +function SettingsCard({ icon, title, sub, tag, children }) { + return ( +
+
+
+
+
{title}
+
{sub}
+
+ {tag} +
+
{children}
+
+ ); +} + +function Field({ label, value, mono, select, placeholder, right }) { + return ( +
+ +
+ {select ? ( +
+ {value || placeholder} + +
+ ) : ( + + )} + {right} +
+
+ ); +} + +Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });