feat(infra): landing-page health badges and the reach alias
nginx serves fixed /healthz/<service> probes (one named upstream each, CORS-readable, no-store) so the landing and Gitea home panels show live status; it joins the observability and storage networks to reach loki, tempo and rustfs, and resolves through Docker DNS so recreated containers are picked up. reach.fhirworx.io is a second hostname for the LEAD public-data explorer (coredns, traefik, SSO client, subdomains).
This commit is contained in:
@@ -159,17 +159,38 @@ h2 {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Status dot */
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--chart-4);
|
||||
border-radius: 50%;
|
||||
/* Health, inside the tile. Same shape as the corwins.media status badge:
|
||||
a label, a status word, and a left edge that carries the state. */
|
||||
.health {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0 0.85rem;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-left-width: 4px;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.health-k {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.health-v {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.health-ok { border-left-color: var(--chart-4) !important; }
|
||||
.health-warn { border-left-color: var(--chart-5) !important; }
|
||||
.health-bad { border-left-color: var(--destructive) !important; }
|
||||
.health-unknown { border-left-color: var(--muted-foreground) !important; }
|
||||
|
||||
/* Icon plate — one navy line glyph in a hairline square, like the
|
||||
identification block on a technical drawing. Glyphs are the inline
|
||||
Tabler sprite in index.html; stroke weight and size live here. */
|
||||
|
||||
@@ -600,10 +600,15 @@ services:
|
||||
dashboard:
|
||||
image: nginx:alpine
|
||||
container_name: dashboard
|
||||
# gateway reaches the apps. observability reaches loki and tempo.
|
||||
# storage reaches rustfs. The landing-page health probes use those names.
|
||||
networks:
|
||||
- gateway
|
||||
- observability
|
||||
- storage
|
||||
volumes:
|
||||
- ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./infra/nginx/healthz-headers.conf:/etc/nginx/healthz-headers.conf:ro
|
||||
- ./infra/nginx/index.html:/usr/share/nginx/html/index.html:ro
|
||||
- ./assets/css/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
|
||||
- ./assets/css/inject.css:/usr/share/nginx/html/fhirworx.css:ro
|
||||
|
||||
@@ -277,6 +277,7 @@ SUBDOMAINS = [
|
||||
"llm",
|
||||
"bcda", # BCDA manager (P52) — Streamlit behind git-sso
|
||||
"lead", # LEAD public-data explorer — Streamlit behind git-sso
|
||||
"reach", # same public-data explorer, at reach.fhirworx.io
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,4 @@
|
||||
192.168.1.192 llm.fhirworx.io
|
||||
192.168.1.192 bcda.fhirworx.io
|
||||
192.168.1.192 lead.fhirworx.io
|
||||
192.168.1.192 reach.fhirworx.io
|
||||
|
||||
10
infra/nginx/healthz-headers.conf
Normal file
10
infra/nginx/healthz-headers.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 2s;
|
||||
proxy_read_timeout 3s;
|
||||
# Some apps already send this header. Two copies make the browser
|
||||
# reject the response, so the badge would say down while the app is up.
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Cache-Control "no-store" always;
|
||||
@@ -51,178 +51,188 @@
|
||||
<h2>Infrastructure</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid" data-health-origin="">
|
||||
<a data-subdomain="llm" target="_blank" class="tile notebook">
|
||||
<span class="badge gpu">GPU</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-messages"/></svg></span>
|
||||
<div class="tile-eyebrow">Chat</div>
|
||||
<div class="tile-title">Comment Chat</div>
|
||||
<p class="tile-desc">RAG chat over indexed CMS rulemaking comments, with cited sources</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="bcda" data-path="/_stcore/health" target="_blank" class="tile notebook">
|
||||
<span class="status"></span>
|
||||
<a data-subdomain="bcda" target="_blank" class="tile notebook">
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-file-invoice"/></svg></span>
|
||||
<div class="tile-eyebrow">Claims</div>
|
||||
<div class="tile-title">BCDA Manager</div>
|
||||
<p class="tile-desc">Beneficiary Claims Data API v3 — exports, store, load, crosswalk</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="lead" data-path="/_stcore/health" target="_blank" class="tile notebook">
|
||||
<span class="status"></span>
|
||||
<a data-subdomain="lead" target="_blank" class="tile notebook">
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-chart-area-line"/></svg></span>
|
||||
<div class="tile-eyebrow">LEAD</div>
|
||||
<div class="tile-title">Public mirror</div>
|
||||
<p class="tile-desc">Read-only explorer over the public-data DuckDB mirror</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="reach" target="_blank" class="tile notebook">
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-chart-area-line"/></svg></span>
|
||||
<div class="tile-eyebrow">REACH</div>
|
||||
<div class="tile-title">Public mirror</div>
|
||||
<p class="tile-desc">Read-only explorer over the public-data DuckDB mirror</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="traefik" target="_blank" class="tile proxy">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-route"/></svg></span>
|
||||
<div class="tile-eyebrow">Networking</div>
|
||||
<div class="tile-title">Traefik</div>
|
||||
<p class="tile-desc">Cloud-native reverse proxy and load balancer with automatic TLS</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="s3console" target="_blank" class="tile storage">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-server-2"/></svg></span>
|
||||
<div class="tile-eyebrow">Storage</div>
|
||||
<div class="tile-title">RustFS</div>
|
||||
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="git" target="_blank" class="tile git">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-git-branch"/></svg></span>
|
||||
<div class="tile-eyebrow">Version Control</div>
|
||||
<div class="tile-title">Gitea</div>
|
||||
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="notebooks" target="_blank" class="tile notebook">
|
||||
<span class="badge gpu">GPU</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-notebook"/></svg></span>
|
||||
<div class="tile-eyebrow">Analysis</div>
|
||||
<div class="tile-title">Notebooks</div>
|
||||
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="zotero" target="_blank" class="tile research">
|
||||
<span class="badge gpu">GPU</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-books"/></svg></span>
|
||||
<div class="tile-eyebrow">Research</div>
|
||||
<div class="tile-title">Zotero</div>
|
||||
<p class="tile-desc">Research reference manager in the cloud</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="docs" target="_blank" class="tile research">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-file-text"/></svg></span>
|
||||
<div class="tile-eyebrow">Documentation</div>
|
||||
<div class="tile-title">Docs</div>
|
||||
<p class="tile-desc">API reference and CMS bibliography browser</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog">
|
||||
<a data-subdomain="nessie" target="_blank" class="tile catalog">
|
||||
<span class="badge data">Data</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-versions"/></svg></span>
|
||||
<div class="tile-eyebrow">Catalog</div>
|
||||
<div class="tile-title">Nessie</div>
|
||||
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="trino" target="_blank" class="tile query">
|
||||
<span class="badge data">Data</span>
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-sql"/></svg></span>
|
||||
<div class="tile-eyebrow">Query</div>
|
||||
<div class="tile-title">Trino</div>
|
||||
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="polaris" target="_blank" class="tile catalog">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-north-star"/></svg></span>
|
||||
<div class="tile-eyebrow">Catalog</div>
|
||||
<div class="tile-title">Polaris</div>
|
||||
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access control</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="grafana" target="_blank" class="tile observability">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-chart-area-line"/></svg></span>
|
||||
<div class="tile-eyebrow">Observability</div>
|
||||
<div class="tile-title">Grafana</div>
|
||||
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="tempo" target="_blank" class="tile metrics">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-timeline"/></svg></span>
|
||||
<div class="tile-eyebrow">Tracing</div>
|
||||
<div class="tile-title">Tempo</div>
|
||||
<p class="tile-desc">Distributed tracing for microservices</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="prometheus" target="_blank" class="tile logs">
|
||||
<span class="status"></span>
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-activity-heartbeat"/></svg></span>
|
||||
<div class="tile-eyebrow">Metrics</div>
|
||||
<div class="tile-title">Prometheus</div>
|
||||
<p class="tile-desc">Metrics collection and alerting</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
|
||||
<span class="status"></span>
|
||||
<a data-subdomain="loki" target="_blank" class="tile database">
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-list-details"/></svg></span>
|
||||
<div class="tile-eyebrow">Logs</div>
|
||||
<div class="tile-title">Loki</div>
|
||||
<p class="tile-desc">Log aggregation system by Grafana</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
|
||||
<a data-subdomain="api" data-path="/health" target="_blank" class="tile proxy">
|
||||
<span class="status"></span>
|
||||
<a data-subdomain="api" target="_blank" class="tile proxy">
|
||||
<span class="icon" aria-hidden="true"><svg><use href="#i-api"/></svg></span>
|
||||
<div class="tile-eyebrow">API</div>
|
||||
<div class="tile-title">Platform API</div>
|
||||
<p class="tile-desc">FastAPI platform backend</p>
|
||||
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
|
||||
<span class="tile-port"></span>
|
||||
<div class="stripe"></div>
|
||||
</a>
|
||||
@@ -234,15 +244,33 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function paintHealth(el, origin, sub, fqdn) {
|
||||
var box = el.querySelector('.health');
|
||||
if (!box) return;
|
||||
var value = box.querySelector('.health-v');
|
||||
function paint(kind, text) {
|
||||
box.className = 'health health-' + kind;
|
||||
value.textContent = text;
|
||||
}
|
||||
fetch(origin + '/healthz/' + sub, {cache: 'no-store'}).then(function (r) {
|
||||
if (r.status === 503) paint('warn', 'not ready');
|
||||
else if (r.ok) paint('ok', 'up');
|
||||
else paint('bad', 'down');
|
||||
}, function () {
|
||||
paint('bad', 'down');
|
||||
});
|
||||
}
|
||||
(function() {
|
||||
var root = document.querySelector('[data-health-origin]');
|
||||
var origin = root ? (root.getAttribute('data-health-origin') || '') : '';
|
||||
var base = location.hostname.replace(/^dashboard\./, '');
|
||||
document.querySelectorAll('[data-subdomain]').forEach(function(el) {
|
||||
var sub = el.dataset.subdomain;
|
||||
var path = el.dataset.path || '';
|
||||
var fqdn = sub + '.' + base;
|
||||
el.href = 'https://' + fqdn + path;
|
||||
el.href = 'https://' + fqdn + '/';
|
||||
var port = el.querySelector('.tile-port');
|
||||
if (port) port.textContent = fqdn;
|
||||
paintHealth(el, origin, sub, fqdn);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,9 @@ http {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Docker's DNS, so a recreated container is picked up without a reload.
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# CORS for theme assets (loaded cross-origin by themed services)
|
||||
location ~* \.(css|svg|png|ico)$ {
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
@@ -21,6 +24,28 @@ http {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Service health for the landing panels. The Gitea home fetches these
|
||||
# cross-origin, so the status code has to be readable from any host.
|
||||
# Each name is one fixed upstream. Nothing here is an open proxy.
|
||||
location = /healthz/llm { set $up llm:8000; rewrite ^ /health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/bcda { set $up bcda:8501; rewrite ^ /_stcore/health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/lead { set $up lead:8501; rewrite ^ /_stcore/health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/reach { set $up lead:8501; rewrite ^ /_stcore/health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/traefik { set $up traefik:8080; rewrite ^ /api/version break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/s3console { set $up rustfs:9001; rewrite ^ /health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/git { set $up git:3000; rewrite ^ /api/healthz break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/notebooks { set $up notebooks:2718; rewrite ^ /health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/zotero { set $up zotero:8080; rewrite ^ / break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/docs { set $up docs:80; rewrite ^ / break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/nessie { set $up nessie:19120; rewrite ^ /api/v2/config break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/trino { set $up trino:8080; rewrite ^ /v1/info break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/polaris { set $up polaris:8182; rewrite ^ /q/health/ready break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/grafana { set $up grafana:3000; rewrite ^ /api/health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/tempo { set $up tempo:3200; rewrite ^ /ready break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/prometheus { set $up prometheus:9090; rewrite ^ /-/healthy break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/loki { set $up loki:3100; rewrite ^ /ready break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
location = /healthz/api { set $up api:8000; rewrite ^ /health break; proxy_pass http://$up; include /etc/nginx/healthz-headers.conf; }
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{{- $multi := dict
|
||||
"rustfs-api" (dict "container" "rustfs" "port" "9000" "subdomain" "s3" "theme" false "mw" "git-sso,infra-headers")
|
||||
"rustfs-console" (dict "container" "rustfs" "port" "9001" "subdomain" "s3console" "theme" true "mw" "git-sso,infra-headers")
|
||||
"reach" (dict "container" "lead" "port" "8501" "subdomain" "reach" "theme" false "mw" "git-sso,secure-headers")
|
||||
-}}
|
||||
http:
|
||||
middlewares:
|
||||
|
||||
@@ -24,7 +24,7 @@ subdomains = [
|
||||
"dashboard", "docs", "git", "ci", "notebooks", "zotero",
|
||||
"webdav", "api", "nessie", "trino", "polaris",
|
||||
"grafana", "prometheus", "tempo", "loki",
|
||||
"s3", "s3console", "llm", "bcda", "lead",
|
||||
"s3", "s3console", "llm", "bcda", "lead", "reach",
|
||||
]
|
||||
|
||||
[services]
|
||||
|
||||
Reference in New Issue
Block a user