cyberia 3.2.9 → 3.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/engine-cyberia.cd.yml +7 -0
- package/.github/workflows/engine-cyberia.ci.yml +14 -2
- package/.github/workflows/ghpkg.ci.yml +1 -0
- package/.github/workflows/npmpkg.ci.yml +10 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/extensions.json +9 -9
- package/.vscode/settings.json +20 -4
- package/CHANGELOG.md +363 -1
- package/CLI-HELP.md +975 -1061
- package/README.md +190 -348
- package/bin/build.js +102 -125
- package/bin/build.template.js +33 -0
- package/bin/cyberia.js +238 -56
- package/bin/deploy.js +16 -3
- package/bin/index.js +238 -56
- package/bump.config.js +26 -0
- package/conf.js +131 -24
- package/deployment.yaml +76 -2
- package/hardhat/package-lock.json +113 -144
- package/hardhat/package.json +4 -3
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +76 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/lxd/lxd-admin-profile.yaml +12 -3
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +3 -1
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/valkey/statefulset.yaml +1 -1
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/package.json +31 -19
- package/scripts/ipxe-setup.sh +52 -49
- package/scripts/k3s-node-setup.sh +81 -46
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/lxd-vm-setup.sh +193 -8
- package/scripts/maas-nat-firewalld.sh +145 -0
- package/scripts/test-monitor.sh +250 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/crypto/crypto.router.js +18 -12
- package/src/api/crypto/crypto.service.js +3 -3
- package/src/api/cyberia-action/cyberia-action.model.js +1 -1
- package/src/api/cyberia-action/cyberia-action.router.js +22 -18
- package/src/api/cyberia-action/cyberia-action.service.js +5 -5
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
- package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
- package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
- package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
- package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
- package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
- package/src/api/cyberia-map/cyberia-map.router.js +35 -30
- package/src/api/cyberia-map/cyberia-map.service.js +7 -7
- package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
- package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +458 -0
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/instance/instance.router.js +29 -24
- package/src/api/instance/instance.service.js +6 -6
- package/src/api/ipfs/ipfs.router.js +21 -16
- package/src/api/ipfs/ipfs.service.js +8 -8
- package/src/api/object-layer/object-layer.router.js +512 -507
- package/src/api/object-layer/object-layer.service.js +17 -14
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +132 -101
- package/src/cli/cluster.js +700 -232
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +291 -294
- package/src/cli/env.js +1 -4
- package/src/cli/fs.js +13 -3
- package/src/cli/image.js +58 -4
- package/src/cli/index.js +127 -15
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +1099 -223
- package/src/cli/monitor.js +396 -9
- package/src/cli/release.js +355 -146
- package/src/cli/repository.js +169 -30
- package/src/cli/run.js +347 -117
- package/src/cli/secrets.js +11 -2
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +5 -0
- package/src/client/components/core/ClientEvents.js +76 -0
- package/src/client/components/core/EventBus.js +4 -0
- package/src/client/components/core/Modal.js +82 -41
- package/src/client/components/core/PanelForm.js +14 -10
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +223 -361
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +212 -185
- package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
- package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +115 -15
- package/src/db/mariadb/MariaDB.js +2 -1
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +130 -21
- package/src/grpc/cyberia/grpc-server.js +25 -57
- package/src/index.js +1 -1
- package/src/runtime/cyberia-client/Dockerfile +10 -7
- package/src/runtime/cyberia-client/Dockerfile.dev +67 -0
- package/src/runtime/cyberia-server/Dockerfile +11 -6
- package/src/runtime/cyberia-server/Dockerfile.dev +47 -0
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/catalog-underpost.js +61 -0
- package/src/server/catalog.js +77 -0
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +496 -135
- package/src/server/ipfs-client.js +5 -3
- package/src/server/process.js +180 -19
- package/src/server/proxy.js +9 -2
- package/src/server/runtime-status.js +235 -0
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +44 -11
- package/src/server/valkey.js +2 -0
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/test/deploy-monitor.test.js +251 -0
- package/bin/file.js +0 -202
- package/bin/vs.js +0 -74
- package/bin/zed.js +0 -84
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -254
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
- package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
// CyberiaServerMetrics.js — SSR dashboard for the cyberia-server
|
|
2
|
+
// metrics API.
|
|
3
|
+
//
|
|
4
|
+
// The runtime contract is:
|
|
5
|
+
// - Polls GET /api/v1/metrics every POLL_INTERVAL_MS.
|
|
6
|
+
// - Derives per-second rates locally by diffing successive snapshots
|
|
7
|
+
// (the server only publishes monotonic counters).
|
|
8
|
+
// - Renders to a static HTML/CSS shell — no SPA framework, no build
|
|
9
|
+
// step. Every value is text in a single-page console.
|
|
10
|
+
//
|
|
11
|
+
// Visual contract:
|
|
12
|
+
// - Retro pixel-art aesthetic: VT323 fixed-width font, hard 2 px
|
|
13
|
+
// borders, terminal palette, no border-radius, no shadows.
|
|
14
|
+
// - No emojis; every glyph in the UI is an SVG resolved through the
|
|
15
|
+
// `<svg><use href="#icon-..."/></svg>` sprite defined in main().
|
|
16
|
+
// - Dense data layout suitable for operators monitoring a live server.
|
|
17
|
+
|
|
18
|
+
const POLL_INTERVAL_MS = 2000;
|
|
19
|
+
|
|
20
|
+
const s = (el) => document.querySelector(el);
|
|
21
|
+
const sa = (el) => document.querySelectorAll(el);
|
|
22
|
+
|
|
23
|
+
const append = (el, html) => s(el).insertAdjacentHTML('beforeend', html);
|
|
24
|
+
|
|
25
|
+
const setHTML = (el, html) => {
|
|
26
|
+
const element = s(el);
|
|
27
|
+
if (element) element.innerHTML = html;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── Formatters ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
33
|
+
|
|
34
|
+
const formatUptime = (seconds) => {
|
|
35
|
+
if (!Number.isFinite(seconds) || seconds < 0) return '--:--:--';
|
|
36
|
+
const s = Math.floor(seconds);
|
|
37
|
+
const d = Math.floor(s / 86400);
|
|
38
|
+
const h = Math.floor((s % 86400) / 3600);
|
|
39
|
+
const m = Math.floor((s % 3600) / 60);
|
|
40
|
+
const sec = s % 60;
|
|
41
|
+
return d > 0 ? `${d}d ${pad(h)}:${pad(m)}:${pad(sec)}` : `${pad(h)}:${pad(m)}:${pad(sec)}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const formatNumber = (n) => {
|
|
45
|
+
if (!Number.isFinite(n)) return '0';
|
|
46
|
+
return Math.round(n).toLocaleString('en-US');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const formatBytes = (bytes) => {
|
|
50
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
51
|
+
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
|
52
|
+
let i = 0;
|
|
53
|
+
let v = bytes;
|
|
54
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
55
|
+
v /= 1024;
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const formatRate = (n, unit) => {
|
|
62
|
+
if (!Number.isFinite(n) || n <= 0) return `0 ${unit}`;
|
|
63
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)} k${unit}`;
|
|
64
|
+
return `${n.toFixed(n >= 100 ? 0 : 1)} ${unit}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const formatPercent = (value) => {
|
|
68
|
+
if (!Number.isFinite(value)) return '0.00';
|
|
69
|
+
return value.toFixed(2);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Health palette. Closed enum — any unknown value falls through to
|
|
73
|
+
// the worst known severity ("critical").
|
|
74
|
+
const HEALTH_TONE = {
|
|
75
|
+
healthy: 'ok',
|
|
76
|
+
degraded: 'warn',
|
|
77
|
+
maintenance: 'info',
|
|
78
|
+
critical: 'err',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const LOAD_TONE = {
|
|
82
|
+
low: 'ok',
|
|
83
|
+
moderate: 'warn',
|
|
84
|
+
high: 'warn',
|
|
85
|
+
critical: 'err',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const toneClass = (tone) => `tone-${tone || 'muted'}`;
|
|
89
|
+
|
|
90
|
+
// ── Rate tracker ────────────────────────────────────────────────────────────
|
|
91
|
+
// The server only exposes monotonically-increasing counters; the
|
|
92
|
+
// dashboard derives /s rates by diffing the prior snapshot. Implemented
|
|
93
|
+
// as a pair of free functions (rather than a class) so the inlined
|
|
94
|
+
// browser script can recreate the closure without method-shorthand
|
|
95
|
+
// surprises from Function.prototype.toString.
|
|
96
|
+
const rateState = { prev: null, prevTs: 0 };
|
|
97
|
+
|
|
98
|
+
const rateDiff = (curr, key) => {
|
|
99
|
+
if (!rateState.prev) return 0;
|
|
100
|
+
const dt = (curr._ts - rateState.prevTs) / 1000;
|
|
101
|
+
if (dt <= 0) return 0;
|
|
102
|
+
return (Number(curr[key] || 0) - Number(rateState.prev[key] || 0)) / dt;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const rateUpdate = (snapshot) => {
|
|
106
|
+
rateState.prev = snapshot;
|
|
107
|
+
rateState.prevTs = snapshot._ts;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ── Icon sprite ────────────────────────────────────────────────────────────
|
|
111
|
+
// Every glyph in the UI is rendered through this single sprite via
|
|
112
|
+
// <svg class="icon"><use href="#icon-..."/></svg>. Icons are designed
|
|
113
|
+
// on a 12 × 12 grid so they stay crisp at the dashboard's pixel scale.
|
|
114
|
+
const iconSprite = html`
|
|
115
|
+
<svg width="0" height="0" style="position: absolute; overflow: hidden;" aria-hidden="true">
|
|
116
|
+
<defs>
|
|
117
|
+
<!-- Pulse / liveness dot -->
|
|
118
|
+
<symbol id="icon-pulse" viewBox="0 0 12 12">
|
|
119
|
+
<path d="M0 6h2l1-3 2 6 2-4 1 2h4v2H7l-1-1-2 4-2-6-1 1H0z" fill="currentColor" />
|
|
120
|
+
</symbol>
|
|
121
|
+
<!-- Server / chip -->
|
|
122
|
+
<symbol id="icon-cpu" viewBox="0 0 12 12">
|
|
123
|
+
<path d="M3 1h6v2h1v6H9v2H3V9H1V3h2zM3 3v6h6V3z M4 4h4v4H4z" fill="currentColor" />
|
|
124
|
+
</symbol>
|
|
125
|
+
<!-- Clock -->
|
|
126
|
+
<symbol id="icon-clock" viewBox="0 0 12 12">
|
|
127
|
+
<path d="M2 2h8v8H2zM4 4v4h4V7H5V4z" fill="currentColor" />
|
|
128
|
+
</symbol>
|
|
129
|
+
<!-- Bar chart / load -->
|
|
130
|
+
<symbol id="icon-load" viewBox="0 0 12 12">
|
|
131
|
+
<path d="M1 9h2v2H1zm3-3h2v5H4zm3-3h2v8H7zm3-2h2v10h-2z" fill="currentColor" />
|
|
132
|
+
</symbol>
|
|
133
|
+
<!-- Plug / websocket -->
|
|
134
|
+
<symbol id="icon-plug" viewBox="0 0 12 12">
|
|
135
|
+
<path d="M3 1h2v3H3zm4 0h2v3H7zM2 4h8v3a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3zM5 10h2v2H5z" fill="currentColor" />
|
|
136
|
+
</symbol>
|
|
137
|
+
<!-- Players / users -->
|
|
138
|
+
<symbol id="icon-users" viewBox="0 0 12 12">
|
|
139
|
+
<path d="M4 1h2v2H4zM3 4h4v2H3zM2 6h6v1H2zM1 7h8v3H1z" fill="currentColor" />
|
|
140
|
+
</symbol>
|
|
141
|
+
<!-- Map / grid -->
|
|
142
|
+
<symbol id="icon-map" viewBox="0 0 12 12">
|
|
143
|
+
<path d="M1 1h3v3H1zm4 0h3v3H5zm4 0h2v3H9zM1 5h3v3H1zm4 0h3v3H5zm4 0h2v3H9zM1 9h3v2H1zm4 0h3v2H5zm4 0h2v2H9z" fill="currentColor" />
|
|
144
|
+
</symbol>
|
|
145
|
+
<!-- Entity / square -->
|
|
146
|
+
<symbol id="icon-cube" viewBox="0 0 12 12">
|
|
147
|
+
<path d="M2 2h8v8H2zM4 4v4h4V4z" fill="currentColor" />
|
|
148
|
+
</symbol>
|
|
149
|
+
<!-- Layer / stack -->
|
|
150
|
+
<symbol id="icon-stack" viewBox="0 0 12 12">
|
|
151
|
+
<path d="M1 3l5-2 5 2-5 2zm0 3l5 2 5-2-2-1-3 1.2L4 5zm0 3l5 2 5-2-2-1-3 1.2L4 8z" fill="currentColor" />
|
|
152
|
+
</symbol>
|
|
153
|
+
<!-- Tick / cadence -->
|
|
154
|
+
<symbol id="icon-tick" viewBox="0 0 12 12">
|
|
155
|
+
<path d="M6 1l1 4h4l-3 2 1 4-3-2-3 2 1-4-3-2h4z" fill="currentColor" />
|
|
156
|
+
</symbol>
|
|
157
|
+
<!-- Alert / error -->
|
|
158
|
+
<symbol id="icon-alert" viewBox="0 0 12 12">
|
|
159
|
+
<path d="M5 1h2v6H5zm0 8h2v2H5z" fill="currentColor" />
|
|
160
|
+
</symbol>
|
|
161
|
+
<!-- Down arrow / inbound -->
|
|
162
|
+
<symbol id="icon-down" viewBox="0 0 12 12">
|
|
163
|
+
<path d="M5 1h2v6h2L6 11 3 7h2z" fill="currentColor" />
|
|
164
|
+
</symbol>
|
|
165
|
+
<!-- Up arrow / outbound -->
|
|
166
|
+
<symbol id="icon-up" viewBox="0 0 12 12">
|
|
167
|
+
<path d="M5 11h2V5h2L6 1 3 5h2z" fill="currentColor" />
|
|
168
|
+
</symbol>
|
|
169
|
+
<!-- Doc / api -->
|
|
170
|
+
<symbol id="icon-doc" viewBox="0 0 12 12">
|
|
171
|
+
<path d="M2 1h6l2 2v8H2zM4 4h4v1H4zm0 2h4v1H4zm0 2h3v1H4z" fill="currentColor" />
|
|
172
|
+
</symbol>
|
|
173
|
+
</defs>
|
|
174
|
+
</svg>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const icon = (id) => `<svg class="icon" aria-hidden="true"><use href="#icon-${id}"/></svg>`;
|
|
178
|
+
|
|
179
|
+
// ── Atoms ──────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
const kv = (label, value, tone) => html`
|
|
182
|
+
<div class="kv">
|
|
183
|
+
<span class="kv-label">${label}</span>
|
|
184
|
+
<span class="kv-value ${toneClass(tone)}">${value}</span>
|
|
185
|
+
</div>
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const meter = (label, current, max, hint) => {
|
|
189
|
+
const denom = Number(max) || 1;
|
|
190
|
+
const pct = Math.min(100, Math.max(0, (Number(current) / denom) * 100));
|
|
191
|
+
const tone = pct >= 90 ? 'err' : pct >= 70 ? 'warn' : 'ok';
|
|
192
|
+
return html`
|
|
193
|
+
<div class="meter">
|
|
194
|
+
<div class="meter-head">
|
|
195
|
+
<span class="meter-label">${label}</span>
|
|
196
|
+
<span class="meter-value ${toneClass(tone)}">${formatNumber(current)} / ${formatNumber(max)}</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="meter-bar">
|
|
199
|
+
<div class="meter-fill ${toneClass(tone)}" style="width: ${pct.toFixed(1)}%"></div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="meter-foot">
|
|
202
|
+
<span>${pct.toFixed(1)} %</span>
|
|
203
|
+
<span>${hint || ''}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const panel = (titleIcon, title, body, meta) => html`
|
|
210
|
+
<section class="panel">
|
|
211
|
+
<header class="panel-head">
|
|
212
|
+
<h2 class="panel-title">${icon(titleIcon)}<span>${title}</span></h2>
|
|
213
|
+
${meta ? `<span class="panel-meta">${meta}</span>` : ''}
|
|
214
|
+
</header>
|
|
215
|
+
<div class="panel-body">${body}</div>
|
|
216
|
+
</section>
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
// ── Renderers ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
const renderHealthPanel = (m) => {
|
|
222
|
+
const tone = HEALTH_TONE[m.health] || 'err';
|
|
223
|
+
return panel(
|
|
224
|
+
'pulse',
|
|
225
|
+
'Health',
|
|
226
|
+
html`
|
|
227
|
+
<div class="health-block">
|
|
228
|
+
<span class="health-dot ${toneClass(tone)}"></span>
|
|
229
|
+
<div>
|
|
230
|
+
<div class="health-status ${toneClass(tone)}">${(m.health || 'unknown').toUpperCase()}</div>
|
|
231
|
+
<div class="health-desc">${m.health_description || '—'}</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="grid-2">
|
|
235
|
+
${kv('Instance', m.instance_code || '—')} ${kv('Uptime', formatUptime(m.server_uptime_sec))}
|
|
236
|
+
</div>
|
|
237
|
+
`,
|
|
238
|
+
new Date(m.timestamp).toISOString().replace('T', ' ').slice(0, 19) + ' UTC',
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const renderRuntimePanel = (m) => {
|
|
243
|
+
const r = m.runtime || {};
|
|
244
|
+
return panel(
|
|
245
|
+
'tick',
|
|
246
|
+
'Simulation',
|
|
247
|
+
html`
|
|
248
|
+
<div class="grid-2">
|
|
249
|
+
${kv('Tick rate', `${r.tick_rate_hz || 0} Hz`)} ${kv('Snapshot rate', `${r.snapshot_rate_hz || 0} Hz`)}
|
|
250
|
+
${kv('Tick #', formatNumber(r.current_tick))} ${kv('Tick duration', `${(r.tick_duration_ms || 0).toFixed(2)} ms`)}
|
|
251
|
+
${kv('AOI radius', `${(r.aoi_radius || 0).toFixed(1)}`)} ${kv('GOMAXPROCS', r.gomaxprocs || r.num_cpu || 0)}
|
|
252
|
+
</div>
|
|
253
|
+
`,
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const renderProcessPanel = (m) => {
|
|
258
|
+
const r = m.runtime || {};
|
|
259
|
+
return panel(
|
|
260
|
+
'cpu',
|
|
261
|
+
'Process',
|
|
262
|
+
html`
|
|
263
|
+
<div class="grid-2">
|
|
264
|
+
${kv('Go version', r.go_version || '—')} ${kv('Goroutines', formatNumber(r.num_goroutine))}
|
|
265
|
+
${kv('Heap alloc', `${formatNumber(r.heap_alloc_mb)} MiB`)}
|
|
266
|
+
${kv('Heap sys', `${formatNumber(r.heap_sys_mb)} MiB`)} ${kv('GC cycles', formatNumber(r.num_gc))}
|
|
267
|
+
${kv('CPU cores', r.num_cpu || 0)}
|
|
268
|
+
</div>
|
|
269
|
+
`,
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const renderWorkloadPanel = (m) => {
|
|
274
|
+
const w = m.workload || {};
|
|
275
|
+
const tone = LOAD_TONE[w.current_load] || 'err';
|
|
276
|
+
return panel(
|
|
277
|
+
'load',
|
|
278
|
+
'Workload',
|
|
279
|
+
html`
|
|
280
|
+
<div class="load-readout">
|
|
281
|
+
<div class="load-pct ${toneClass(tone)}">${formatPercent(w.load_percentage)}%</div>
|
|
282
|
+
<div class="load-tag ${toneClass(tone)}">${(w.current_load || 'unknown').toUpperCase()}</div>
|
|
283
|
+
</div>
|
|
284
|
+
${meter('Entity capacity', m.entities?.total_entities || 0, w.max_entity_capacity || 0, 'entities')}
|
|
285
|
+
${meter('Object layers', m.entities?.total_object_layers || 0, w.max_object_layers || 0, 'layers')}
|
|
286
|
+
`,
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const renderWebsocketPanel = (m, snap) => {
|
|
291
|
+
const w = m.websocket || {};
|
|
292
|
+
const inRate = rateDiff(snap, '_msgIn');
|
|
293
|
+
const outRate = rateDiff(snap, '_msgOut');
|
|
294
|
+
const bInRate = rateDiff(snap, '_byIn');
|
|
295
|
+
const bOutRate = rateDiff(snap, '_byOut');
|
|
296
|
+
const errRate = rateDiff(snap, '_errTotal');
|
|
297
|
+
const errTone = errRate > 0.1 ? 'err' : (w.read_errors_total || 0) + (w.write_errors_total || 0) > 0 ? 'warn' : 'ok';
|
|
298
|
+
|
|
299
|
+
return panel(
|
|
300
|
+
'plug',
|
|
301
|
+
'WebSocket',
|
|
302
|
+
html`
|
|
303
|
+
<div class="grid-3">
|
|
304
|
+
${kv('Status', (w.status || '—').toUpperCase(), w.status === 'running' ? 'ok' : 'warn')}
|
|
305
|
+
${kv('Active', formatNumber(w.active_connections))} ${kv('Uptime', formatUptime(w.uptime_sec))}
|
|
306
|
+
</div>
|
|
307
|
+
<table class="grid-table">
|
|
308
|
+
<thead>
|
|
309
|
+
<tr>
|
|
310
|
+
<th></th>
|
|
311
|
+
<th>RATE / s</th>
|
|
312
|
+
<th>TOTAL</th>
|
|
313
|
+
</tr>
|
|
314
|
+
</thead>
|
|
315
|
+
<tbody>
|
|
316
|
+
<tr>
|
|
317
|
+
<th>${icon('down')} Messages in</th>
|
|
318
|
+
<td>${formatRate(inRate, 'msg/s')}</td>
|
|
319
|
+
<td>${formatNumber(w.messages_in_total)}</td>
|
|
320
|
+
</tr>
|
|
321
|
+
<tr>
|
|
322
|
+
<th>${icon('up')} Messages out</th>
|
|
323
|
+
<td>${formatRate(outRate, 'msg/s')}</td>
|
|
324
|
+
<td>${formatNumber(w.messages_out_total)}</td>
|
|
325
|
+
</tr>
|
|
326
|
+
<tr>
|
|
327
|
+
<th>${icon('down')} Bytes in</th>
|
|
328
|
+
<td>${formatBytes(bInRate)}/s</td>
|
|
329
|
+
<td>${formatBytes(w.bytes_in_total)}</td>
|
|
330
|
+
</tr>
|
|
331
|
+
<tr>
|
|
332
|
+
<th>${icon('up')} Bytes out</th>
|
|
333
|
+
<td>${formatBytes(bOutRate)}/s</td>
|
|
334
|
+
<td>${formatBytes(w.bytes_out_total)}</td>
|
|
335
|
+
</tr>
|
|
336
|
+
<tr class="${toneClass(errTone)}">
|
|
337
|
+
<th>${icon('alert')} Errors</th>
|
|
338
|
+
<td>${formatRate(errRate, 'err/s')}</td>
|
|
339
|
+
<td>${formatNumber((w.read_errors_total || 0) + (w.write_errors_total || 0))}</td>
|
|
340
|
+
</tr>
|
|
341
|
+
<tr>
|
|
342
|
+
<th>${icon('users')} Connects</th>
|
|
343
|
+
<td>—</td>
|
|
344
|
+
<td>${formatNumber(w.connects_total)}</td>
|
|
345
|
+
</tr>
|
|
346
|
+
<tr>
|
|
347
|
+
<th>${icon('users')} Disconnects</th>
|
|
348
|
+
<td>—</td>
|
|
349
|
+
<td>${formatNumber(w.disconnects_total)}</td>
|
|
350
|
+
</tr>
|
|
351
|
+
</tbody>
|
|
352
|
+
</table>
|
|
353
|
+
`,
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const renderEntitiesPanel = (m) => {
|
|
358
|
+
const e = m.entities || {};
|
|
359
|
+
// Stable type → icon mapping. Keys mirror the closed enum the API
|
|
360
|
+
// emits in entities_by_type; an unknown key falls back to the
|
|
361
|
+
// generic cube glyph.
|
|
362
|
+
const typeIcon = {
|
|
363
|
+
player: 'users',
|
|
364
|
+
bot: 'cpu',
|
|
365
|
+
floor: 'map',
|
|
366
|
+
obstacle: 'cube',
|
|
367
|
+
foreground: 'stack',
|
|
368
|
+
portal: 'plug',
|
|
369
|
+
resource: 'stack',
|
|
370
|
+
};
|
|
371
|
+
const rows = (e.entities_by_type || [])
|
|
372
|
+
.map(
|
|
373
|
+
(et) => html`
|
|
374
|
+
<tr class="${et.count === 0 ? 'muted' : ''}">
|
|
375
|
+
<th>${icon(typeIcon[et.type] || 'cube')} ${et.type}</th>
|
|
376
|
+
<td>${formatNumber(et.count)}</td>
|
|
377
|
+
<td>${formatNumber(et.active_object_layers)}</td>
|
|
378
|
+
<td>${formatNumber(et.total_object_layers)}</td>
|
|
379
|
+
</tr>
|
|
380
|
+
`,
|
|
381
|
+
)
|
|
382
|
+
.join('');
|
|
383
|
+
return panel(
|
|
384
|
+
'cube',
|
|
385
|
+
'Entities',
|
|
386
|
+
html`
|
|
387
|
+
<div class="grid-3">
|
|
388
|
+
${kv('Total', formatNumber(e.total_entities))} ${kv('Active layers', formatNumber(e.active_object_layers))}
|
|
389
|
+
${kv('Avg layers/entity', formatPercent(e.avg_object_layers_per_entity))}
|
|
390
|
+
</div>
|
|
391
|
+
<table class="grid-table">
|
|
392
|
+
<thead>
|
|
393
|
+
<tr>
|
|
394
|
+
<th>TYPE</th>
|
|
395
|
+
<th>COUNT</th>
|
|
396
|
+
<th>ACTIVE</th>
|
|
397
|
+
<th>LAYERS</th>
|
|
398
|
+
</tr>
|
|
399
|
+
</thead>
|
|
400
|
+
<tbody>
|
|
401
|
+
${rows || `<tr><td colspan="4" class="muted">no entities loaded</td></tr>`}
|
|
402
|
+
</tbody>
|
|
403
|
+
</table>
|
|
404
|
+
`,
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const renderMapsPanel = (m) => {
|
|
409
|
+
const maps = m.maps || [];
|
|
410
|
+
if (maps.length === 0) {
|
|
411
|
+
return panel('map', 'Maps', `<div class="muted center">no maps loaded</div>`);
|
|
412
|
+
}
|
|
413
|
+
const rows = maps
|
|
414
|
+
.map(
|
|
415
|
+
(mp) => html`
|
|
416
|
+
<tr>
|
|
417
|
+
<th>${icon('map')} ${mp.map_code}</th>
|
|
418
|
+
<td>${mp.grid_w}×${mp.grid_h}</td>
|
|
419
|
+
<td class="${mp.players > 0 ? 'tone-ok' : ''}">${formatNumber(mp.players)}</td>
|
|
420
|
+
<td>${formatNumber(mp.bots)}</td>
|
|
421
|
+
<td>${formatNumber(mp.floors)}</td>
|
|
422
|
+
<td>${formatNumber(mp.obstacles)}</td>
|
|
423
|
+
<td>${formatNumber(mp.foregrounds)}</td>
|
|
424
|
+
<td>${formatNumber(mp.portals)}</td>
|
|
425
|
+
<td>${formatNumber(mp.resources)}</td>
|
|
426
|
+
<td><strong>${formatNumber(mp.total_entities)}</strong></td>
|
|
427
|
+
</tr>
|
|
428
|
+
`,
|
|
429
|
+
)
|
|
430
|
+
.join('');
|
|
431
|
+
return panel(
|
|
432
|
+
'map',
|
|
433
|
+
'Maps',
|
|
434
|
+
html`
|
|
435
|
+
<table class="grid-table dense">
|
|
436
|
+
<thead>
|
|
437
|
+
<tr>
|
|
438
|
+
<th>CODE</th>
|
|
439
|
+
<th>GRID</th>
|
|
440
|
+
<th>PLY</th>
|
|
441
|
+
<th>BOT</th>
|
|
442
|
+
<th>FLR</th>
|
|
443
|
+
<th>OBS</th>
|
|
444
|
+
<th>FG</th>
|
|
445
|
+
<th>PRT</th>
|
|
446
|
+
<th>RES</th>
|
|
447
|
+
<th>TOTAL</th>
|
|
448
|
+
</tr>
|
|
449
|
+
</thead>
|
|
450
|
+
<tbody>
|
|
451
|
+
${rows}
|
|
452
|
+
</tbody>
|
|
453
|
+
</table>
|
|
454
|
+
`,
|
|
455
|
+
`${maps.length} map${maps.length === 1 ? '' : 's'}`,
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const renderDashboard = (m) => {
|
|
460
|
+
// Snapshot the throughput counters into rate-tracker shape so the
|
|
461
|
+
// rate diffs are stable across panel re-renders.
|
|
462
|
+
const snap = {
|
|
463
|
+
_ts: Date.now(),
|
|
464
|
+
_msgIn: m.websocket?.messages_in_total || 0,
|
|
465
|
+
_msgOut: m.websocket?.messages_out_total || 0,
|
|
466
|
+
_byIn: m.websocket?.bytes_in_total || 0,
|
|
467
|
+
_byOut: m.websocket?.bytes_out_total || 0,
|
|
468
|
+
_errTotal: (m.websocket?.read_errors_total || 0) + (m.websocket?.write_errors_total || 0),
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const body = html`
|
|
472
|
+
<main class="dash">
|
|
473
|
+
<header class="dash-head">
|
|
474
|
+
<div class="brand">
|
|
475
|
+
<span class="brand-glyph">${icon('cpu')}</span>
|
|
476
|
+
<span class="brand-text">CYBERIA SERVER</span>
|
|
477
|
+
</div>
|
|
478
|
+
<nav class="dash-nav">
|
|
479
|
+
<a href="/api/v1/docs">${icon('doc')}<span>API docs</span></a>
|
|
480
|
+
<a href="/api/v1/openapi.json">${icon('doc')}<span>openapi.json</span></a>
|
|
481
|
+
<a href="/api/v1/postman.json" download>${icon('doc')}<span>postman</span></a>
|
|
482
|
+
</nav>
|
|
483
|
+
</header>
|
|
484
|
+
<section class="grid-12">
|
|
485
|
+
<div class="col-4">${renderHealthPanel(m)}</div>
|
|
486
|
+
<div class="col-4">${renderRuntimePanel(m)}</div>
|
|
487
|
+
<div class="col-4">${renderProcessPanel(m)}</div>
|
|
488
|
+
<div class="col-4">${renderWorkloadPanel(m)}</div>
|
|
489
|
+
<div class="col-8">${renderWebsocketPanel(m, snap)}</div>
|
|
490
|
+
<div class="col-6">${renderEntitiesPanel(m)}</div>
|
|
491
|
+
<div class="col-6">${renderMapsPanel(m)}</div>
|
|
492
|
+
</section>
|
|
493
|
+
<footer class="dash-foot">
|
|
494
|
+
<span>POLL ${POLL_INTERVAL_MS} ms</span>
|
|
495
|
+
<span>RFC 9457 problem+json on error</span>
|
|
496
|
+
<span>v${(window.renderPayload && window.renderPayload.version) || '0.0.0'}</span>
|
|
497
|
+
</footer>
|
|
498
|
+
</main>
|
|
499
|
+
`;
|
|
500
|
+
|
|
501
|
+
setHTML('.dashboard-content', body);
|
|
502
|
+
rateUpdate(snap);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// ── Fetch loop ─────────────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
const renderError = (status, detail) => {
|
|
508
|
+
setHTML(
|
|
509
|
+
'.dashboard-content',
|
|
510
|
+
html`
|
|
511
|
+
<div class="dash-error">
|
|
512
|
+
<div class="dash-error-tag">${icon('alert')} HTTP ${status}</div>
|
|
513
|
+
<div class="dash-error-body">${detail}</div>
|
|
514
|
+
<div class="dash-error-foot">retrying in ${POLL_INTERVAL_MS / 1000}s</div>
|
|
515
|
+
</div>
|
|
516
|
+
`,
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const fetchMetrics = async () => {
|
|
521
|
+
try {
|
|
522
|
+
const res = await fetch('/api/v1/metrics', { headers: { Accept: 'application/json' } });
|
|
523
|
+
if (!res.ok) {
|
|
524
|
+
// RFC 9457: server returns problem+json with a stable `title`/`detail`.
|
|
525
|
+
let detail = res.statusText;
|
|
526
|
+
try {
|
|
527
|
+
const problem = await res.json();
|
|
528
|
+
detail = problem.detail || problem.title || detail;
|
|
529
|
+
} catch (_) {
|
|
530
|
+
// body wasn't JSON; ignore.
|
|
531
|
+
}
|
|
532
|
+
renderError(res.status, detail);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const metrics = await res.json();
|
|
536
|
+
renderDashboard(metrics);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
renderError('NET', String((err && err.message) || err));
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const main = () => {
|
|
543
|
+
append('body', iconSprite);
|
|
544
|
+
append(
|
|
545
|
+
'body',
|
|
546
|
+
html`
|
|
547
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
548
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
549
|
+
<link href="https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap" rel="stylesheet" />
|
|
550
|
+
`,
|
|
551
|
+
);
|
|
552
|
+
append(
|
|
553
|
+
'body',
|
|
554
|
+
html`
|
|
555
|
+
<style>
|
|
556
|
+
:root {
|
|
557
|
+
--bg: #0a0d12;
|
|
558
|
+
--bg-2: #11161e;
|
|
559
|
+
--bg-3: #161c25;
|
|
560
|
+
--fg: #d8dee9;
|
|
561
|
+
--fg-dim: #8a93a3;
|
|
562
|
+
--fg-muted: #5b6473;
|
|
563
|
+
--line: #2a3140;
|
|
564
|
+
--line-2: #1a1f2a;
|
|
565
|
+
--ok: #7ee787;
|
|
566
|
+
--warn: #f0b429;
|
|
567
|
+
--err: #ff6b6b;
|
|
568
|
+
--info: #c084fc;
|
|
569
|
+
--accent: #7dd3fc;
|
|
570
|
+
}
|
|
571
|
+
* {
|
|
572
|
+
box-sizing: border-box;
|
|
573
|
+
}
|
|
574
|
+
html,
|
|
575
|
+
body {
|
|
576
|
+
margin: 0;
|
|
577
|
+
padding: 0;
|
|
578
|
+
background: var(--bg);
|
|
579
|
+
color: var(--fg);
|
|
580
|
+
font-family: 'VT323', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
|
|
581
|
+
font-size: 18px;
|
|
582
|
+
line-height: 1.35;
|
|
583
|
+
letter-spacing: 0.5px;
|
|
584
|
+
min-height: 100vh;
|
|
585
|
+
image-rendering: pixelated;
|
|
586
|
+
-webkit-font-smoothing: none;
|
|
587
|
+
}
|
|
588
|
+
body::before {
|
|
589
|
+
content: '';
|
|
590
|
+
position: fixed;
|
|
591
|
+
inset: 0;
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
background-image: repeating-linear-gradient(
|
|
594
|
+
to bottom,
|
|
595
|
+
rgba(255, 255, 255, 0.03) 0,
|
|
596
|
+
rgba(255, 255, 255, 0.03) 1px,
|
|
597
|
+
transparent 1px,
|
|
598
|
+
transparent 3px
|
|
599
|
+
);
|
|
600
|
+
z-index: 1;
|
|
601
|
+
}
|
|
602
|
+
.icon {
|
|
603
|
+
width: 12px;
|
|
604
|
+
height: 12px;
|
|
605
|
+
fill: currentColor;
|
|
606
|
+
shape-rendering: crispEdges;
|
|
607
|
+
vertical-align: -1px;
|
|
608
|
+
margin-right: 6px;
|
|
609
|
+
}
|
|
610
|
+
a {
|
|
611
|
+
color: var(--accent);
|
|
612
|
+
text-decoration: none;
|
|
613
|
+
}
|
|
614
|
+
a:hover {
|
|
615
|
+
color: var(--fg);
|
|
616
|
+
}
|
|
617
|
+
.dash {
|
|
618
|
+
position: relative;
|
|
619
|
+
z-index: 2;
|
|
620
|
+
max-width: 1440px;
|
|
621
|
+
margin: 0 auto;
|
|
622
|
+
padding: 16px 20px 32px;
|
|
623
|
+
}
|
|
624
|
+
.dash-head {
|
|
625
|
+
display: flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
justify-content: space-between;
|
|
628
|
+
gap: 16px;
|
|
629
|
+
border: 2px solid var(--line);
|
|
630
|
+
background: var(--bg-2);
|
|
631
|
+
padding: 10px 16px;
|
|
632
|
+
margin-bottom: 16px;
|
|
633
|
+
}
|
|
634
|
+
.brand {
|
|
635
|
+
display: flex;
|
|
636
|
+
align-items: center;
|
|
637
|
+
gap: 10px;
|
|
638
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
639
|
+
font-size: 11px;
|
|
640
|
+
letter-spacing: 2px;
|
|
641
|
+
color: var(--accent);
|
|
642
|
+
}
|
|
643
|
+
.brand-glyph .icon {
|
|
644
|
+
width: 18px;
|
|
645
|
+
height: 18px;
|
|
646
|
+
margin: 0;
|
|
647
|
+
}
|
|
648
|
+
.dash-nav {
|
|
649
|
+
display: flex;
|
|
650
|
+
gap: 8px;
|
|
651
|
+
flex-wrap: wrap;
|
|
652
|
+
}
|
|
653
|
+
.dash-nav a {
|
|
654
|
+
display: inline-flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
padding: 4px 10px;
|
|
657
|
+
border: 2px solid var(--line);
|
|
658
|
+
background: var(--bg-3);
|
|
659
|
+
color: var(--fg);
|
|
660
|
+
font-size: 14px;
|
|
661
|
+
letter-spacing: 1px;
|
|
662
|
+
text-transform: uppercase;
|
|
663
|
+
}
|
|
664
|
+
.dash-nav a:hover {
|
|
665
|
+
border-color: var(--accent);
|
|
666
|
+
color: var(--accent);
|
|
667
|
+
}
|
|
668
|
+
.grid-12 {
|
|
669
|
+
display: grid;
|
|
670
|
+
grid-template-columns: repeat(12, 1fr);
|
|
671
|
+
gap: 12px;
|
|
672
|
+
}
|
|
673
|
+
.col-4 {
|
|
674
|
+
grid-column: span 4;
|
|
675
|
+
}
|
|
676
|
+
.col-6 {
|
|
677
|
+
grid-column: span 6;
|
|
678
|
+
}
|
|
679
|
+
.col-8 {
|
|
680
|
+
grid-column: span 8;
|
|
681
|
+
}
|
|
682
|
+
@media (max-width: 1100px) {
|
|
683
|
+
.col-4,
|
|
684
|
+
.col-6,
|
|
685
|
+
.col-8 {
|
|
686
|
+
grid-column: span 12;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
.panel {
|
|
690
|
+
border: 2px solid var(--line);
|
|
691
|
+
background: var(--bg-2);
|
|
692
|
+
display: flex;
|
|
693
|
+
flex-direction: column;
|
|
694
|
+
min-height: 100%;
|
|
695
|
+
}
|
|
696
|
+
.panel-head {
|
|
697
|
+
display: flex;
|
|
698
|
+
align-items: center;
|
|
699
|
+
justify-content: space-between;
|
|
700
|
+
padding: 8px 12px;
|
|
701
|
+
border-bottom: 2px solid var(--line);
|
|
702
|
+
background: var(--bg-3);
|
|
703
|
+
}
|
|
704
|
+
.panel-title {
|
|
705
|
+
margin: 0;
|
|
706
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
707
|
+
font-size: 9px;
|
|
708
|
+
letter-spacing: 2px;
|
|
709
|
+
color: var(--fg);
|
|
710
|
+
text-transform: uppercase;
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
}
|
|
714
|
+
.panel-meta {
|
|
715
|
+
font-size: 13px;
|
|
716
|
+
color: var(--fg-muted);
|
|
717
|
+
letter-spacing: 1px;
|
|
718
|
+
}
|
|
719
|
+
.panel-body {
|
|
720
|
+
padding: 12px;
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-direction: column;
|
|
723
|
+
gap: 12px;
|
|
724
|
+
flex: 1;
|
|
725
|
+
}
|
|
726
|
+
.kv {
|
|
727
|
+
display: flex;
|
|
728
|
+
justify-content: space-between;
|
|
729
|
+
padding: 4px 6px;
|
|
730
|
+
background: var(--bg-3);
|
|
731
|
+
border: 1px solid var(--line-2);
|
|
732
|
+
}
|
|
733
|
+
.kv-label {
|
|
734
|
+
color: var(--fg-dim);
|
|
735
|
+
font-size: 14px;
|
|
736
|
+
text-transform: uppercase;
|
|
737
|
+
letter-spacing: 1px;
|
|
738
|
+
}
|
|
739
|
+
.kv-value {
|
|
740
|
+
color: var(--fg);
|
|
741
|
+
font-weight: 700;
|
|
742
|
+
font-size: 16px;
|
|
743
|
+
}
|
|
744
|
+
.grid-2 {
|
|
745
|
+
display: grid;
|
|
746
|
+
grid-template-columns: 1fr 1fr;
|
|
747
|
+
gap: 6px;
|
|
748
|
+
}
|
|
749
|
+
.grid-3 {
|
|
750
|
+
display: grid;
|
|
751
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
752
|
+
gap: 6px;
|
|
753
|
+
}
|
|
754
|
+
.meter {
|
|
755
|
+
background: var(--bg-3);
|
|
756
|
+
border: 1px solid var(--line-2);
|
|
757
|
+
padding: 6px 8px;
|
|
758
|
+
}
|
|
759
|
+
.meter-head,
|
|
760
|
+
.meter-foot {
|
|
761
|
+
display: flex;
|
|
762
|
+
justify-content: space-between;
|
|
763
|
+
font-size: 14px;
|
|
764
|
+
}
|
|
765
|
+
.meter-label {
|
|
766
|
+
color: var(--fg-dim);
|
|
767
|
+
text-transform: uppercase;
|
|
768
|
+
letter-spacing: 1px;
|
|
769
|
+
}
|
|
770
|
+
.meter-value {
|
|
771
|
+
font-weight: 700;
|
|
772
|
+
}
|
|
773
|
+
.meter-bar {
|
|
774
|
+
height: 10px;
|
|
775
|
+
margin: 4px 0;
|
|
776
|
+
background: var(--bg);
|
|
777
|
+
border: 1px solid var(--line);
|
|
778
|
+
position: relative;
|
|
779
|
+
overflow: hidden;
|
|
780
|
+
}
|
|
781
|
+
.meter-fill {
|
|
782
|
+
height: 100%;
|
|
783
|
+
background-image: repeating-linear-gradient(
|
|
784
|
+
45deg,
|
|
785
|
+
currentColor 0,
|
|
786
|
+
currentColor 4px,
|
|
787
|
+
rgba(0, 0, 0, 0.25) 4px,
|
|
788
|
+
rgba(0, 0, 0, 0.25) 8px
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
.meter-foot {
|
|
792
|
+
color: var(--fg-muted);
|
|
793
|
+
font-size: 13px;
|
|
794
|
+
}
|
|
795
|
+
.grid-table {
|
|
796
|
+
width: 100%;
|
|
797
|
+
border-collapse: collapse;
|
|
798
|
+
font-size: 15px;
|
|
799
|
+
}
|
|
800
|
+
.grid-table th,
|
|
801
|
+
.grid-table td {
|
|
802
|
+
padding: 4px 6px;
|
|
803
|
+
text-align: right;
|
|
804
|
+
border-bottom: 1px solid var(--line-2);
|
|
805
|
+
}
|
|
806
|
+
.grid-table thead th {
|
|
807
|
+
color: var(--fg-muted);
|
|
808
|
+
text-align: right;
|
|
809
|
+
text-transform: uppercase;
|
|
810
|
+
letter-spacing: 1px;
|
|
811
|
+
font-size: 12px;
|
|
812
|
+
border-bottom: 2px solid var(--line);
|
|
813
|
+
}
|
|
814
|
+
.grid-table tbody th {
|
|
815
|
+
text-align: left;
|
|
816
|
+
color: var(--fg-dim);
|
|
817
|
+
font-weight: 400;
|
|
818
|
+
text-transform: capitalize;
|
|
819
|
+
}
|
|
820
|
+
.grid-table.dense th,
|
|
821
|
+
.grid-table.dense td {
|
|
822
|
+
padding: 3px 6px;
|
|
823
|
+
font-size: 14px;
|
|
824
|
+
}
|
|
825
|
+
.health-block {
|
|
826
|
+
display: flex;
|
|
827
|
+
align-items: center;
|
|
828
|
+
gap: 14px;
|
|
829
|
+
padding: 8px 4px;
|
|
830
|
+
}
|
|
831
|
+
.health-dot {
|
|
832
|
+
width: 16px;
|
|
833
|
+
height: 16px;
|
|
834
|
+
border: 2px solid currentColor;
|
|
835
|
+
background: currentColor;
|
|
836
|
+
flex: 0 0 16px;
|
|
837
|
+
animation: blink 1.6s steps(2, end) infinite;
|
|
838
|
+
}
|
|
839
|
+
@keyframes blink {
|
|
840
|
+
0%,
|
|
841
|
+
60% {
|
|
842
|
+
opacity: 1;
|
|
843
|
+
}
|
|
844
|
+
70%,
|
|
845
|
+
100% {
|
|
846
|
+
opacity: 0.35;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
.health-status {
|
|
850
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
851
|
+
font-size: 14px;
|
|
852
|
+
letter-spacing: 2px;
|
|
853
|
+
}
|
|
854
|
+
.health-desc {
|
|
855
|
+
font-size: 14px;
|
|
856
|
+
color: var(--fg-dim);
|
|
857
|
+
margin-top: 4px;
|
|
858
|
+
}
|
|
859
|
+
.load-readout {
|
|
860
|
+
display: flex;
|
|
861
|
+
align-items: baseline;
|
|
862
|
+
gap: 12px;
|
|
863
|
+
padding: 4px 6px;
|
|
864
|
+
background: var(--bg-3);
|
|
865
|
+
border: 1px solid var(--line-2);
|
|
866
|
+
}
|
|
867
|
+
.load-pct {
|
|
868
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
869
|
+
font-size: 22px;
|
|
870
|
+
letter-spacing: 1px;
|
|
871
|
+
}
|
|
872
|
+
.load-tag {
|
|
873
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
874
|
+
font-size: 9px;
|
|
875
|
+
letter-spacing: 2px;
|
|
876
|
+
padding: 2px 6px;
|
|
877
|
+
border: 2px solid currentColor;
|
|
878
|
+
}
|
|
879
|
+
.dash-foot {
|
|
880
|
+
margin-top: 16px;
|
|
881
|
+
padding: 8px 16px;
|
|
882
|
+
border: 2px solid var(--line);
|
|
883
|
+
background: var(--bg-2);
|
|
884
|
+
display: flex;
|
|
885
|
+
justify-content: space-between;
|
|
886
|
+
font-size: 14px;
|
|
887
|
+
color: var(--fg-muted);
|
|
888
|
+
letter-spacing: 1px;
|
|
889
|
+
}
|
|
890
|
+
.dash-error {
|
|
891
|
+
margin: 80px auto;
|
|
892
|
+
max-width: 520px;
|
|
893
|
+
border: 2px solid var(--err);
|
|
894
|
+
background: var(--bg-2);
|
|
895
|
+
color: var(--err);
|
|
896
|
+
padding: 24px;
|
|
897
|
+
text-align: center;
|
|
898
|
+
}
|
|
899
|
+
.dash-error-tag {
|
|
900
|
+
font-family: 'Press Start 2P', 'VT323', monospace;
|
|
901
|
+
font-size: 12px;
|
|
902
|
+
letter-spacing: 2px;
|
|
903
|
+
margin-bottom: 12px;
|
|
904
|
+
}
|
|
905
|
+
.dash-error-body {
|
|
906
|
+
color: var(--fg);
|
|
907
|
+
font-size: 16px;
|
|
908
|
+
}
|
|
909
|
+
.dash-error-foot {
|
|
910
|
+
color: var(--fg-muted);
|
|
911
|
+
font-size: 13px;
|
|
912
|
+
margin-top: 12px;
|
|
913
|
+
}
|
|
914
|
+
.muted {
|
|
915
|
+
color: var(--fg-muted);
|
|
916
|
+
}
|
|
917
|
+
.center {
|
|
918
|
+
text-align: center;
|
|
919
|
+
}
|
|
920
|
+
.tone-ok {
|
|
921
|
+
color: var(--ok);
|
|
922
|
+
}
|
|
923
|
+
.tone-warn {
|
|
924
|
+
color: var(--warn);
|
|
925
|
+
}
|
|
926
|
+
.tone-err {
|
|
927
|
+
color: var(--err);
|
|
928
|
+
}
|
|
929
|
+
.tone-info {
|
|
930
|
+
color: var(--info);
|
|
931
|
+
}
|
|
932
|
+
.tone-muted {
|
|
933
|
+
color: var(--fg-muted);
|
|
934
|
+
}
|
|
935
|
+
</style>
|
|
936
|
+
<div class="dashboard-content"></div>
|
|
937
|
+
`,
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
fetchMetrics();
|
|
941
|
+
setInterval(fetchMetrics, POLL_INTERVAL_MS);
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
SrrComponent = () =>
|
|
945
|
+
html`<script>
|
|
946
|
+
{
|
|
947
|
+
const POLL_INTERVAL_MS = ${POLL_INTERVAL_MS};
|
|
948
|
+
const s = ${s};
|
|
949
|
+
const sa = ${sa};
|
|
950
|
+
const append = ${append};
|
|
951
|
+
const setHTML = ${setHTML};
|
|
952
|
+
const pad = ${pad};
|
|
953
|
+
const formatUptime = ${formatUptime};
|
|
954
|
+
const formatNumber = ${formatNumber};
|
|
955
|
+
const formatBytes = ${formatBytes};
|
|
956
|
+
const formatRate = ${formatRate};
|
|
957
|
+
const formatPercent = ${formatPercent};
|
|
958
|
+
const HEALTH_TONE = ${JSON.stringify(HEALTH_TONE)};
|
|
959
|
+
const LOAD_TONE = ${JSON.stringify(LOAD_TONE)};
|
|
960
|
+
const toneClass = ${toneClass};
|
|
961
|
+
const rateState = { prev: null, prevTs: 0 };
|
|
962
|
+
const rateDiff = ${rateDiff};
|
|
963
|
+
const rateUpdate = ${rateUpdate};
|
|
964
|
+
const iconSprite = ${JSON.stringify(iconSprite)};
|
|
965
|
+
const icon = ${icon};
|
|
966
|
+
const kv = ${kv};
|
|
967
|
+
const meter = ${meter};
|
|
968
|
+
const panel = ${panel};
|
|
969
|
+
const renderHealthPanel = ${renderHealthPanel};
|
|
970
|
+
const renderRuntimePanel = ${renderRuntimePanel};
|
|
971
|
+
const renderProcessPanel = ${renderProcessPanel};
|
|
972
|
+
const renderWorkloadPanel = ${renderWorkloadPanel};
|
|
973
|
+
const renderWebsocketPanel = ${renderWebsocketPanel};
|
|
974
|
+
const renderEntitiesPanel = ${renderEntitiesPanel};
|
|
975
|
+
const renderMapsPanel = ${renderMapsPanel};
|
|
976
|
+
const renderDashboard = ${renderDashboard};
|
|
977
|
+
const renderError = ${renderError};
|
|
978
|
+
const fetchMetrics = ${fetchMetrics};
|
|
979
|
+
const main = ${main};
|
|
980
|
+
window.onload = main;
|
|
981
|
+
}
|
|
982
|
+
</script>`;
|