Add app
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
.git
|
||||
*.local
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
*.local
|
||||
@@ -0,0 +1,27 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Two-stage build: compile the Svelte SPA with Vite, then serve the static
|
||||
# bundle from a tiny nginx. The main serving-stack nginx reverse-proxies
|
||||
# /kiosk/ to this container (see ../docker-compose.yml + ../http/default.conf),
|
||||
# so this image only needs to serve the built assets at its own root.
|
||||
|
||||
# ---- build stage ----
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Install deps first for layer caching. package-lock.json is committed, so use
|
||||
# `npm ci` for a reproducible, lockfile-exact install.
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build # -> /app/dist (base path /kiosk/)
|
||||
|
||||
# ---- serve stage ----
|
||||
FROM nginx:stable-alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1
|
||||
@@ -0,0 +1,54 @@
|
||||
# Fleet Kiosk App
|
||||
|
||||
A cyber sci-fi **spaceship monitoring & control terminal** — the page the
|
||||
diskless kiosk (weston + Chromium) loads at boot. Replaces the old single static
|
||||
`serving/http/kiosk/index.html` test page.
|
||||
|
||||
Built with **Svelte 5** + **Tailwind CSS v4** + **Vite**, served as a static
|
||||
bundle from its own nginx, and reverse-proxied at `/kiosk/` by the main serving
|
||||
nginx (`../http/default.conf`). Fully self-contained — no CDNs or web fonts — so
|
||||
it renders on the air-gapped boot LAN (the stack's dnsmasq sinkholes external
|
||||
call-home).
|
||||
|
||||
## Pages
|
||||
|
||||
Top navbar with five views, each with dummy telemetry:
|
||||
|
||||
- **BRIDGE** — overview tiles, systems status, subsystem load, event log
|
||||
- **REACTOR** — core output/temp/containment, power distribution, fuel cells
|
||||
- **NAVIGATION** — heading/velocity, tactical radar plot, flight plan, FTL drive
|
||||
- **LIFE SUPPORT** — atmosphere mix, per-deck environment, recyclers
|
||||
- **COMMS** — channels, signal bars, message traffic
|
||||
|
||||
Routing is hash-based (`#/power`, `#/nav`, …) so deep links stay client-side and
|
||||
the proxy needs no SPA-fallback rewrite. A shared 1 Hz clock and small
|
||||
random-walk "jitter" helpers make the static data feel live.
|
||||
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:5173/kiosk/
|
||||
```
|
||||
|
||||
## Build the container
|
||||
|
||||
It builds as part of the serving stack:
|
||||
|
||||
```bash
|
||||
cd .. # serving/
|
||||
docker compose build kiosk
|
||||
docker compose up -d
|
||||
# main nginx serves it at http://${SERVER_IP}/kiosk/ (kiosk_url default)
|
||||
```
|
||||
|
||||
The container publishes on `127.0.0.1:${KIOSK_APP_PORT:-8088}`; the host-network
|
||||
main nginx proxies `/kiosk/` to it. To point the diskless kiosk here, keep the
|
||||
Packer `kiosk_url=http://boot.fleet.lab/kiosk/` default (resolved to the serving
|
||||
host by the stack's dnsmasq).
|
||||
|
||||
## Replacing the dummy data
|
||||
|
||||
All fictional telemetry lives in [src/lib/data.js](src/lib/data.js). A future
|
||||
runtime agent could swap these constants for a `fetch()` to a fleet API; the
|
||||
components only read from that module.
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>FLEET // BRIDGE TERMINAL</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
# Internal nginx for the kiosk SPA container. The main serving nginx proxies
|
||||
# /kiosk/ here with the prefix stripped (proxy_pass .../;), so requests arrive
|
||||
# at this server rooted at "/". try_files gives the SPA fallback; routing is
|
||||
# hash-based anyway, so this fallback only matters for a hard refresh on "/".
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Don't cache the entry document (so a new build is picked up immediately);
|
||||
# the hashed assets under /assets/ are immutable and cached hard.
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Generated
+2075
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fleet-kiosk-app",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Cyber sci-fi spaceship monitoring/control terminal (Svelte + Tailwind) shown by the diskless kiosk.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"svelte": "^5.19.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import Navbar from './lib/Navbar.svelte'
|
||||
import StatusBar from './lib/StatusBar.svelte'
|
||||
import { nav } from './lib/router.svelte.js'
|
||||
import { clock, hhmmss } from './lib/clock.svelte.js'
|
||||
|
||||
import Dashboard from './pages/Dashboard.svelte'
|
||||
import Power from './pages/Power.svelte'
|
||||
import Navigation from './pages/Navigation.svelte'
|
||||
import LifeSupport from './pages/LifeSupport.svelte'
|
||||
import Comms from './pages/Comms.svelte'
|
||||
|
||||
const pages = {
|
||||
dashboard: Dashboard,
|
||||
power: Power,
|
||||
nav: Navigation,
|
||||
life: LifeSupport,
|
||||
comms: Comms,
|
||||
}
|
||||
|
||||
let Current = $derived(pages[nav.current] ?? Dashboard)
|
||||
|
||||
const ticker =
|
||||
'SYS // all primary buses nominal ◦ FTL capacitor charging — 61% ◦ ' +
|
||||
'sensor sweep grid 7-K complete ◦ comms relay 2 degraded — rerouting ◦ ' +
|
||||
'reactor output 84.2% rated ◦ next waypoint: Tarsis Station, ETA 04:12:38 ◦ '
|
||||
</script>
|
||||
|
||||
<div class="relative z-10 flex h-full flex-col">
|
||||
<Navbar />
|
||||
<StatusBar />
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<Current />
|
||||
</main>
|
||||
|
||||
<footer class="flex items-center gap-3 overflow-hidden border-t border-haze/20 bg-void/70 px-4 py-1.5">
|
||||
<span class="shrink-0 text-[0.6rem] uppercase tracking-[0.2em] text-flare glow-sm">● LIVE</span>
|
||||
<span class="shrink-0 text-[0.6rem] tabular-nums text-haze">{hhmmss(clock.now)}</span>
|
||||
<div class="relative flex-1 overflow-hidden">
|
||||
<span class="ticker text-[0.65rem] uppercase tracking-[0.18em] text-haze">{ticker}{ticker}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Cyber sci-fi palette. Defining these as @theme tokens makes them available
|
||||
as Tailwind utilities (bg-hull, text-term, border-haze, ...). Font is the
|
||||
stock DejaVu Sans Mono shipped in the kiosk image — no web fonts (offline). */
|
||||
@theme {
|
||||
--color-void: #040a0d;
|
||||
--color-hull: #081419;
|
||||
--color-hull2: #0c1f27;
|
||||
--color-term: #00ffd0;
|
||||
--color-haze: #4a8c84;
|
||||
--color-flare: #ff2e88;
|
||||
--color-warn: #ffb000;
|
||||
--color-ok: #2dff8a;
|
||||
--color-alert: #ff3b3b;
|
||||
|
||||
--font-mono: "DejaVu Sans Mono", ui-monospace, "SFMono-Regular", "Courier New", monospace;
|
||||
}
|
||||
|
||||
:root { color-scheme: dark; }
|
||||
|
||||
html, body, #app { height: 100%; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background-color: var(--color-void);
|
||||
color: var(--color-term);
|
||||
font-family: var(--font-mono);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Faint engineering-grid backdrop + center glow + vignette. */
|
||||
#app::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 255, 208, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 255, 208, 0.035) 1px, transparent 1px),
|
||||
radial-gradient(circle at 50% 22%, rgba(0, 90, 100, 0.35), transparent 65%);
|
||||
background-size: 42px 42px, 42px 42px, 100% 100%;
|
||||
}
|
||||
|
||||
/* CRT scanline overlay sitting above content. */
|
||||
#app::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0px,
|
||||
rgba(0, 0, 0, 0) 2px,
|
||||
rgba(0, 0, 0, 0.16) 3px
|
||||
);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.glow { text-shadow: 0 0 10px currentColor; }
|
||||
.glow-sm { text-shadow: 0 0 6px currentColor; }
|
||||
|
||||
/* Cut-corner panel edge — the recurring "console module" silhouette. */
|
||||
.cut {
|
||||
clip-path: polygon(
|
||||
0 0,
|
||||
calc(100% - 14px) 0,
|
||||
100% 14px,
|
||||
100% 100%,
|
||||
14px 100%,
|
||||
0 calc(100% - 14px)
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
|
||||
.blink { animation: blink 1.4s steps(2, jump-none) infinite; }
|
||||
|
||||
@keyframes ticker { from { transform: translateX(100%); } to { transform: translateX(-100%); } }
|
||||
.ticker { display: inline-block; white-space: nowrap; animation: ticker 32s linear infinite; }
|
||||
|
||||
/* Radar sweep wedge. */
|
||||
@keyframes radar { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
.radar-sweep {
|
||||
background: conic-gradient(from 0deg, rgba(0, 255, 208, 0.28), rgba(0, 255, 208, 0) 70deg);
|
||||
border-radius: 9999px;
|
||||
animation: radar 4s linear infinite;
|
||||
}
|
||||
|
||||
/* Thin neon scrollbars to match the theme. */
|
||||
* { scrollbar-width: thin; scrollbar-color: rgba(0, 255, 208, 0.3) transparent; }
|
||||
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
*::-webkit-scrollbar-thumb { background: rgba(0, 255, 208, 0.3); }
|
||||
*::-webkit-scrollbar-track { background: transparent; }
|
||||
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
// Horizontal gauge. `tone` is a full bg-* class (statically scannable).
|
||||
let { label, pct = 0, tone = 'bg-term', value = null, unit = '%' } = $props()
|
||||
let clamped = $derived(Math.max(0, Math.min(100, pct)))
|
||||
</script>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-baseline justify-between text-[0.65rem] uppercase tracking-wider">
|
||||
<span class="text-haze">{label}</span>
|
||||
<span class="text-term tabular-nums glow-sm">{value ?? clamped}{unit}</span>
|
||||
</div>
|
||||
<div class="h-2 w-full overflow-hidden border border-haze/30 bg-void">
|
||||
<div class="h-full {tone} transition-[width] duration-700 ease-out" style="width:{clamped}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { navItems, ship } from './data.js'
|
||||
import { nav } from './router.svelte.js'
|
||||
</script>
|
||||
|
||||
<nav class="flex items-stretch border-b border-haze/30 bg-hull/80">
|
||||
<div class="flex items-center gap-3 border-r border-haze/30 px-4 py-3">
|
||||
<span class="text-flare glow text-2xl leading-none">◭</span>
|
||||
<div class="leading-tight">
|
||||
<div class="text-term glow-sm text-sm tracking-[0.25em]">{ship.name}</div>
|
||||
<div class="text-haze text-[0.6rem] tracking-[0.2em]">{ship.registry}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-stretch overflow-x-auto">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href="#/{item.id}"
|
||||
aria-current={nav.current === item.id ? 'page' : undefined}
|
||||
class="group relative flex items-center gap-2 px-4 py-3 text-xs uppercase tracking-[0.2em] transition-colors
|
||||
{nav.current === item.id ? 'text-term glow-sm bg-term/5' : 'text-haze hover:text-term hover:bg-term/5'}"
|
||||
>
|
||||
<span class="text-base leading-none">{item.glyph}</span>
|
||||
<span class="hidden md:inline">{item.label}</span>
|
||||
{#if nav.current === item.id}
|
||||
<span class="absolute inset-x-0 bottom-0 h-[2px] bg-term glow"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
// Reusable console "module": cut-corner border, header strip, body slot.
|
||||
// `accent` is a full Tailwind class string so the class is statically visible
|
||||
// to Tailwind's scanner (e.g. "text-flare").
|
||||
let { title = '', accent = 'text-term', children } = $props()
|
||||
</script>
|
||||
|
||||
<section class="cut relative border border-haze/30 bg-hull/70">
|
||||
<div class="absolute left-0 top-0 h-[2px] w-12 bg-term/70"></div>
|
||||
<header class="flex items-center gap-2 border-b border-haze/25 bg-hull2/50 px-3 py-2">
|
||||
<span class="text-term/60 text-xs">▌</span>
|
||||
<h2 class="text-[0.7rem] uppercase tracking-[0.3em] {accent} glow-sm">{title}</h2>
|
||||
</header>
|
||||
<div class="p-3 md:p-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
// Sweeping radar with positioned blips. `blips`: [{x,y,tone,label}] in %.
|
||||
let { blips = [] } = $props()
|
||||
</script>
|
||||
|
||||
<div class="relative mx-auto aspect-square w-full max-w-[300px] overflow-hidden rounded-full border border-haze/40 bg-void/60">
|
||||
<div class="absolute inset-[14%] rounded-full border border-haze/20"></div>
|
||||
<div class="absolute inset-[32%] rounded-full border border-haze/20"></div>
|
||||
<div class="absolute inset-[48%] rounded-full border border-haze/20"></div>
|
||||
<div class="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-haze/15"></div>
|
||||
<div class="absolute top-1/2 left-0 h-px w-full -translate-y-1/2 bg-haze/15"></div>
|
||||
|
||||
<div class="radar-sweep absolute inset-0"></div>
|
||||
|
||||
<!-- own ship -->
|
||||
<div class="absolute left-1/2 top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-flare glow"></div>
|
||||
|
||||
{#each blips as b}
|
||||
<div
|
||||
class="absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full {b.tone ?? 'bg-term'} glow blink"
|
||||
style="left:{b.x}%; top:{b.y}%"
|
||||
title={b.label}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
let { label, value, unit = '', tone = 'text-term', sub = '' } = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-[0.6rem] uppercase tracking-[0.2em] text-haze">{label}</span>
|
||||
<span class="text-2xl md:text-3xl font-bold tabular-nums {tone} glow-sm leading-none">
|
||||
{value}{#if unit}<span class="ml-1 text-sm font-normal text-haze">{unit}</span>{/if}
|
||||
</span>
|
||||
{#if sub}<span class="text-[0.65rem] text-haze">{sub}</span>{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { ship } from './data.js'
|
||||
import { clock, stardate, hhmmss } from './clock.svelte.js'
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 border-b border-haze/20 bg-void/60 px-4 py-1.5 text-[0.65rem] uppercase tracking-[0.18em] text-haze">
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-1">
|
||||
<span><span class="text-haze/70">CLASS</span> <span class="text-term">{ship.class}</span></span>
|
||||
<span><span class="text-haze/70">CMD</span> <span class="text-term">{ship.captain}</span></span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-1">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-ok glow blink"></span>
|
||||
<span class="text-ok glow-sm">CONDITION GREEN</span>
|
||||
</span>
|
||||
<span><span class="text-haze/70">STARDATE</span> <span class="text-term tabular-nums">{stardate(clock.now)}</span></span>
|
||||
<span><span class="text-haze/70">MISSION TIME</span> <span class="text-term tabular-nums glow-sm">{hhmmss(clock.now)}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
// Status pill. Literal class map so every variant survives Tailwind purging.
|
||||
let { tone = 'term', children } = $props()
|
||||
const map = {
|
||||
term: 'border-term/60 text-term',
|
||||
ok: 'border-ok/60 text-ok',
|
||||
warn: 'border-warn/60 text-warn',
|
||||
alert: 'border-alert/60 text-alert',
|
||||
flare: 'border-flare/60 text-flare',
|
||||
haze: 'border-haze/60 text-haze',
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center gap-1 border px-2 py-0.5 text-[0.6rem] uppercase tracking-[0.18em] glow-sm {map[tone] ?? map.term}">
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,24 @@
|
||||
// Shared 1 Hz clock. Module-level $state + a single interval, so every consumer
|
||||
// (status bar, footer) shares one timer. Read `clock.now` reactively.
|
||||
|
||||
let _now = $state(new Date())
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
setInterval(() => { _now = new Date() }, 1000)
|
||||
}
|
||||
|
||||
export const clock = {
|
||||
get now() { return _now },
|
||||
}
|
||||
|
||||
// Playful "stardate" derived from the wall clock, purely cosmetic.
|
||||
export function stardate(d) {
|
||||
const epoch = Date.UTC(2350, 0, 1)
|
||||
const days = (d.getTime() - epoch) / 86_400_000
|
||||
return (47000 + days / 3.65).toFixed(1)
|
||||
}
|
||||
|
||||
export function hhmmss(d) {
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Dummy data for the spaceship terminal. All static/fictional — no real
|
||||
// telemetry source yet. A future runtime agent could replace these with a
|
||||
// fetch() to a fleet API; the components only read from here.
|
||||
|
||||
export const navItems = [
|
||||
{ id: 'dashboard', label: 'BRIDGE', glyph: '◈' },
|
||||
{ id: 'power', label: 'REACTOR', glyph: '⬡' },
|
||||
{ id: 'nav', label: 'NAVIGATION', glyph: '✧' },
|
||||
{ id: 'life', label: 'LIFE SUPPORT', glyph: '❂' },
|
||||
{ id: 'comms', label: 'COMMS', glyph: '☰' },
|
||||
]
|
||||
|
||||
export const ship = {
|
||||
name: 'ISV AURELIAN',
|
||||
registry: 'FN-774-Δ',
|
||||
class: 'Lancer-class Deep Survey Cruiser',
|
||||
captain: 'Cmdr. V. Okonkwo',
|
||||
}
|
||||
|
||||
// --- BRIDGE / dashboard ------------------------------------------------------
|
||||
export const systemStatus = [
|
||||
{ name: 'Reactor Core', state: 'NOMINAL', tone: 'ok' },
|
||||
{ name: 'Shield Matrix', state: 'NOMINAL', tone: 'ok' },
|
||||
{ name: 'FTL Drive', state: 'CHARGING', tone: 'warn' },
|
||||
{ name: 'Life Support', state: 'NOMINAL', tone: 'ok' },
|
||||
{ name: 'Hull Integrity', state: '98.4%', tone: 'ok' },
|
||||
{ name: 'Weapons', state: 'STANDBY', tone: 'term' },
|
||||
{ name: 'Sensor Array', state: 'NOMINAL', tone: 'ok' },
|
||||
{ name: 'Comms Relay', state: 'DEGRADED', tone: 'alert' },
|
||||
]
|
||||
|
||||
export const subsystemLoad = [
|
||||
{ label: 'Propulsion', pct: 72, tone: 'bg-term' },
|
||||
{ label: 'Shields', pct: 64, tone: 'bg-term' },
|
||||
{ label: 'Life Support', pct: 38, tone: 'bg-ok' },
|
||||
{ label: 'Sensors', pct: 51, tone: 'bg-term' },
|
||||
{ label: 'FTL Charge', pct: 61, tone: 'bg-warn' },
|
||||
{ label: 'Weapons', pct: 9, tone: 'bg-haze' },
|
||||
]
|
||||
|
||||
export const logEntries = [
|
||||
{ t: '07:42:11', tone: 'warn', msg: 'Jump capacitor at 61% — full charge ETA 00:18:24' },
|
||||
{ t: '07:39:02', tone: 'alert', msg: 'Comms relay 2 packet loss 14% — rerouting via relay 1' },
|
||||
{ t: '07:31:55', tone: 'term', msg: 'Sensor sweep complete — 3 contacts logged in grid 7-K' },
|
||||
{ t: '07:28:40', tone: 'ok', msg: 'Hull micro-fracture sealed on deck 4, section C' },
|
||||
{ t: '07:14:09', tone: 'ok', msg: 'Reactor output stabilised at 84.2% rated' },
|
||||
{ t: '06:58:21', tone: 'term', msg: 'Course correction +0.4° applied to heading 287' },
|
||||
]
|
||||
|
||||
// --- REACTOR / power ---------------------------------------------------------
|
||||
export const reactor = {
|
||||
output: 84.2, // % rated
|
||||
coreTemp: 4120, // K
|
||||
containment: 99.1, // % field strength
|
||||
rpm: 11840, // turbine
|
||||
}
|
||||
|
||||
export const powerDistribution = [
|
||||
{ label: 'Propulsion Bus', pct: 72, tone: 'bg-term' },
|
||||
{ label: 'Shield Emitters', pct: 64, tone: 'bg-term' },
|
||||
{ label: 'FTL Capacitor', pct: 61, tone: 'bg-warn' },
|
||||
{ label: 'Sensors / EW', pct: 51, tone: 'bg-term' },
|
||||
{ label: 'Life Support', pct: 38, tone: 'bg-ok' },
|
||||
{ label: 'Habitation', pct: 27, tone: 'bg-ok' },
|
||||
{ label: 'Weapons Standby', pct: 9, tone: 'bg-haze' },
|
||||
]
|
||||
|
||||
export const fuelCells = [
|
||||
{ id: 'CELL-A', charge: 96, tone: 'bg-ok' },
|
||||
{ id: 'CELL-B', charge: 91, tone: 'bg-ok' },
|
||||
{ id: 'CELL-C', charge: 78, tone: 'bg-term' },
|
||||
{ id: 'CELL-D', charge: 44, tone: 'bg-warn' },
|
||||
]
|
||||
|
||||
// --- NAVIGATION --------------------------------------------------------------
|
||||
export const navState = {
|
||||
heading: 287.4, // degrees
|
||||
velocity: 0.21, // c
|
||||
system: 'Cygnus Reach',
|
||||
sector: 'Grid 7-K',
|
||||
destination: 'Tarsis Station',
|
||||
eta: '04:12:38',
|
||||
ftlCharge: 61,
|
||||
coords: { x: '+1184.22', y: '-0473.90', z: '+0061.07' },
|
||||
}
|
||||
|
||||
export const waypoints = [
|
||||
{ name: 'Tarsis Station', dist: '2.4 AU', status: 'LOCKED', tone: 'ok' },
|
||||
{ name: 'Helex Belt', dist: '0.8 AU', status: 'PASSING', tone: 'warn' },
|
||||
{ name: 'Relay Buoy 19', dist: '11.6 AU', status: 'QUEUED', tone: 'term' },
|
||||
{ name: 'Cygnus Gate', dist: '34.0 AU', status: 'QUEUED', tone: 'term' },
|
||||
]
|
||||
|
||||
// blips positioned in % within the radar circle (50/50 = center = own ship)
|
||||
export const contacts = [
|
||||
{ x: 64, y: 38, tone: 'bg-term', label: 'Freighter "Coriol"' },
|
||||
{ x: 41, y: 27, tone: 'bg-warn', label: 'Unknown — passive' },
|
||||
{ x: 72, y: 66, tone: 'bg-ok', label: 'Tarsis Station' },
|
||||
{ x: 30, y: 58, tone: 'bg-flare', label: 'Debris field' },
|
||||
]
|
||||
|
||||
// --- LIFE SUPPORT ------------------------------------------------------------
|
||||
export const atmosphere = [
|
||||
{ label: 'Oxygen', value: 20.9, unit: '%', pct: 87, tone: 'bg-ok' },
|
||||
{ label: 'CO₂', value: 0.04, unit: '%', pct: 12, tone: 'bg-ok' },
|
||||
{ label: 'Cabin Press.', value: 101.1, unit: 'kPa', pct: 92, tone: 'bg-ok' },
|
||||
{ label: 'Temperature', value: 21.4, unit: '°C', pct: 71, tone: 'bg-term' },
|
||||
{ label: 'Humidity', value: 44, unit: '%', pct: 44, tone: 'bg-term' },
|
||||
{ label: 'Water Reserve', value: 68, unit: '%', pct: 68, tone: 'bg-term' },
|
||||
]
|
||||
|
||||
export const decks = [
|
||||
{ deck: 'Deck 1 — Bridge', o2: 'OK', temp: '21.0°C', tone: 'ok' },
|
||||
{ deck: 'Deck 2 — Habitation', o2: 'OK', temp: '22.3°C', tone: 'ok' },
|
||||
{ deck: 'Deck 3 — Med / Sci', o2: 'OK', temp: '20.6°C', tone: 'ok' },
|
||||
{ deck: 'Deck 4 — Engineering', o2: 'LOW', temp: '26.8°C', tone: 'warn' },
|
||||
{ deck: 'Deck 5 — Cargo', o2: 'SEALED', temp: '8.1°C', tone: 'term' },
|
||||
]
|
||||
|
||||
export const crew = { total: 84, onDuty: 31, medical: 2, cryo: 12 }
|
||||
|
||||
// --- COMMS -------------------------------------------------------------------
|
||||
export const channels = [
|
||||
{ name: 'Fleet Command', band: 'Q-band', status: 'ENCRYPTED', tone: 'ok' },
|
||||
{ name: 'Tarsis Control', band: 'S-band', status: 'OPEN', tone: 'term' },
|
||||
{ name: 'Relay Buoy 19', band: 'X-band', status: 'WEAK', tone: 'warn' },
|
||||
{ name: 'Distress Guard', band: '121.5', status: 'MONITOR', tone: 'term' },
|
||||
{ name: 'Internal Net', band: 'Hardline', status: 'NOMINAL', tone: 'ok' },
|
||||
]
|
||||
|
||||
export const messages = [
|
||||
{ t: '07:41', from: 'FLEET CMD', tone: 'ok', text: 'Patrol pattern delta authorised. Maintain EMCON.' },
|
||||
{ t: '07:33', from: 'TARSIS CTL', tone: 'term', text: 'Docking bay 4 reserved. Transmit manifest on approach.' },
|
||||
{ t: '07:19', from: 'RELAY-19', tone: 'warn', text: 'Signal degradation detected. Recommend X-band switch.' },
|
||||
{ t: '06:52', from: 'CORIOL', tone: 'term', text: 'Thanks for the escort, Aurelian. Safe travels.' },
|
||||
{ t: '06:40', from: 'FLEET CMD', tone: 'flare', text: 'Priority weather: ion storm forming in grid 9-B.' },
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
// Reactive "telemetry" helper: returns an object whose .value drifts around a
|
||||
// base value on an interval, so static dummy data feels like a live readout.
|
||||
// Call inside a component's script (it registers an $effect for cleanup).
|
||||
|
||||
export function useJitter(base, { amp = 1, decimals = 0, every = 2000 } = {}) {
|
||||
let value = $state(base)
|
||||
|
||||
$effect(() => {
|
||||
const id = setInterval(() => {
|
||||
const delta = (Math.random() - 0.5) * 2 * amp
|
||||
value = Number((base + delta).toFixed(decimals))
|
||||
}, every)
|
||||
return () => clearInterval(id)
|
||||
})
|
||||
|
||||
return { get value() { return value } }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Tiny hash-based router using a module-level rune. Hash routing keeps deep
|
||||
// links client-side (they never reach nginx), so the reverse proxy needs no
|
||||
// SPA-fallback rewrite. `nav.current` is reactive: read it in markup/$derived.
|
||||
|
||||
function parse() {
|
||||
if (typeof window === 'undefined') return 'dashboard'
|
||||
const raw = window.location.hash.replace(/^#\/?/, '')
|
||||
return raw.split(/[/?]/)[0] || 'dashboard'
|
||||
}
|
||||
|
||||
let _route = $state(parse())
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('hashchange', () => { _route = parse() })
|
||||
if (!window.location.hash) window.location.hash = '#/dashboard'
|
||||
}
|
||||
|
||||
export const nav = {
|
||||
get current() { return _route },
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, { target: document.getElementById('app') })
|
||||
|
||||
export default app
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import Panel from '../lib/Panel.svelte'
|
||||
import Stat from '../lib/Stat.svelte'
|
||||
import Bar from '../lib/Bar.svelte'
|
||||
import Tag from '../lib/Tag.svelte'
|
||||
import { channels, messages } from '../lib/data.js'
|
||||
|
||||
const fromTone = {
|
||||
ok: 'text-ok', warn: 'text-warn', alert: 'text-alert',
|
||||
flare: 'text-flare', term: 'text-term',
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Panel title="Uplink"><Stat label="fleet command" value="ENCRYPTED" tone="text-ok" sub="Q-band · 0% loss" /></Panel>
|
||||
<Panel title="Active Channels"><Stat label="monitored" value={channels.length} sub="1 degraded" /></Panel>
|
||||
<Panel title="Signal Latency"><Stat label="to relay" value={418} unit="ms" tone="text-warn" sub="lightspeed lag" /></Panel>
|
||||
<Panel title="EMCON"><Stat label="emissions" value="DELTA" tone="text-flare" sub="restricted transmit" /></Panel>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<Panel title="Channels" accent="text-term">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="text-left text-haze/70 uppercase tracking-wider">
|
||||
<th class="pb-2 font-normal">Channel</th>
|
||||
<th class="pb-2 font-normal">Band</th>
|
||||
<th class="pb-2 text-right font-normal">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-haze/15">
|
||||
{#each channels as c}
|
||||
<tr>
|
||||
<td class="py-2 text-term">{c.name}</td>
|
||||
<td class="py-2 tabular-nums text-haze">{c.band}</td>
|
||||
<td class="py-2 text-right"><Tag tone={c.tone}>{c.status}</Tag></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4 space-y-3">
|
||||
<Bar label="Antenna alignment" pct={97} tone="bg-ok" />
|
||||
<Bar label="Relay-19 link" pct={43} tone="bg-warn" />
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Message Traffic" accent="text-flare">
|
||||
<ul class="space-y-2">
|
||||
{#each messages as m}
|
||||
<li class="border border-haze/20 bg-void/40 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-[0.6rem] uppercase tracking-wider">
|
||||
<span class="{fromTone[m.tone] ?? 'text-term'} glow-sm">{m.from}</span>
|
||||
<span class="tabular-nums text-haze">{m.t}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-snug text-haze/90">{m.text}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script>
|
||||
import Panel from '../lib/Panel.svelte'
|
||||
import Stat from '../lib/Stat.svelte'
|
||||
import Bar from '../lib/Bar.svelte'
|
||||
import Tag from '../lib/Tag.svelte'
|
||||
import { useJitter } from '../lib/live.svelte.js'
|
||||
import { systemStatus, subsystemLoad, logEntries } from '../lib/data.js'
|
||||
|
||||
const output = useJitter(84.2, { amp: 0.6, decimals: 1 })
|
||||
const velocity = useJitter(0.21, { amp: 0.004, decimals: 3, every: 1500 })
|
||||
const hull = useJitter(98.4, { amp: 0.05, decimals: 2, every: 3000 })
|
||||
const shields = useJitter(91.7, { amp: 0.8, decimals: 1 })
|
||||
|
||||
// Literal classes so Tailwind keeps them (no dynamic interpolation).
|
||||
const dot = {
|
||||
term: 'text-term', ok: 'text-ok', warn: 'text-warn',
|
||||
alert: 'text-alert', flare: 'text-flare',
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Panel title="Velocity"><Stat label="relative" value={velocity.value} unit="c" sub="sublight cruise" /></Panel>
|
||||
<Panel title="Hull Integrity"><Stat label="structural" value={hull.value} unit="%" tone="text-ok" sub="no active breaches" /></Panel>
|
||||
<Panel title="Shield Matrix"><Stat label="forward arc" value={shields.value} unit="%" sub="harmonics stable" /></Panel>
|
||||
<Panel title="Reactor Output"><Stat label="rated" value={output.value} unit="%" tone="text-warn" sub="core nominal" /></Panel>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<Panel title="Systems Status" accent="text-term">
|
||||
<ul class="divide-y divide-haze/15">
|
||||
{#each systemStatus as s}
|
||||
<li class="flex items-center justify-between py-2 text-sm">
|
||||
<span class="text-haze">{s.name}</span>
|
||||
<Tag tone={s.tone}>{s.state}</Tag>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Subsystem Load" accent="text-term">
|
||||
<div class="space-y-3">
|
||||
{#each subsystemLoad as b}
|
||||
<Bar label={b.label} pct={b.pct} tone={b.tone} />
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Event Log" accent="text-flare">
|
||||
<ul class="space-y-2 text-xs">
|
||||
{#each logEntries as e}
|
||||
<li class="flex gap-2">
|
||||
<span class="shrink-0 tabular-nums text-haze">{e.t}</span>
|
||||
<span class="shrink-0 {dot[e.tone] ?? 'text-term'}">▍</span>
|
||||
<span class="text-haze/90 leading-snug">{e.msg}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import Panel from '../lib/Panel.svelte'
|
||||
import Stat from '../lib/Stat.svelte'
|
||||
import Bar from '../lib/Bar.svelte'
|
||||
import Tag from '../lib/Tag.svelte'
|
||||
import { atmosphere, decks, crew } from '../lib/data.js'
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Panel title="Crew Aboard"><Stat label="souls" value={crew.total} sub={crew.onDuty + ' on active duty'} /></Panel>
|
||||
<Panel title="In Cryo"><Stat label="long-haul" value={crew.cryo} tone="text-term" sub="pods nominal" /></Panel>
|
||||
<Panel title="Medical"><Stat label="infirmary" value={crew.medical} tone="text-warn" sub="under observation" /></Panel>
|
||||
<Panel title="Atmosphere"><Stat label="overall" value="STABLE" tone="text-ok" sub="recyclers nominal" /></Panel>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<Panel title="Atmospheric Mix" accent="text-term">
|
||||
<div class="space-y-3 lg:col-span-2">
|
||||
{#each atmosphere as a}
|
||||
<Bar label={a.label} pct={a.pct} tone={a.tone} value={a.value} unit={' ' + a.unit} />
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Deck Environment" accent="text-term">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="text-left text-haze/70 uppercase tracking-wider">
|
||||
<th class="pb-2 font-normal">Deck</th>
|
||||
<th class="pb-2 font-normal">O₂</th>
|
||||
<th class="pb-2 text-right font-normal">Temp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-haze/15">
|
||||
{#each decks as d}
|
||||
<tr>
|
||||
<td class="py-2 text-term">{d.deck}</td>
|
||||
<td class="py-2"><Tag tone={d.tone}>{d.o2}</Tag></td>
|
||||
<td class="py-2 text-right tabular-nums text-haze">{d.temp}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Recyclers & Reserves" accent="text-flare">
|
||||
<div class="space-y-3">
|
||||
<Bar label="CO₂ scrubbers" pct={88} tone="bg-ok" />
|
||||
<Bar label="O₂ generation" pct={82} tone="bg-ok" />
|
||||
<Bar label="Water recovery" pct={68} tone="bg-term" />
|
||||
<Bar label="Waste reclaim" pct={59} tone="bg-term" />
|
||||
<div class="mt-3 flex items-center justify-between border-t border-haze/20 pt-3 text-xs">
|
||||
<span class="text-haze uppercase tracking-wider">Emergency O₂</span>
|
||||
<Tag tone="ok">72 hrs</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import Panel from '../lib/Panel.svelte'
|
||||
import Stat from '../lib/Stat.svelte'
|
||||
import Bar from '../lib/Bar.svelte'
|
||||
import Tag from '../lib/Tag.svelte'
|
||||
import Radar from '../lib/Radar.svelte'
|
||||
import { useJitter } from '../lib/live.svelte.js'
|
||||
import { navState, waypoints, contacts } from '../lib/data.js'
|
||||
|
||||
const heading = useJitter(navState.heading, { amp: 0.3, decimals: 1, every: 1500 })
|
||||
const velocity = useJitter(navState.velocity, { amp: 0.004, decimals: 3, every: 1500 })
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Panel title="Heading"><Stat label="bearing" value={heading.value} unit="°" sub="auto-helm engaged" /></Panel>
|
||||
<Panel title="Velocity"><Stat label="relative" value={velocity.value} unit="c" sub="sublight" /></Panel>
|
||||
<Panel title="Current System"><Stat label="position" value={navState.system} tone="text-term" sub={navState.sector} /></Panel>
|
||||
<Panel title="Destination"><Stat label="next waypoint" value={navState.destination} tone="text-flare" sub={'ETA ' + navState.eta} /></Panel>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<Panel title="Tactical Plot" accent="text-term">
|
||||
<Radar blips={contacts} />
|
||||
<div class="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-[0.65rem] text-haze">
|
||||
{#each contacts as c}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-2 w-2 rounded-full {c.tone} glow"></span>
|
||||
<span class="truncate">{c.label}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Flight Plan" accent="text-term">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="text-left text-haze/70 uppercase tracking-wider">
|
||||
<th class="pb-2 font-normal">Waypoint</th>
|
||||
<th class="pb-2 font-normal">Range</th>
|
||||
<th class="pb-2 text-right font-normal">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-haze/15">
|
||||
{#each waypoints as w}
|
||||
<tr>
|
||||
<td class="py-2 text-term">{w.name}</td>
|
||||
<td class="py-2 tabular-nums text-haze">{w.dist}</td>
|
||||
<td class="py-2 text-right"><Tag tone={w.tone}>{w.status}</Tag></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel>
|
||||
|
||||
<Panel title="FTL Drive" accent="text-flare">
|
||||
<div class="space-y-4">
|
||||
<Bar label="Jump capacitor" pct={navState.ftlCharge} tone="bg-warn" />
|
||||
<div class="grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div class="border border-haze/20 bg-void/40 py-2">
|
||||
<div class="text-[0.55rem] uppercase text-haze">X</div>
|
||||
<div class="tabular-nums text-term glow-sm">{navState.coords.x}</div>
|
||||
</div>
|
||||
<div class="border border-haze/20 bg-void/40 py-2">
|
||||
<div class="text-[0.55rem] uppercase text-haze">Y</div>
|
||||
<div class="tabular-nums text-term glow-sm">{navState.coords.y}</div>
|
||||
</div>
|
||||
<div class="border border-haze/20 bg-void/40 py-2">
|
||||
<div class="text-[0.55rem] uppercase text-haze">Z</div>
|
||||
<div class="tabular-nums text-term glow-sm">{navState.coords.z}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between border-t border-haze/20 pt-3 text-xs">
|
||||
<span class="text-haze uppercase tracking-wider">Jump readiness</span>
|
||||
<Tag tone="warn">CHARGING</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import Panel from '../lib/Panel.svelte'
|
||||
import Stat from '../lib/Stat.svelte'
|
||||
import Bar from '../lib/Bar.svelte'
|
||||
import Tag from '../lib/Tag.svelte'
|
||||
import { useJitter } from '../lib/live.svelte.js'
|
||||
import { reactor, powerDistribution, fuelCells } from '../lib/data.js'
|
||||
|
||||
const output = useJitter(reactor.output, { amp: 0.7, decimals: 1 })
|
||||
const temp = useJitter(reactor.coreTemp, { amp: 18, decimals: 0, every: 1500 })
|
||||
const rpm = useJitter(reactor.rpm, { amp: 40, decimals: 0, every: 1200 })
|
||||
const cont = useJitter(reactor.containment, { amp: 0.15, decimals: 1, every: 2500 })
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Panel title="Core Output"><Stat label="rated power" value={output.value} unit="%" tone="text-warn" sub="84% of 2.4 TW" /></Panel>
|
||||
<Panel title="Core Temp"><Stat label="plasma" value={temp.value} unit="K" tone="text-flare" sub="within tolerance" /></Panel>
|
||||
<Panel title="Containment"><Stat label="field strength" value={cont.value} unit="%" tone="text-ok" sub="magnetic bottle stable" /></Panel>
|
||||
<Panel title="Turbine"><Stat label="primary spindle" value={rpm.value} unit="rpm" sub="no vibration faults" /></Panel>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<Panel title="Power Distribution" accent="text-term">
|
||||
<div class="space-y-3 lg:col-span-2">
|
||||
{#each powerDistribution as p}
|
||||
<Bar label={p.label} pct={p.pct} tone={p.tone} />
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Fuel Cells" accent="text-term">
|
||||
<div class="space-y-3">
|
||||
{#each fuelCells as c}
|
||||
<Bar label={c.id} pct={c.charge} tone={c.tone} />
|
||||
{/each}
|
||||
<div class="mt-3 flex items-center justify-between border-t border-haze/20 pt-3 text-xs">
|
||||
<span class="text-haze">Deuterium reserve</span>
|
||||
<Tag tone="ok">77% / 14 days</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Reactor Controls" accent="text-flare">
|
||||
<div class="space-y-2">
|
||||
{#each [
|
||||
{ l: 'Auto-throttle', v: 'ENGAGED', t: 'ok' },
|
||||
{ l: 'SCRAM arming', v: 'SAFE', t: 'term' },
|
||||
{ l: 'Coolant loop A', v: 'FLOW 100%', t: 'ok' },
|
||||
{ l: 'Coolant loop B', v: 'STANDBY', t: 'term' },
|
||||
{ l: 'Plasma injectors', v: '6 / 6 ONLINE', t: 'ok' },
|
||||
{ l: 'Overload damper', v: 'ARMED', t: 'warn' },
|
||||
] as r}
|
||||
<div class="flex items-center justify-between border border-haze/20 bg-void/40 px-3 py-2 text-xs">
|
||||
<span class="text-haze uppercase tracking-wider">{r.l}</span>
|
||||
<Tag tone={r.t}>{r.v}</Tag>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// The kiosk loads the app at http://boot.fleet.lab/kiosk/ (the main serving
|
||||
// nginx reverse-proxies /kiosk/ -> this container). base must match that path
|
||||
// so emitted asset URLs are absolute under /kiosk/. Hash-based routing is used
|
||||
// (see src/lib/router.svelte.js) so deep links never hit the server — no SPA
|
||||
// fallback rewrite is needed through the proxy.
|
||||
export default defineConfig({
|
||||
base: '/kiosk/',
|
||||
plugins: [svelte(), tailwindcss()],
|
||||
build: {
|
||||
target: 'es2022',
|
||||
// Fully self-contained bundle (no CDN/fonts) so it renders on the
|
||||
// air-gapped boot LAN — the dnsmasq sinkhole blocks external call-home.
|
||||
assetsInlineLimit: 4096,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user