diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 7b80471..d110dfa 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -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. */ diff --git a/compose.yml b/compose.yml index 1bc399d..6dd7a68 100644 --- a/compose.yml +++ b/compose.yml @@ -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 diff --git a/dev/scripts/bootstrap_sso.py b/dev/scripts/bootstrap_sso.py index 862954a..f14fc50 100644 --- a/dev/scripts/bootstrap_sso.py +++ b/dev/scripts/bootstrap_sso.py @@ -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 ] diff --git a/infra/coredns/hosts b/infra/coredns/hosts index 5fb5f2d..8518f2f 100644 --- a/infra/coredns/hosts +++ b/infra/coredns/hosts @@ -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 diff --git a/infra/nginx/healthz-headers.conf b/infra/nginx/healthz-headers.conf new file mode 100644 index 0000000..bf62633 --- /dev/null +++ b/infra/nginx/healthz-headers.conf @@ -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; diff --git a/infra/nginx/index.html b/infra/nginx/index.html index 05fc17e..fd25e8a 100644 --- a/infra/nginx/index.html +++ b/infra/nginx/index.html @@ -51,178 +51,188 @@

Infrastructure

-
+
GPU -
Chat
Comment Chat

RAG chat over indexed CMS rulemaking comments, with cited sources

+ healthchecking
- - +
Claims
BCDA Manager

Beneficiary Claims Data API v3 — exports, store, load, crosswalk

+ healthchecking
- - +
LEAD
Public mirror

Read-only explorer over the public-data DuckDB mirror

+ healthchecking + +
+
+ + + +
REACH
+
Public mirror
+

Read-only explorer over the public-data DuckDB mirror

+ healthchecking
-
Networking
Traefik

Cloud-native reverse proxy and load balancer with automatic TLS

+ healthchecking
-
Storage
RustFS

S3-compatible object storage for all your data needs

+ healthchecking
-
Version Control
Gitea

Self-hosted Git service with packages and CI

+ healthchecking
GPU -
Analysis
Notebooks

Marimo reactive notebooks with CUDA acceleration

+ healthchecking
GPU -
Research
Zotero

Research reference manager in the cloud

+ healthchecking
-
Documentation
Docs

API reference and CMS bibliography browser

+ healthchecking
- + Data -
Catalog
Nessie

Git-like versioned data catalog for Apache Iceberg

+ healthchecking
Data -
Query
Trino

Distributed SQL query engine for the lakehouse

+ healthchecking
-
Catalog
Polaris

Apache Iceberg catalog with governance and Unity-style access control

+ healthchecking
-
Observability
Grafana

Observability dashboards for metrics, logs, and traces

+ healthchecking
-
Tracing
Tempo

Distributed tracing for microservices

+ healthchecking
-
Metrics
Prometheus

Metrics collection and alerting

+ healthchecking
- - +
Logs
Loki

Log aggregation system by Grafana

+ healthchecking
- - +
API
Platform API

FastAPI platform backend

+ healthchecking
@@ -234,15 +244,33 @@
diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf index 802b989..2a888d7 100644 --- a/infra/nginx/nginx.conf +++ b/infra/nginx/nginx.conf @@ -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; } diff --git a/infra/traefik/dynamic/services.yml b/infra/traefik/dynamic/services.yml index 1f5703b..b9f424f 100644 --- a/infra/traefik/dynamic/services.yml +++ b/infra/traefik/dynamic/services.yml @@ -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: diff --git a/stack.toml b/stack.toml index 4caf6d7..40fa93f 100644 --- a/stack.toml +++ b/stack.toml @@ -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]