Initial commit of dashboard

Static status dashboard served via nginx, with the switch TLS proxy
section relabeled from WWTP to Extreme.
This commit is contained in:
dcstephenson
2026-07-29 13:47:35 -05:00
commit 40fb835319
4 changed files with 290 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
+22
View File
@@ -0,0 +1,22 @@
# dashboard — links page for services on this box
Static launcher page linking out to every service's own web UI (NetBox,
NocoDB, camera-list, go2rtc, LLDP mapper, switch-config-manager, RV50x
manager, and the stunnel-proxied WWTP switches). No health polling, no
live status — just direct links, same as typing the URL by hand.
## Setup
```
docker compose up -d --build
```
Open `http://<vm-ip>:8090/`.
## Updating
`index.html` is a single static file — edit it directly (add/remove a
card, fix a port) and run `docker compose up -d --build` to pick up the
change. There's no generator script; unlike camera-list or lldp-mapper
this doesn't pull from NocoDB, since it's just a fixed list of the
services running on this host.
+8
View File
@@ -0,0 +1,8 @@
services:
dashboard:
build: .
container_name: dashboard
ports:
- "8090:80"
restart: unless-stopped
+258
View File
@@ -0,0 +1,258 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Services</title>
<style>
:root {
--bg: #f5f6f8;
--card-bg: #ffffff;
--border: #e2e4e9;
--text: #1b1e24;
--muted: #6b7280;
--accent: #3b5bfd;
--tag-bg: #eef0ff;
--tag-text: #3b5bfd;
--shadow: 0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.08);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14161a;
--card-bg: #1d2027;
--border: #2c2f38;
--text: #e7e9ee;
--muted: #9aa1af;
--accent: #7f96ff;
--tag-bg: #262b45;
--tag-text: #b7c1ff;
--shadow: 0 1px 2px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.4);
}
}
:root[data-theme="dark"] {
--bg: #14161a; --card-bg: #1d2027; --border: #2c2f38; --text: #e7e9ee;
--muted: #9aa1af; --accent: #7f96ff; --tag-bg: #262b45; --tag-text: #b7c1ff;
--shadow: 0 1px 2px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.4);
}
:root[data-theme="light"] {
--bg: #f5f6f8; --card-bg: #ffffff; --border: #e2e4e9; --text: #1b1e24;
--muted: #6b7280; --accent: #3b5bfd; --tag-bg: #eef0ff; --tag-text: #3b5bfd;
--shadow: 0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
padding: 2.5rem 1.5rem 4rem;
}
.wrap { max-width: 980px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
.sub { color: var(--muted); margin: 0 0 2rem; font-size: .95rem; }
h2.section {
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
margin: 2.25rem 0 .75rem;
font-weight: 600;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
a.card {
display: block;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.1rem 1.2rem;
text-decoration: none;
color: inherit;
box-shadow: var(--shadow);
transition: transform .12s ease, border-color .12s ease;
}
a.card:hover {
transform: translateY(-2px);
border-color: var(--accent);
}
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
margin-bottom: .4rem;
}
.card-name {
font-weight: 600;
font-size: 1.02rem;
}
.port {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: .72rem;
color: var(--tag-text);
background: var(--tag-bg);
padding: .15rem .45rem;
border-radius: 6px;
white-space: nowrap;
}
.card-desc {
color: var(--muted);
font-size: .86rem;
line-height: 1.4;
}
.offline-note {
display: inline-block;
margin-top: .5rem;
font-size: .74rem;
color: #b45309;
background: rgba(180,83,9,0.12);
padding: .1rem .45rem;
border-radius: 6px;
}
details.subgroup {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.1rem 1.2rem;
box-shadow: var(--shadow);
}
details.subgroup summary {
cursor: pointer;
font-weight: 600;
font-size: 1.02rem;
list-style: none;
}
details.subgroup summary::-webkit-details-marker { display: none; }
details.subgroup summary::after {
content: "▸";
float: right;
color: var(--muted);
transition: transform .12s ease;
}
details.subgroup[open] summary::after { transform: rotate(90deg); }
.subgroup-desc {
color: var(--muted);
font-size: .86rem;
margin: .4rem 0 .9rem;
}
.sublinks {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: .5rem;
}
.sublinks a {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
padding: .45rem .6rem;
border: 1px solid var(--border);
border-radius: 8px;
text-decoration: none;
color: inherit;
font-size: .85rem;
}
.sublinks a:hover { border-color: var(--accent); }
.sublinks .port { font-size: .68rem; }
.tool-note {
color: var(--muted);
font-size: .86rem;
padding: .6rem 0 0;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
background: var(--tag-bg);
color: var(--tag-text);
padding: .05rem .35rem;
border-radius: 5px;
font-size: .85em;
}
</style>
</head>
<body>
<div class="wrap">
<h1>Services</h1>
<p class="sub">Links to every web UI running on this box (192.168.16.137). No live status — click through to check a service directly.</p>
<h2 class="section">Network &amp; Infrastructure</h2>
<div class="grid">
<a class="card" href="http://192.168.16.137:8000/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">NetBox</span><span class="port">:8000</span></div>
<div class="card-desc">IPAM / DCIM — source of truth for network inventory.</div>
</a>
<a class="card" href="http://192.168.16.137:5000/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">LLDP Mapper</span><span class="port">:5000</span></div>
<div class="card-desc">SSH-based topology discovery for FS switches, visualized with Cytoscape.js.</div>
</a>
<a class="card" href="http://192.168.16.137:8003/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">Switch Config Manager</span><span class="port">:8003</span></div>
<div class="card-desc">Write, save, and push switch configs over SSH.</div>
</a>
</div>
<h2 class="section">Cameras</h2>
<div class="grid">
<a class="card" href="http://192.168.16.137:8081/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">Camera List</span><span class="port">:8081</span></div>
<div class="card-desc">Links to every camera's own web UI, plus an ONVIF discovery scan vs. NocoDB.</div>
</a>
<a class="card" href="http://192.168.16.137:1984/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">go2rtc</span><span class="port">:1984</span></div>
<div class="card-desc">Live camera viewer — click a camera name to view its stream.</div>
<span class="offline-note">not currently running — start with docker compose in /opt/go2rtc</span>
</a>
</div>
<h2 class="section">Cellular &amp; Data</h2>
<div class="grid">
<a class="card" href="http://192.168.16.137:8002/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">RV50x Manager</span><span class="port">:8002</span></div>
<div class="card-desc">Manage Sierra Wireless AirLink RV50x cellular modems — configs, SSH/AT terminal.</div>
</a>
<a class="card" href="http://192.168.16.137:8080/" target="_blank" rel="noopener">
<div class="card-top"><span class="card-name">NocoDB</span><span class="port">:8080</span></div>
<div class="card-desc">Database backend behind the camera, switch, and modem inventories.</div>
</a>
</div>
<h2 class="section">Extreme Switch TLS Proxy (stunnel)</h2>
<details class="subgroup">
<summary>20 switches via stunnel &mdash; TLS 1.0 legacy switches, proxied through 192.168.16.137</summary>
<p class="subgroup-desc">Browser warns about a self-signed/expired cert on each &mdash; click through, this is expected. Container is currently stopped; start with <code>docker compose up -d</code> in <code>/opt/stunnel</code>.</p>
<div class="sublinks">
<a href="https://192.168.16.137:4500/" target="_blank" rel="noopener"><span>wwtp-offc-sw01</span><span class="port">:4500</span></a>
<a href="https://192.168.16.137:4501/" target="_blank" rel="noopener"><span>wwtp-srvr-sw01</span><span class="port">:4501</span></a>
<a href="https://192.168.16.137:4502/" target="_blank" rel="noopener"><span>wwtp-ecb1-sw01</span><span class="port">:4502</span></a>
<a href="https://192.168.16.137:4503/" target="_blank" rel="noopener"><span>wwtp-head-sw01</span><span class="port">:4503</span></a>
<a href="https://192.168.16.137:4504/" target="_blank" rel="noopener"><span>wwtp-ecb2-sw01</span><span class="port">:4504</span></a>
<a href="https://192.168.16.137:4505/" target="_blank" rel="noopener"><span>wwtp-ags1-sw01</span><span class="port">:4505</span></a>
<a href="https://192.168.16.137:4506/" target="_blank" rel="noopener"><span>wwtp-ags2-sw01</span><span class="port">:4506</span></a>
<a href="https://192.168.16.137:4507/" target="_blank" rel="noopener"><span>wwtp-ags3-sw01</span><span class="port">:4507</span></a>
<a href="https://192.168.16.137:4508/" target="_blank" rel="noopener"><span>wwtp-coag-sw01</span><span class="port">:4508</span></a>
<a href="https://192.168.16.137:4509/" target="_blank" rel="noopener"><span>wwtp-reuse-sw01</span><span class="port">:4509</span></a>
<a href="https://192.168.16.137:4510/" target="_blank" rel="noopener"><span>wwtp-ecb3-sw01</span><span class="port">:4510</span></a>
<a href="https://192.168.16.137:4511/" target="_blank" rel="noopener"><span>wwtp-yard-sw01</span><span class="port">:4511</span></a>
<a href="https://192.168.16.137:4512/" target="_blank" rel="noopener"><span>wwtp-gate-sw01</span><span class="port">:4512</span></a>
<a href="https://192.168.16.137:4513/" target="_blank" rel="noopener"><span>wwtp-ecb1-sw02</span><span class="port">:4513</span></a>
<a href="https://192.168.16.137:4514/" target="_blank" rel="noopener"><span>wwtp-ecb2-sw02</span><span class="port">:4514</span></a>
<a href="https://192.168.16.137:4515/" target="_blank" rel="noopener"><span>wwtp-uv-sw01</span><span class="port">:4515</span></a>
<a href="https://192.168.16.137:4516/" target="_blank" rel="noopener"><span>wwtp-fitr-sw01</span><span class="port">:4516</span></a>
<a href="https://192.168.16.137:4517/" target="_blank" rel="noopener"><span>wwtp-crn-sw01</span><span class="port">:4517</span></a>
<a href="https://192.168.16.137:4518/" target="_blank" rel="noopener"><span>wwtp-bst-sw01</span><span class="port">:4518</span></a>
<a href="https://192.168.16.137:4519/" target="_blank" rel="noopener"><span>wwtp-ecb3-sw02</span><span class="port">:4519</span></a>
</div>
</details>
<h2 class="section">On-demand tools</h2>
<div class="grid">
<div class="card" style="cursor:default;">
<div class="card-top"><span class="card-name">onvif-discover</span></div>
<div class="card-desc">One-off ONVIF camera sweep vs. NocoDB &mdash; not a running service, no link. Run from <code>/opt/onvif-discover</code>.</div>
</div>
</div>
</div>
</body>
</html>