From 7795a1d8adef7bf765bf1b1eaf781a3834c9aebf Mon Sep 17 00:00:00 2001 From: Chris Zerafa Date: Tue, 14 Jul 2026 22:17:52 -0400 Subject: [PATCH] Import current Voyager console --- README.md | 27 ++ config/services.json | 121 +++++++ config/services.json.backup-20260714-215555 | 121 +++++++ css/voyager.css | 336 ++++++++++++++++++++ css/voyager.css.backup-20260714-215555 | 336 ++++++++++++++++++++ index.html | 131 ++++++++ index.html.backup | 238 ++++++++++++++ index.html.backup-20260714-215555 | 93 ++++++ js/voyager.js | 53 +++ js/voyager.js.backup-20260714-215555 | 53 +++ 10 files changed, 1509 insertions(+) create mode 100644 README.md create mode 100644 config/services.json create mode 100644 config/services.json.backup-20260714-215555 create mode 100644 css/voyager.css create mode 100644 css/voyager.css.backup-20260714-215555 create mode 100644 index.html create mode 100644 index.html.backup create mode 100644 index.html.backup-20260714-215555 create mode 100644 js/voyager.js create mode 100644 js/voyager.js.backup-20260714-215555 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6552695 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# VOYAGER · Edge01 + +Static infrastructure console served by Caddy. + +## Deploy + +Copy the project contents into: + +`/opt/docker/caddy/site/portal/` + +Caddy already serves that host directory at: + +`https://portal.zerafa.com` + +No Caddy restart is required for HTML, CSS, JavaScript, or JSON changes. + +## Structure + +- `index.html` +- `css/voyager.css` +- `js/voyager.js` +- `config/services.json` + +## Design principle + +Voyager does not show a single global “everything is healthy” message. +Each system has its own independent state tile so the operator can sweep the page and spot warnings or failures. diff --git a/config/services.json b/config/services.json new file mode 100644 index 0000000..76d0807 --- /dev/null +++ b/config/services.json @@ -0,0 +1,121 @@ +{ + "status": [ + { + "name": "Edge01", + "state": "online", + "label": "Online", + "value": "Reachable", + "detail": "DreamHost VPS" + }, + { + "name": "Docker", + "state": "online", + "label": "Online", + "value": "Running", + "detail": "Container engine" + }, + { + "name": "Portainer", + "state": "online", + "label": "Online", + "value": "Available", + "detail": "portainer.zerafa.com" + }, + { + "name": "Gitea", + "state": "online", + "label": "Online", + "value": "Available", + "detail": "git.zerafa.com" + }, + { + "name": "TLS", + "state": "online", + "label": "Valid", + "value": "Active", + "detail": "Caddy certificates" + }, + { + "name": "Backups", + "state": "warning", + "label": "Review", + "value": "Pending", + "detail": "Restic + B2" + }, + { + "name": "Tunnel", + "state": "warning", + "label": "Pending", + "value": "Not built", + "detail": "Home network access" + }, + { + "name": "Monitoring", + "state": "warning", + "label": "Planned", + "value": "Next phase", + "detail": "Uptime Kuma" + } + ], + "services": [ + { + "name": "Portainer", + "icon": "P", + "description": "Docker containers, stacks, networks and volumes.", + "url": "https://portainer.zerafa.com", + "category": "Docker" + }, + { + "name": "Gitea", + "icon": "G", + "description": "Infrastructure repositories and documentation.", + "url": "https://git.zerafa.com", + "category": "Git" + }, + { + "name": "Edge01", + "icon": "E", + "description": "Primary VPS landing page and public edge host.", + "url": "https://edge01.zerafa.com", + "category": "Server" + } + ], + "home": [ + { + "name": "Home Assistant", + "icon": "HA", + "description": "Automation dashboards, devices and integrations.", + "category": "Automation" + }, + { + "name": "Proxmox", + "icon": "PX", + "description": "Cluster, virtual machines and containers.", + "category": "Virtualization" + }, + { + "name": "UniFi", + "icon": "U", + "description": "Gateways, switches, access points and clients.", + "category": "Network" + }, + { + "name": "Pi-hole", + "icon": "PH", + "description": "DNS filtering and resolver statistics.", + "category": "DNS" + }, + { + "name": "ADS-B", + "icon": "AD", + "description": "Aircraft tracking and feeder status.", + "category": "Radio" + }, + { + "name": "NAS", + "icon": "NAS", + "description": "Storage, backups and private file services.", + "category": "Storage" + } + ] +} \ No newline at end of file diff --git a/config/services.json.backup-20260714-215555 b/config/services.json.backup-20260714-215555 new file mode 100644 index 0000000..76d0807 --- /dev/null +++ b/config/services.json.backup-20260714-215555 @@ -0,0 +1,121 @@ +{ + "status": [ + { + "name": "Edge01", + "state": "online", + "label": "Online", + "value": "Reachable", + "detail": "DreamHost VPS" + }, + { + "name": "Docker", + "state": "online", + "label": "Online", + "value": "Running", + "detail": "Container engine" + }, + { + "name": "Portainer", + "state": "online", + "label": "Online", + "value": "Available", + "detail": "portainer.zerafa.com" + }, + { + "name": "Gitea", + "state": "online", + "label": "Online", + "value": "Available", + "detail": "git.zerafa.com" + }, + { + "name": "TLS", + "state": "online", + "label": "Valid", + "value": "Active", + "detail": "Caddy certificates" + }, + { + "name": "Backups", + "state": "warning", + "label": "Review", + "value": "Pending", + "detail": "Restic + B2" + }, + { + "name": "Tunnel", + "state": "warning", + "label": "Pending", + "value": "Not built", + "detail": "Home network access" + }, + { + "name": "Monitoring", + "state": "warning", + "label": "Planned", + "value": "Next phase", + "detail": "Uptime Kuma" + } + ], + "services": [ + { + "name": "Portainer", + "icon": "P", + "description": "Docker containers, stacks, networks and volumes.", + "url": "https://portainer.zerafa.com", + "category": "Docker" + }, + { + "name": "Gitea", + "icon": "G", + "description": "Infrastructure repositories and documentation.", + "url": "https://git.zerafa.com", + "category": "Git" + }, + { + "name": "Edge01", + "icon": "E", + "description": "Primary VPS landing page and public edge host.", + "url": "https://edge01.zerafa.com", + "category": "Server" + } + ], + "home": [ + { + "name": "Home Assistant", + "icon": "HA", + "description": "Automation dashboards, devices and integrations.", + "category": "Automation" + }, + { + "name": "Proxmox", + "icon": "PX", + "description": "Cluster, virtual machines and containers.", + "category": "Virtualization" + }, + { + "name": "UniFi", + "icon": "U", + "description": "Gateways, switches, access points and clients.", + "category": "Network" + }, + { + "name": "Pi-hole", + "icon": "PH", + "description": "DNS filtering and resolver statistics.", + "category": "DNS" + }, + { + "name": "ADS-B", + "icon": "AD", + "description": "Aircraft tracking and feeder status.", + "category": "Radio" + }, + { + "name": "NAS", + "icon": "NAS", + "description": "Storage, backups and private file services.", + "category": "Storage" + } + ] +} \ No newline at end of file diff --git a/css/voyager.css b/css/voyager.css new file mode 100644 index 0000000..2c1189c --- /dev/null +++ b/css/voyager.css @@ -0,0 +1,336 @@ +:root { + color-scheme: dark; + --bg: #090d14; + --sidebar: #0d131d; + --panel: #131b27; + --panel-hover: #192434; + --text: #f4f6f8; + --muted: #98a5b5; + --line: #263244; + --accent: #67d7c4; + --online: #45d483; + --warning: #e3b653; + --offline: #ff6b6b; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +a { color: inherit; } + +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 250px 1fr; +} + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + padding: 24px 18px; + border-right: 1px solid var(--line); + background: var(--sidebar); + display: flex; + flex-direction: column; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 6px 28px; +} + +.brand-mark { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 10px; + background: linear-gradient(145deg, rgba(103,215,196,.18), rgba(255,255,255,.03)); + font-weight: 900; + color: var(--accent); +} + +.brand-name { + font-size: .95rem; + font-weight: 900; + letter-spacing: .14em; +} + +.brand-node { + margin-top: 3px; + color: var(--muted); + font-size: .82rem; +} + +.nav { + display: grid; + gap: 6px; +} + +.nav-link { + padding: 11px 12px; + border-radius: 9px; + color: var(--muted); + text-decoration: none; + font-size: .92rem; +} + +.nav-link:hover, +.nav-link.active { + background: rgba(103,215,196,.08); + color: var(--text); +} + +.sidebar-footer { + margin-top: auto; + padding: 12px; + color: var(--muted); + font-size: .8rem; + display: flex; + align-items: center; + gap: 8px; +} + +.main { + width: min(1240px, calc(100% - 48px)); + margin: 0 auto; + padding: 36px 0 48px; +} + +.topbar { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: end; + padding-bottom: 28px; + border-bottom: 1px solid var(--line); +} + +.eyebrow, +.section-kicker { + margin: 0 0 8px; + color: var(--accent); + font-size: .72rem; + letter-spacing: .14em; + text-transform: uppercase; + font-weight: 800; +} + +h1 { + margin: 0; + font-size: clamp(2.6rem, 6vw, 5.6rem); + line-height: .92; + letter-spacing: -.055em; +} + +.node-label { + margin: 10px 0 0; + color: var(--muted); + font-size: 1.1rem; +} + +.topbar-meta { + display: grid; + gap: 5px; + color: var(--muted); + text-align: right; + font-size: .86rem; +} + +.section { + padding-top: 34px; +} + +.section-heading { + display: flex; + justify-content: space-between; + align-items: end; + gap: 20px; + margin-bottom: 15px; +} + +.section-heading h2 { + margin: 0; + font-size: 1.3rem; +} + +.section-heading > p { + margin: 0; + color: var(--muted); + font-size: .86rem; +} + +.status-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.status-card, +.service-card { + border: 1px solid var(--line); + border-radius: 14px; + background: var(--panel); +} + +.status-card { + min-height: 130px; + padding: 18px; +} + +.status-top { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.status-name { + font-weight: 750; +} + +.status-state { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: .78rem; + font-weight: 750; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + display: inline-block; +} + +.online { color: var(--online); } +.warning { color: var(--warning); } +.offline { color: var(--offline); } + +.status-value { + margin-top: 28px; + font-size: 1.5rem; + font-weight: 800; +} + +.status-detail { + margin-top: 6px; + color: var(--muted); + font-size: .8rem; +} + +.service-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.service-card { + min-height: 160px; + padding: 18px; + position: relative; + text-decoration: none; + transition: transform .15s ease, border-color .15s ease, background .15s ease; +} + +.service-card:hover { + transform: translateY(-3px); + border-color: var(--accent); + background: var(--panel-hover); +} + +.service-card.disabled { + opacity: .5; + pointer-events: none; +} + +.service-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 9px; + color: var(--accent); + font-weight: 900; + font-size: .82rem; +} + +.service-card h3 { + margin: 18px 0 6px; + font-size: 1.05rem; +} + +.service-card p { + margin: 0; + color: var(--muted); + font-size: .87rem; + line-height: 1.5; +} + +.service-foot { + position: absolute; + left: 18px; + right: 18px; + bottom: 14px; + display: flex; + justify-content: space-between; + color: var(--muted); + font-size: .72rem; + text-transform: uppercase; + letter-spacing: .08em; +} + +footer { + margin-top: 38px; + padding-top: 18px; + border-top: 1px solid var(--line); + display: flex; + justify-content: space-between; + gap: 16px; + color: var(--muted); + font-size: .8rem; +} + +@media (max-width: 1050px) { + .status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .service-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 760px) { + .app-shell { grid-template-columns: 1fr; } + .sidebar { + position: static; + width: auto; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--line); + } + .nav { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .sidebar-footer { display: none; } + .main { width: min(100% - 24px, 1240px); padding-top: 24px; } + .topbar, + .section-heading { align-items: flex-start; flex-direction: column; } + .topbar-meta { text-align: left; } +} + +@media (max-width: 560px) { + .status-grid, + .service-grid { grid-template-columns: 1fr; } + .nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} diff --git a/css/voyager.css.backup-20260714-215555 b/css/voyager.css.backup-20260714-215555 new file mode 100644 index 0000000..2c1189c --- /dev/null +++ b/css/voyager.css.backup-20260714-215555 @@ -0,0 +1,336 @@ +:root { + color-scheme: dark; + --bg: #090d14; + --sidebar: #0d131d; + --panel: #131b27; + --panel-hover: #192434; + --text: #f4f6f8; + --muted: #98a5b5; + --line: #263244; + --accent: #67d7c4; + --online: #45d483; + --warning: #e3b653; + --offline: #ff6b6b; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +a { color: inherit; } + +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 250px 1fr; +} + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + padding: 24px 18px; + border-right: 1px solid var(--line); + background: var(--sidebar); + display: flex; + flex-direction: column; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 6px 28px; +} + +.brand-mark { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 10px; + background: linear-gradient(145deg, rgba(103,215,196,.18), rgba(255,255,255,.03)); + font-weight: 900; + color: var(--accent); +} + +.brand-name { + font-size: .95rem; + font-weight: 900; + letter-spacing: .14em; +} + +.brand-node { + margin-top: 3px; + color: var(--muted); + font-size: .82rem; +} + +.nav { + display: grid; + gap: 6px; +} + +.nav-link { + padding: 11px 12px; + border-radius: 9px; + color: var(--muted); + text-decoration: none; + font-size: .92rem; +} + +.nav-link:hover, +.nav-link.active { + background: rgba(103,215,196,.08); + color: var(--text); +} + +.sidebar-footer { + margin-top: auto; + padding: 12px; + color: var(--muted); + font-size: .8rem; + display: flex; + align-items: center; + gap: 8px; +} + +.main { + width: min(1240px, calc(100% - 48px)); + margin: 0 auto; + padding: 36px 0 48px; +} + +.topbar { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: end; + padding-bottom: 28px; + border-bottom: 1px solid var(--line); +} + +.eyebrow, +.section-kicker { + margin: 0 0 8px; + color: var(--accent); + font-size: .72rem; + letter-spacing: .14em; + text-transform: uppercase; + font-weight: 800; +} + +h1 { + margin: 0; + font-size: clamp(2.6rem, 6vw, 5.6rem); + line-height: .92; + letter-spacing: -.055em; +} + +.node-label { + margin: 10px 0 0; + color: var(--muted); + font-size: 1.1rem; +} + +.topbar-meta { + display: grid; + gap: 5px; + color: var(--muted); + text-align: right; + font-size: .86rem; +} + +.section { + padding-top: 34px; +} + +.section-heading { + display: flex; + justify-content: space-between; + align-items: end; + gap: 20px; + margin-bottom: 15px; +} + +.section-heading h2 { + margin: 0; + font-size: 1.3rem; +} + +.section-heading > p { + margin: 0; + color: var(--muted); + font-size: .86rem; +} + +.status-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.status-card, +.service-card { + border: 1px solid var(--line); + border-radius: 14px; + background: var(--panel); +} + +.status-card { + min-height: 130px; + padding: 18px; +} + +.status-top { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.status-name { + font-weight: 750; +} + +.status-state { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: .78rem; + font-weight: 750; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + display: inline-block; +} + +.online { color: var(--online); } +.warning { color: var(--warning); } +.offline { color: var(--offline); } + +.status-value { + margin-top: 28px; + font-size: 1.5rem; + font-weight: 800; +} + +.status-detail { + margin-top: 6px; + color: var(--muted); + font-size: .8rem; +} + +.service-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.service-card { + min-height: 160px; + padding: 18px; + position: relative; + text-decoration: none; + transition: transform .15s ease, border-color .15s ease, background .15s ease; +} + +.service-card:hover { + transform: translateY(-3px); + border-color: var(--accent); + background: var(--panel-hover); +} + +.service-card.disabled { + opacity: .5; + pointer-events: none; +} + +.service-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 9px; + color: var(--accent); + font-weight: 900; + font-size: .82rem; +} + +.service-card h3 { + margin: 18px 0 6px; + font-size: 1.05rem; +} + +.service-card p { + margin: 0; + color: var(--muted); + font-size: .87rem; + line-height: 1.5; +} + +.service-foot { + position: absolute; + left: 18px; + right: 18px; + bottom: 14px; + display: flex; + justify-content: space-between; + color: var(--muted); + font-size: .72rem; + text-transform: uppercase; + letter-spacing: .08em; +} + +footer { + margin-top: 38px; + padding-top: 18px; + border-top: 1px solid var(--line); + display: flex; + justify-content: space-between; + gap: 16px; + color: var(--muted); + font-size: .8rem; +} + +@media (max-width: 1050px) { + .status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .service-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 760px) { + .app-shell { grid-template-columns: 1fr; } + .sidebar { + position: static; + width: auto; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--line); + } + .nav { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .sidebar-footer { display: none; } + .main { width: min(100% - 24px, 1240px); padding-top: 24px; } + .topbar, + .section-heading { align-items: flex-start; flex-direction: column; } + .topbar-meta { text-align: left; } +} + +@media (max-width: 560px) { + .status-grid, + .service-grid { grid-template-columns: 1fr; } + .nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0e54f52 --- /dev/null +++ b/index.html @@ -0,0 +1,131 @@ + + + + + + VOYAGER · Edge01 + + + + +
+ + +
+
+
+

Infrastructure Operations Console

+

VOYAGER

+

Edge01 · DreamHost VPS

+
+
+ Debian 13 + 208.113.210.193 +
+
+ +
+
+
+

Overview

+

System sweep

+
+

Each system reports independently.

+
+
+
+ +
+
+
+

Mail Security

+

Gateway operations

+
+

Waiting for telemetry…

+
+ +
+ +
+
+
+
+

Traffic

+

Today

+
+ Postfix +
+
+
+ + +
+
+ +
+
+
+

Applications

+

Quick launch

+
+

Public services hosted on Edge01.

+
+
+
+ +
+
+
+

Private Network

+

Home systems

+
+

These activate after the secure tunnel is built.

+
+
+
+ +
+ VOYAGER · Edge01 + Operations Console v0.3.0 +
+
+
+ + + + diff --git a/index.html.backup b/index.html.backup new file mode 100644 index 0000000..036dc71 --- /dev/null +++ b/index.html.backup @@ -0,0 +1,238 @@ + + + + + + Zerafa Infrastructure Portal + + + + +
+
+
+

Operations Portal

+

Zerafa
Infrastructure

+

VPS services, home lab systems and project tools.

+
+
Portal Online
+
+ +
+

VPS Services

+ +
+ +
+

Home Lab

+
+
+

Home Assistant

+

Home automation dashboards, devices and integrations.

+ Tunnel Pending +
+ +
+

Proxmox

+

Virtual machines, containers and cluster management.

+ Tunnel Pending +
+ +
+

UniFi

+

Network gateways, switches, access points and clients.

+ Tunnel Pending +
+ +
+

Pi-hole

+

DNS filtering, query statistics and resolver status.

+ Tunnel Pending +
+ +
+

ADS-B

+

Aircraft tracking, receiver statistics and feeder links.

+ Tunnel Pending +
+ +
+

Monitoring

+

Service uptime, alerts and infrastructure health.

+ Coming Soon +
+
+
+ +
portal.zerafa.com
+
+ + diff --git a/index.html.backup-20260714-215555 b/index.html.backup-20260714-215555 new file mode 100644 index 0000000..3685621 --- /dev/null +++ b/index.html.backup-20260714-215555 @@ -0,0 +1,93 @@ + + + + + + VOYAGER · Edge01 + + + + +
+ + +
+
+
+

Infrastructure Console

+

VOYAGER

+

Edge01

+
+
+ DreamHost VPS + Debian 13 +
+
+ +
+
+
+

Overview

+

System sweep

+
+

Each system reports independently.

+
+ +
+
+ +
+
+
+

Services

+

Quick launch

+
+

Public services hosted on Edge01.

+
+ +
+
+ +
+
+
+

Private Network

+

Home lab

+
+

These activate after the secure tunnel is built.

+
+ +
+
+ +
+ VOYAGER · Edge01 + Infrastructure Console v0.2.0 +
+
+
+ + + + diff --git a/js/voyager.js b/js/voyager.js new file mode 100644 index 0000000..528edb1 --- /dev/null +++ b/js/voyager.js @@ -0,0 +1,53 @@ +async function loadConfig() { + const response = await fetch('config/services.json', { cache: 'no-store' }); + if (!response.ok) throw new Error(`Config load failed: ${response.status}`); + return response.json(); +} + +function renderStatus(items) { + const grid = document.getElementById('status-grid'); + grid.innerHTML = items.map(item => ` +
+
+ ${item.name} + + ${item.label} + +
+
${item.value}
+
${item.detail}
+
+ `).join(''); +} + +function renderServices(targetId, items, disabled = false) { + const grid = document.getElementById(targetId); + grid.innerHTML = items.map(item => { + const classes = `service-card${disabled ? ' disabled' : ''}`; + const tag = disabled ? 'div' : 'a'; + const href = disabled ? '' : ` href="${item.url}"`; + return ` + <${tag} class="${classes}"${href}> +
${item.icon}
+

${item.name}

+

${item.description}

+
+ ${item.category} + ${disabled ? 'Pending' : 'Open ↗'} +
+ + `; + }).join(''); +} + +loadConfig() + .then(config => { + renderStatus(config.status); + renderServices('service-grid', config.services); + renderServices('home-grid', config.home, true); + }) + .catch(error => { + console.error(error); + document.getElementById('status-grid').innerHTML = + '
Configuration
Unavailable
Could not load services.json
'; + }); diff --git a/js/voyager.js.backup-20260714-215555 b/js/voyager.js.backup-20260714-215555 new file mode 100644 index 0000000..528edb1 --- /dev/null +++ b/js/voyager.js.backup-20260714-215555 @@ -0,0 +1,53 @@ +async function loadConfig() { + const response = await fetch('config/services.json', { cache: 'no-store' }); + if (!response.ok) throw new Error(`Config load failed: ${response.status}`); + return response.json(); +} + +function renderStatus(items) { + const grid = document.getElementById('status-grid'); + grid.innerHTML = items.map(item => ` +
+
+ ${item.name} + + ${item.label} + +
+
${item.value}
+
${item.detail}
+
+ `).join(''); +} + +function renderServices(targetId, items, disabled = false) { + const grid = document.getElementById(targetId); + grid.innerHTML = items.map(item => { + const classes = `service-card${disabled ? ' disabled' : ''}`; + const tag = disabled ? 'div' : 'a'; + const href = disabled ? '' : ` href="${item.url}"`; + return ` + <${tag} class="${classes}"${href}> +
${item.icon}
+

${item.name}

+

${item.description}

+
+ ${item.category} + ${disabled ? 'Pending' : 'Open ↗'} +
+ + `; + }).join(''); +} + +loadConfig() + .then(config => { + renderStatus(config.status); + renderServices('service-grid', config.services); + renderServices('home-grid', config.home, true); + }) + .catch(error => { + console.error(error); + document.getElementById('status-grid').innerHTML = + '
Configuration
Unavailable
Could not load services.json
'; + });