Import current Voyager console
This commit is contained in:
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+336
@@ -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)); }
|
||||||
|
}
|
||||||
@@ -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)); }
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VOYAGER · Edge01</title>
|
||||||
|
<meta name="description" content="Voyager operations console for Edge01 and connected systems.">
|
||||||
|
<link rel="stylesheet" href="css/voyager.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-mark">V</div>
|
||||||
|
<div>
|
||||||
|
<div class="brand-name">VOYAGER</div>
|
||||||
|
<div class="brand-node">Edge01 Operations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a class="nav-link active" href="#overview">Overview</a>
|
||||||
|
<a class="nav-link" href="#mail">Mail</a>
|
||||||
|
<a class="nav-link" href="#services">Applications</a>
|
||||||
|
<a class="nav-link" href="#home-lab">Private</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span class="status-dot online"></span>
|
||||||
|
voyager.zerafa.com
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="topbar">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Infrastructure Operations Console</p>
|
||||||
|
<h1>VOYAGER</h1>
|
||||||
|
<p class="node-label">Edge01 · DreamHost VPS</p>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-meta">
|
||||||
|
<span>Debian 13</span>
|
||||||
|
<span>208.113.210.193</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="overview" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Overview</p>
|
||||||
|
<h2>System sweep</h2>
|
||||||
|
</div>
|
||||||
|
<p>Each system reports independently.</p>
|
||||||
|
</div>
|
||||||
|
<div id="status-grid" class="status-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="mail" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Mail Security</p>
|
||||||
|
<h2>Gateway operations</h2>
|
||||||
|
</div>
|
||||||
|
<p id="mail-updated">Waiting for telemetry…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mail-health-grid" class="status-grid"></div>
|
||||||
|
|
||||||
|
<div class="mail-layout">
|
||||||
|
<article class="operations-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Traffic</p>
|
||||||
|
<h3>Today</h3>
|
||||||
|
</div>
|
||||||
|
<span class="panel-tag">Postfix</span>
|
||||||
|
</div>
|
||||||
|
<div id="mail-metrics" class="metric-grid"></div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="operations-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Security</p>
|
||||||
|
<h3>Gateway details</h3>
|
||||||
|
</div>
|
||||||
|
<span class="panel-tag">MX</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mail-details" class="detail-list"></div>
|
||||||
|
|
||||||
|
<a class="panel-action" href="https://rspamd.zerafa.com">
|
||||||
|
Open Rspamd Console
|
||||||
|
<span>↗</span>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="services" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Applications</p>
|
||||||
|
<h2>Quick launch</h2>
|
||||||
|
</div>
|
||||||
|
<p>Public services hosted on Edge01.</p>
|
||||||
|
</div>
|
||||||
|
<div id="service-grid" class="service-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="home-lab" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Private Network</p>
|
||||||
|
<h2>Home systems</h2>
|
||||||
|
</div>
|
||||||
|
<p>These activate after the secure tunnel is built.</p>
|
||||||
|
</div>
|
||||||
|
<div id="home-grid" class="service-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span>VOYAGER · Edge01</span>
|
||||||
|
<span>Operations Console v0.3.0</span>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/voyager.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Zerafa Infrastructure Portal</title>
|
||||||
|
<meta name="description" content="Zerafa infrastructure and home lab portal.">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0a0f1d;
|
||||||
|
--panel: rgba(20, 28, 48, 0.88);
|
||||||
|
--panel-hover: rgba(29, 40, 67, 0.96);
|
||||||
|
--text: #f4f7fb;
|
||||||
|
--muted: #9da9bd;
|
||||||
|
--accent: #79aaff;
|
||||||
|
--border: #283550;
|
||||||
|
--green: #45d483;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 15% 0%, rgba(55, 92, 175, .28), transparent 35%),
|
||||||
|
radial-gradient(circle at 100% 10%, rgba(35, 120, 150, .16), transparent 30%),
|
||||||
|
var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
width: min(1120px, calc(100% - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 56px 0 72px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 44px;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: .78rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(2.5rem, 7vw, 5.2rem);
|
||||||
|
line-height: .96;
|
||||||
|
letter-spacing: -.055em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
margin: 18px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(14, 22, 39, .75);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .92rem;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 18px rgba(69, 212, 131, .75);
|
||||||
|
}
|
||||||
|
section { margin-top: 38px; }
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
font-size: .82rem;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 22px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: transform .16s ease, border-color .16s ease, background .16s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card::after {
|
||||||
|
content: "↗";
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--panel-hover);
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 9px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 22px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
opacity: .52;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.disabled::after { content: "Soon"; font-size: .76rem; }
|
||||||
|
footer {
|
||||||
|
margin-top: 52px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .88rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
header { flex-direction: column; }
|
||||||
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.wrap { padding-top: 38px; }
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.card { min-height: 132px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Operations Portal</p>
|
||||||
|
<h1>Zerafa<br>Infrastructure</h1>
|
||||||
|
<p class="subtitle">VPS services, home lab systems and project tools.</p>
|
||||||
|
</div>
|
||||||
|
<div class="status"><span class="dot"></span> Portal Online</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p class="section-title">VPS Services</p>
|
||||||
|
<div class="grid">
|
||||||
|
<a class="card" href="https://portainer.zerafa.com">
|
||||||
|
<h2>Portainer</h2>
|
||||||
|
<p>Manage Docker containers, stacks, networks and volumes.</p>
|
||||||
|
<span class="tag">Docker</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="https://git.zerafa.com">
|
||||||
|
<h2>Gitea</h2>
|
||||||
|
<p>Infrastructure repositories, configuration and documentation.</p>
|
||||||
|
<span class="tag">Git</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="https://edge01.zerafa.com">
|
||||||
|
<h2>Edge01</h2>
|
||||||
|
<p>Primary DreamHost VPS landing page and infrastructure host.</p>
|
||||||
|
<span class="tag">Server</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p class="section-title">Home Lab</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>Home Assistant</h2>
|
||||||
|
<p>Home automation dashboards, devices and integrations.</p>
|
||||||
|
<span class="tag">Tunnel Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>Proxmox</h2>
|
||||||
|
<p>Virtual machines, containers and cluster management.</p>
|
||||||
|
<span class="tag">Tunnel Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>UniFi</h2>
|
||||||
|
<p>Network gateways, switches, access points and clients.</p>
|
||||||
|
<span class="tag">Tunnel Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>Pi-hole</h2>
|
||||||
|
<p>DNS filtering, query statistics and resolver status.</p>
|
||||||
|
<span class="tag">Tunnel Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>ADS-B</h2>
|
||||||
|
<p>Aircraft tracking, receiver statistics and feeder links.</p>
|
||||||
|
<span class="tag">Tunnel Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card disabled">
|
||||||
|
<h2>Monitoring</h2>
|
||||||
|
<p>Service uptime, alerts and infrastructure health.</p>
|
||||||
|
<span class="tag">Coming Soon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>portal.zerafa.com</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VOYAGER · Edge01</title>
|
||||||
|
<meta name="description" content="Voyager infrastructure console for Edge01 and connected systems.">
|
||||||
|
<link rel="stylesheet" href="css/voyager.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-mark">V</div>
|
||||||
|
<div>
|
||||||
|
<div class="brand-name">VOYAGER</div>
|
||||||
|
<div class="brand-node">Edge01</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a class="nav-link active" href="#overview">Overview</a>
|
||||||
|
<a class="nav-link" href="#infrastructure">Infrastructure</a>
|
||||||
|
<a class="nav-link" href="#services">Services</a>
|
||||||
|
<a class="nav-link" href="#home-lab">Home Lab</a>
|
||||||
|
<a class="nav-link" href="#monitoring">Monitoring</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span class="status-dot online"></span>
|
||||||
|
portal.zerafa.com
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="topbar">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Infrastructure Console</p>
|
||||||
|
<h1>VOYAGER</h1>
|
||||||
|
<p class="node-label">Edge01</p>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-meta">
|
||||||
|
<span>DreamHost VPS</span>
|
||||||
|
<span>Debian 13</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="overview" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Overview</p>
|
||||||
|
<h2>System sweep</h2>
|
||||||
|
</div>
|
||||||
|
<p>Each system reports independently.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status-grid" class="status-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="services" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Services</p>
|
||||||
|
<h2>Quick launch</h2>
|
||||||
|
</div>
|
||||||
|
<p>Public services hosted on Edge01.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="service-grid" class="service-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="home-lab" class="section">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="section-kicker">Private Network</p>
|
||||||
|
<h2>Home lab</h2>
|
||||||
|
</div>
|
||||||
|
<p>These activate after the secure tunnel is built.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="home-grid" class="service-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span>VOYAGER · Edge01</span>
|
||||||
|
<span>Infrastructure Console v0.2.0</span>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/voyager.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 => `
|
||||||
|
<article class="status-card">
|
||||||
|
<div class="status-top">
|
||||||
|
<span class="status-name">${item.name}</span>
|
||||||
|
<span class="status-state ${item.state}">
|
||||||
|
<span class="status-dot"></span>${item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-value">${item.value}</div>
|
||||||
|
<div class="status-detail">${item.detail}</div>
|
||||||
|
</article>
|
||||||
|
`).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}>
|
||||||
|
<div class="service-icon">${item.icon}</div>
|
||||||
|
<h3>${item.name}</h3>
|
||||||
|
<p>${item.description}</p>
|
||||||
|
<div class="service-foot">
|
||||||
|
<span>${item.category}</span>
|
||||||
|
<span>${disabled ? 'Pending' : 'Open ↗'}</span>
|
||||||
|
</div>
|
||||||
|
</${tag}>
|
||||||
|
`;
|
||||||
|
}).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 =
|
||||||
|
'<article class="status-card"><div class="status-name">Configuration</div><div class="status-value">Unavailable</div><div class="status-detail">Could not load services.json</div></article>';
|
||||||
|
});
|
||||||
@@ -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 => `
|
||||||
|
<article class="status-card">
|
||||||
|
<div class="status-top">
|
||||||
|
<span class="status-name">${item.name}</span>
|
||||||
|
<span class="status-state ${item.state}">
|
||||||
|
<span class="status-dot"></span>${item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-value">${item.value}</div>
|
||||||
|
<div class="status-detail">${item.detail}</div>
|
||||||
|
</article>
|
||||||
|
`).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}>
|
||||||
|
<div class="service-icon">${item.icon}</div>
|
||||||
|
<h3>${item.name}</h3>
|
||||||
|
<p>${item.description}</p>
|
||||||
|
<div class="service-foot">
|
||||||
|
<span>${item.category}</span>
|
||||||
|
<span>${disabled ? 'Pending' : 'Open ↗'}</span>
|
||||||
|
</div>
|
||||||
|
</${tag}>
|
||||||
|
`;
|
||||||
|
}).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 =
|
||||||
|
'<article class="status-card"><div class="status-name">Configuration</div><div class="status-value">Unavailable</div><div class="status-detail">Could not load services.json</div></article>';
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user