feat: port fhirworx theme — editorial redesign based on hti5.org
Some checks failed
CI / skinny-install (aco) (push) Successful in 54s
CI / skinny-install (api) (push) Successful in 32s
CI / skinny-install (bcda) (push) Successful in 33s
CI / skinny-install (bib) (push) Successful in 30s
CI / skinny-install (bls) (push) Successful in 26s
CI / lint-test (push) Successful in 3m19s
CI / skinny-install (ccw) (push) Successful in 27s
CI / skinny-install (cms) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 38s
CI / skinny-install (conf) (push) Successful in 33s
CI / skinny-install (opps) (push) Successful in 43s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Successful in 7s
Infra CI / docs (push) Failing after 10s
Infra CI / api (push) Successful in 13s
Infra CI / mc (push) Successful in 7s
CI / skinny-install (perf) (push) Successful in 31s
CI / skinny-install (pfs) (push) Successful in 30s
CI / skinny-install (rex) (push) Successful in 31s
Deploy / build-scan-report (push) Failing after 1m47s

Rip and replace nature/loch with fhirworx — a Federal Register / data
journalism aesthetic inspired by hti5.org.

Typography: Playfair Display for display headings, Source Serif 4 for
reading body, JetBrains Mono reserved for IDs, labels, metadata (docket
codes, commit hashes, query editors, file paths, ports). Stripped
carpet-bombed font-family: mono !important across inject.css and
per-service sheets; kept mono only where load-bearing (th, pre/code,
query editors, span IDs, badge/chip/tag, bucket names, query IDs).

Palette: warm cream (#F7F5F0) background, near-black warm foreground,
deep federal navy (#1C2B3A) primary/sidebar, indigo chart scale +
teal (support) + amber (opposition) semantic accents. PALETTE extended
to 8 colours (+ plum, gray, sepia) for categorical notebook charts.

Rename (no backwards compat):
- assets/nature.py → assets/fhirworx.py
- assets/css/loch.css → assets/css/fhirworx.css
- theme-loch.css → theme-fhirworx.css (Gitea)
- Altair theme registered as "fhirworx" via new alt.theme.register
  decorator API, eliminating the altair 5.5 deprecation warning
- Traefik inject-loch middleware → inject-fhirworx
- Gitea DEFAULT_THEME=fhirworx
- Delete empty theme-throwback.css stub

Rewrote infra/nginx/index.html and infra/gitea/custom/templates/home.tmpl
with editorial masthead (rule-line + Playfair headline + mono identifier
+ serif tagline + rule-line-thin), SERVICES eyebrow + Infrastructure h2,
tiles with .tile-eyebrow / .tile-title / .tile-desc / .tile-port
structure. Stripped letter-spacing: 2-4px tricks and text-transform:
uppercase from non-metadata elements across all per-service sheets.

Propagated import renames: src/conf/connect.py, src/aco/dag.py,
notebooks/acodb_explorer.py, tests/conf/test_connect.py,
tests/test_notebook_layout.py, docs/src/css/custom.css, README.md,
README.md.j2, compose.yml, infra/traefik/dynamic/services.yml.
This commit is contained in:
kert
2026-04-10 14:37:20 -04:00
parent cb530c25d9
commit c81992184a
25 changed files with 2056 additions and 1567 deletions

View File

@@ -228,7 +228,7 @@ ROOT_KEY=$KEY uv run python -m api.auth derive $COMMIT_SHA --redact
## Design system ## Design system
Theme: **loch** (deep-navy). Traefik's `inject-loch` middleware rewrites HTML to inject `loch.css` and the favicon. Per-service CSS in `assets/css/`. Altair chart theme in `assets/nature.py` (Nature-conformant, accessible palette, IBM Plex Mono headings). Theme: **fhirworx** (federal editorial). Traefik's `inject-fhirworx` middleware rewrites HTML to inject `fhirworx.css` and the favicon. Per-service CSS in `assets/css/`. Altair chart theme in `assets/fhirworx.py` (federal palette, Source Serif 4 / Playfair Display / JetBrains Mono).
## Data lakehouse ## Data lakehouse
@@ -491,7 +491,7 @@ stack/
│ └── zot/ │ └── zot/
├── infra/ Service configs and Dockerfiles ├── infra/ Service configs and Dockerfiles
│ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc) │ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc)
│ ├── traefik/ Reverse proxy + loch CSS injection │ ├── traefik/ Reverse proxy + fhirworx CSS injection
│ ├── grafana/ Dashboards and datasource provisioning │ ├── grafana/ Dashboards and datasource provisioning
│ ├── prometheus/ Metrics collection │ ├── prometheus/ Metrics collection
│ ├── loki/ Log aggregation │ ├── loki/ Log aggregation
@@ -499,7 +499,7 @@ stack/
├── assets/ Branding, styles, generated artifacts ├── assets/ Branding, styles, generated artifacts
│ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker) │ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker)
│ ├── icons/ Favicons, logos, coverage badge │ ├── icons/ Favicons, logos, coverage badge
│ └── nature.py Nature-conformant chart palette │ └── fhirworx.py fhirworx chart palette and Altair theme
├── cloud/ Cloud provider deployment guides ├── cloud/ Cloud provider deployment guides
│ ├── self-hosted/ Docker Compose (default) │ ├── self-hosted/ Docker Compose (default)
│ ├── aws/ S3, RDS, Glue, ECS │ ├── aws/ S3, RDS, Glue, ECS

View File

@@ -221,7 +221,7 @@ ROOT_KEY=$KEY uv run python -m api.auth derive $COMMIT_SHA --redact
## Design system ## Design system
Theme: **loch** (deep-navy). Traefik's `inject-loch` middleware rewrites HTML to inject `loch.css` and the favicon. Per-service CSS in `assets/css/`. Altair chart theme in `assets/nature.py` (Nature-conformant, accessible palette, IBM Plex Mono headings). Theme: **fhirworx** (federal editorial). Traefik's `inject-fhirworx` middleware rewrites HTML to inject `fhirworx.css` and the favicon. Per-service CSS in `assets/css/`. Altair chart theme in `assets/fhirworx.py` (federal palette, Source Serif 4 / Playfair Display / JetBrains Mono).
## Data lakehouse ## Data lakehouse
@@ -472,7 +472,7 @@ stack/
{% endfor %} {% endfor %}
├── infra/ Service configs and Dockerfiles ├── infra/ Service configs and Dockerfiles
│ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc) │ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc)
│ ├── traefik/ Reverse proxy + loch CSS injection │ ├── traefik/ Reverse proxy + fhirworx CSS injection
│ ├── grafana/ Dashboards and datasource provisioning │ ├── grafana/ Dashboards and datasource provisioning
│ ├── prometheus/ Metrics collection │ ├── prometheus/ Metrics collection
│ ├── loki/ Log aggregation │ ├── loki/ Log aggregation
@@ -480,7 +480,7 @@ stack/
├── assets/ Branding, styles, generated artifacts ├── assets/ Branding, styles, generated artifacts
│ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker) │ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker)
│ ├── icons/ Favicons, logos, coverage badge │ ├── icons/ Favicons, logos, coverage badge
│ └── nature.py Nature-conformant chart palette │ └── fhirworx.py fhirworx chart palette and Altair theme
├── cloud/ Cloud provider deployment guides ├── cloud/ Cloud provider deployment guides
│ ├── self-hosted/ Docker Compose (default) │ ├── self-hosted/ Docker Compose (default)
│ ├── aws/ S3, RDS, Glue, ECS │ ├── aws/ S3, RDS, Glue, ECS

View File

@@ -1,20 +1,27 @@
/* LOCH — deep-navy design system for Dashboard (nginx landing page) */ /* HTI-5 — Federal Register editorial design system for Dashboard (nginx landing page) */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root { :root {
--ds-navy: #0a1628; --background: #F7F5F0;
--ds-navy-light: #162040; --foreground: #1A1A18;
--ds-navy-mid: #1e2d50; --card: #FAFAF7;
--ds-slate: #2a3f5f; --card-foreground: #1A1A18;
--ds-steel: #7a90b0; --primary: #1C2B3A;
--ds-white: #f0f0f0; --primary-foreground: #F7F5F0;
--ds-blue: #4488dd; --secondary: #EDEBE6;
--ds-blue-light: #5588dd; --muted-foreground: #6B6B68;
--ds-blue-bright: #88aaee; --border: #D4D0C8;
--ds-red: #ee5577; --sidebar: #1C2B3A;
--ds-red-light: #dd4466; --sidebar-foreground: #B8C5D0;
--ds-green: #44aa44; --sidebar-accent: #253748;
--chart-4: #2E8B6E;
--chart-5: #C8702A;
--destructive: #C0392B;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
} }
* { * {
@@ -24,169 +31,234 @@
} }
body { body {
font-family: 'IBM Plex Mono', 'Courier New', monospace; font-family: var(--font-body);
background: var(--ds-navy); font-size: 16px;
line-height: 1.65;
background: var(--background);
color: var(--foreground);
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
color: var(--ds-white); -webkit-font-smoothing: antialiased;
} }
.container { .container {
max-width: 1400px; max-width: 1180px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 3rem 1.5rem;
} }
header { /* ── Masthead ──────────────────────────────────────────────── */
text-align: center;
padding: 40px 20px; .masthead {
border-bottom: 4px solid var(--ds-red); margin-bottom: 1.5rem;
}
.rule-line {
border: none;
border-top: 3px solid var(--foreground);
margin-bottom: 1rem;
}
.rule-line-thin {
border: none;
border-top: 1px solid var(--border);
margin-bottom: 1rem;
}
.masthead-row {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
} }
h1 { h1 {
font-family: 'IBM Plex Mono', monospace; font-family: var(--font-display);
font-weight: 600; font-weight: 800;
font-size: 2rem; font-size: clamp(2.5rem, 5vw, 3.5rem);
color: var(--ds-white); line-height: 1.1;
letter-spacing: 6px; letter-spacing: -0.02em;
text-shadow: 3px 3px 0 var(--ds-navy-mid); color: var(--foreground);
} }
.subtitle { .masthead-eyebrow {
font-family: 'IBM Plex Mono', monospace; font-family: var(--font-mono);
color: var(--ds-blue-bright); font-size: 0.7rem;
font-size: 0.55rem; text-transform: uppercase;
margin-top: 15px; letter-spacing: 0.12em;
letter-spacing: 2px; color: var(--muted-foreground);
align-self: flex-end;
padding-bottom: 0.35rem;
} }
.score-bar { .tagline {
display: flex; font-family: var(--font-body);
justify-content: center; font-size: 1.125rem;
gap: 40px; color: var(--muted-foreground);
margin-top: 20px; max-width: 65ch;
font-family: 'IBM Plex Mono', monospace; line-height: 1.65;
font-size: 0.5rem; margin-bottom: 1.5rem;
} }
.score-item { /* ── Services section ─────────────────────────────────────── */
color: var(--ds-white);
background: var(--ds-blue); .section-header {
padding: 6px 14px; margin-bottom: 1.5rem;
border: 2px solid var(--ds-blue-light);
} }
.section-eyebrow {
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted-foreground);
margin-bottom: 0.4rem;
display: block;
}
h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.75rem;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--foreground);
}
/* ── Tile grid ────────────────────────────────────────────── */
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px; gap: 1.25rem;
padding: 20px 0; padding: 0;
margin-top: 20px; margin-top: 0;
} }
.tile { .tile {
background: var(--ds-navy-light); background: var(--card);
border: 3px solid var(--ds-slate); border: 1px solid var(--border);
padding: 25px; border-top: 3px solid var(--foreground);
padding: 1.5rem;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.2s ease; transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
display: block; display: block;
} }
.tile:hover { .tile:hover {
border-color: var(--ds-blue); border-color: var(--primary);
background: var(--ds-navy-mid); background: var(--card);
} }
/* All tile variants use the same base style */ .tile:hover .tile-title {
.tile .icon { color: var(--primary);
font-size: 2.5rem;
margin-bottom: 15px;
display: block;
}
.tile.storage .icon,
.tile.database .icon { color: var(--ds-red); }
.tile.git .icon { color: var(--ds-blue-bright); }
.tile.ci .icon { color: var(--ds-blue-light); }
.tile.notebook .icon,
.tile.research .icon { color: var(--ds-white); }
.tile.catalog .icon { color: var(--ds-blue-bright); }
.tile.query .icon { color: var(--ds-blue-light); }
.tile.observability .icon { color: var(--ds-red-light); }
.tile.metrics .icon { color: var(--ds-blue-bright); }
.tile.logs .icon { color: var(--ds-blue-light); }
.tile.proxy .icon { color: var(--ds-white); }
.tile-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: var(--ds-white);
margin-bottom: 10px;
letter-spacing: 1px;
}
.tile-desc {
font-size: 0.75rem;
color: var(--ds-steel);
line-height: 1.6;
margin-bottom: 15px;
}
.tile-port {
font-size: 0.7rem;
color: var(--ds-blue-bright);
display: inline-block;
} }
/* Status dot */
.status { .status {
position: absolute; position: absolute;
top: 15px; top: 1rem;
right: 15px; right: 1rem;
width: 10px; width: 8px;
height: 10px; height: 8px;
background: var(--ds-green); background: var(--chart-4);
border-radius: 50%; border-radius: 50%;
} }
/* Icon — decorative, not oversized */
.tile .icon {
font-size: 1.5rem;
margin-bottom: 0.75rem;
display: block;
line-height: 1;
}
/* Tile eyebrow — mono metadata label (category, service type) */
.tile-eyebrow {
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted-foreground);
margin-bottom: 0.25rem;
}
/* Tile title — Playfair Display, editorial display size */
.tile-title {
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--foreground);
margin: 0.25rem 0 0.5rem;
transition: color 0.15s ease;
}
/* Tile description — Source Serif 4, reading body */
.tile-desc {
font-family: var(--font-body);
font-size: 0.95rem;
color: var(--muted-foreground);
line-height: 1.55;
margin-bottom: 1rem;
}
/* Tile port/metadata — mono, muted, small */
.tile-port {
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--muted-foreground);
opacity: 0.7;
display: inline-block;
}
/* Bottom accent line */
.stripe { .stripe {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 3px; height: 2px;
background: var(--ds-red); background: var(--border);
} }
footer { /* ── Badge labels ─────────────────────────────────────────── */
text-align: center;
padding: 40px;
color: var(--ds-slate);
font-size: 0.7rem;
border-top: 3px solid var(--ds-slate);
margin-top: 20px;
}
footer span {
color: var(--ds-red);
}
/* Badges */
.badge { .badge {
position: absolute; position: absolute;
top: -1px; top: -1px;
left: 20px; left: 1.25rem;
font-family: 'IBM Plex Mono', monospace; font-family: var(--font-mono);
color: var(--ds-white); color: var(--primary-foreground);
font-size: 0.35rem; font-size: 0.6rem;
padding: 4px 8px; padding: 3px 7px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 0.08em;
} }
.badge.new { background: var(--ds-red); } .badge.new { background: var(--chart-5); }
.badge.gpu { background: var(--ds-blue); } .badge.gpu { background: var(--primary); }
.badge.data { background: var(--ds-blue); } .badge.data { background: var(--primary); }
/* ── Footer ───────────────────────────────────────────────── */
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 3px solid var(--foreground);
text-align: left;
color: var(--muted-foreground);
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.08em;
}
footer span {
color: var(--chart-5);
}

157
assets/css/fhirworx.css Normal file
View File

@@ -0,0 +1,157 @@
/* HTI-5 — fhirworx design system
Single source of truth for the colour palette.
Design: Federal Register Meets Data Journalism
Inspired by ProPublica, The Marshall Project, and the Federal Register
When changing a token here, propagate to the service-specific files:
inject.css, dashboard.css, gitea.css, woodpecker.css, marimo.css */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
/* ── Core palette ──────────────────────────────────────────── */
/* Off-white background, near-black text */
--background: #F7F5F0;
--foreground: #1A1A18;
/* Card: slightly warmer white */
--card: #FAFAF7;
--card-foreground: #1A1A18;
--popover: #FAFAF7;
--popover-foreground: #1A1A18;
/* Primary: deep federal blue/navy */
--primary: #1C2B3A;
--primary-foreground: #F7F5F0;
--secondary: #EDEBE6;
--secondary-foreground: #2D2D2B;
--muted: #EDEBE6;
--muted-foreground: #6B6B68;
--accent: #EDEBE6;
--accent-foreground: #1A1A18;
--destructive: #C0392B;
--destructive-foreground: #F7F5F0;
--border: #D4D0C8;
--input: #D4D0C8;
--ring: #1C2B3A;
--radius: 0.2rem;
/* ── Sidebar: deep federal navy ────────────────────────────── */
--sidebar: #1C2B3A;
--sidebar-foreground: #B8C5D0;
--sidebar-primary: #5B8FA8;
--sidebar-primary-foreground: #F7F5F0;
--sidebar-accent: #253748;
--sidebar-accent-foreground: #EEE9E0;
--sidebar-border: #253748;
--sidebar-ring: #5B8FA8;
/* ── Chart palette: indigo scale + teal + amber ────────────── */
--chart-1: #6B7FD7; /* indigo medium */
--chart-2: #4A5CB8; /* indigo dark */
--chart-3: #2E3D8F; /* indigo deep */
--chart-4: #2E8B6E; /* teal (support) */
--chart-5: #C8702A; /* amber (opposition) */
/* ── Typography ─────────────────────────────────────────────── */
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
}
/* ── Editorial base styles ────────────────────────────────── */
body {
font-family: var(--font-body);
font-size: 16px;
line-height: 1.65;
background: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--foreground);
}
code, kbd, pre, .font-mono {
font-family: var(--font-mono);
}
/* ── Editorial component classes ──────────────────────────── */
/* Rule lines — structural dividers, not borders */
.rule-line {
border: none;
border-top: 3px solid var(--foreground);
margin-bottom: 1rem;
}
.rule-line-thin {
border: none;
border-top: 1px solid var(--border);
margin-bottom: 1rem;
}
/* Eyebrow — canonical mono label treatment for metadata, IDs, KPI labels */
.eyebrow {
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted-foreground);
}
/* Pull quote — editorial emphasis */
.pull-quote {
border-left: 3px solid var(--primary);
padding-left: 1rem;
font-style: italic;
color: var(--muted-foreground);
font-family: var(--font-body);
}
/* Prose — long-form reading columns */
.prose {
max-width: 65ch;
}
.prose p {
line-height: 1.75;
color: var(--foreground);
}
/* ── WCAG 2.2 AA — Accessibility ──────────────────────────── */
/* Focus indicators (2.4.7, 2.4.11) */
:focus-visible {
outline: 2px solid var(--primary) !important;
outline-offset: 2px !important;
}
/* Reduced motion (2.3.3) */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Minimum target size (2.5.8) */
button, [role="button"], a, input, select, textarea {
min-height: 24px;
min-width: 24px;
}

View File

@@ -1,162 +1,163 @@
/* LOCH — deep-navy design system for Gitea */ /* HTI-5 — fhirworx design system for Gitea */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap');
/* Theme metadata */ /* Theme metadata */
gitea-theme-meta-info { gitea-theme-meta-info {
--theme-display-name: "Loch"; --theme-display-name: "Fhirworx";
} }
:root { :root {
--is-dark-theme: true; --is-dark-theme: false;
/* Loch Color Palette */ /* HTI-5 Color Palette */
--ds-navy: #0a1628; --background: #F7F5F0;
--ds-navy-light: #162040; --foreground: #1A1A18;
--ds-navy-mid: #1e2d50; --card: #FAFAF7;
--ds-slate: #2a3f5f; --primary: #1C2B3A;
--ds-steel: #7a90b0; --primary-foreground: #F7F5F0;
--ds-white: #f0f0f0; --secondary: #EDEBE6;
--muted-foreground: #6B6B68;
--border: #D4D0C8;
--sidebar: #1C2B3A;
--sidebar-foreground: #B8C5D0;
--sidebar-accent: #253748;
--destructive: #C0392B;
--chart-4: #2E8B6E;
--chart-5: #C8702A;
--ds-blue: #4488dd; --font-display: "Playfair Display", Georgia, serif;
--ds-blue-light: #5588dd; --font-body: "Source Serif 4", Georgia, serif;
--ds-blue-bright: #88aaee; --font-mono: "JetBrains Mono", "Fira Code", monospace;
--ds-red: #ee5577; /* Gitea color mappings — derived from hti5 tokens */
--ds-red-light: #dd4466; --color-primary: var(--primary);
--color-primary-light: #2E3D8F;
--color-primary-dark: #111c26;
--color-primary-alpha-10: color-mix(in srgb, var(--primary) 10%, transparent);
--color-primary-alpha-20: color-mix(in srgb, var(--primary) 20%, transparent);
--color-primary-alpha-40: color-mix(in srgb, var(--primary) 40%, transparent);
--ds-green: #44aa44; --color-secondary: var(--muted-foreground);
--ds-green-light: #66cc66; --color-secondary-light: #9B9B98;
--ds-yellow: #ddaa33; --color-secondary-dark: var(--foreground);
/* Gitea color mappings */
--color-primary: var(--ds-blue);
--color-primary-light: var(--ds-blue-light);
--color-primary-dark: #2255aa;
--color-primary-alpha-10: rgba(51, 102, 204, 0.1);
--color-primary-alpha-20: rgba(51, 102, 204, 0.2);
--color-primary-alpha-40: rgba(51, 102, 204, 0.4);
--color-secondary: var(--ds-slate);
--color-secondary-light: var(--ds-steel);
--color-secondary-dark: var(--ds-navy-light);
/* Background colors */ /* Background colors */
--color-body: var(--ds-navy); --color-body: var(--background);
--color-box-body: var(--ds-navy); --color-box-body: var(--card);
--color-box-header: var(--ds-navy-light); --color-box-header: var(--secondary);
/* Text colors */ /* Text colors */
--color-text: var(--ds-white); --color-text: var(--foreground);
--color-text-light: var(--ds-steel); --color-text-light: var(--muted-foreground);
--color-text-dark: var(--ds-white); --color-text-dark: var(--foreground);
/* Input colors */ /* Input colors */
--color-input-text: var(--ds-white); --color-input-text: var(--foreground);
--color-input-background: var(--ds-navy-mid); --color-input-background: var(--card);
--color-input-border: var(--ds-slate); --color-input-border: var(--border);
/* Status colors */ /* Status colors */
--color-success: var(--ds-green); --color-success: var(--chart-4);
--color-success-text: var(--ds-white); --color-success-text: #fff;
--color-warning: var(--ds-yellow); --color-warning: var(--chart-5);
--color-warning-text: var(--ds-navy); --color-warning-text: #fff;
--color-error: var(--ds-red); --color-error: var(--destructive);
--color-error-text: var(--ds-white); --color-error-text: #fff;
--color-info: var(--ds-blue-light); --color-info: var(--primary);
--color-info-text: var(--ds-white); --color-info-text: var(--primary-foreground);
/* Border */ /* Border */
--color-border: var(--ds-slate); --color-border: var(--border);
/* Link colors */ /* Link colors */
--color-link: var(--ds-blue-bright); --color-link: var(--primary);
--color-link-hover: var(--ds-white); --color-link-hover: var(--foreground);
/* Code */ /* Code */
--color-code-bg: var(--ds-navy-light); --color-code-bg: var(--secondary);
--color-code-border: var(--ds-slate); --color-code-border: var(--border);
/* Shadows */ /* Shadows */
--color-shadow: rgba(0, 0, 0, 0.4); --color-shadow: rgba(0, 0, 0, 0.1);
} }
/* Global styles */ /* Global styles */
body { body {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
background: var(--ds-navy) !important; background: var(--background) !important;
color: var(--foreground) !important;
} }
/* Headings */ /* Headings */
h1, h2, h3, h4, h5, h6, h1, h2, h3, h4, h5, h6,
.header-wrapper .header { .header-wrapper .header {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-display) !important;
font-weight: 600 !important; font-weight: 600 !important;
letter-spacing: 0.5px !important; line-height: 1.3 !important;
} }
h1 { font-size: 18px !important; color: var(--ds-white) !important; } h1 { font-size: 22px !important; color: var(--foreground) !important; }
h2 { font-size: 14px !important; color: var(--ds-blue-bright) !important; } h2 { font-size: 17px !important; color: var(--primary) !important; }
h3 { font-size: 12px !important; color: var(--ds-white) !important; } h3 { font-size: 14px !important; color: var(--foreground) !important; }
/* Navbar */ /* Navbar */
.ui.secondary.menu.navbar, .ui.secondary.menu.navbar,
.navbar { .navbar {
background: var(--ds-navy-light) !important; background: var(--sidebar) !important;
border-bottom: 3px solid var(--ds-slate) !important; border-bottom: 3px solid var(--foreground) !important;
} }
.navbar .item { .navbar .item {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
color: var(--ds-white) !important; color: var(--sidebar-foreground) !important;
} }
.navbar .item:hover { .navbar .item:hover {
background: var(--ds-navy-mid) !important; background: var(--sidebar-accent) !important;
color: var(--ds-blue-bright) !important; color: var(--sidebar-accent-foreground, #EEE9E0) !important;
} }
.navbar .item.active { .navbar .item.active {
background: var(--ds-blue) !important; background: var(--sidebar-accent) !important;
color: var(--ds-white) !important; color: #fff !important;
} }
/* Buttons */ /* Buttons */
.ui.button, .ui.button,
.button { .button {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
font-weight: 500 !important; font-weight: 500 !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
transition: all 0.15s ease !important; transition: all 0.15s ease !important;
} }
.ui.primary.button, .ui.primary.button,
.ui.blue.button { .ui.blue.button {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
border: 2px solid var(--ds-blue-light) !important; border: 1px solid var(--primary) !important;
} }
.ui.primary.button:hover, .ui.primary.button:hover,
.ui.blue.button:hover { .ui.blue.button:hover {
background: var(--ds-blue-light) !important; background: var(--sidebar-accent) !important;
color: var(--ds-white) !important; color: #fff !important;
} }
.ui.green.button { .ui.green.button {
background: var(--ds-green) !important; background: var(--chart-4) !important;
color: var(--ds-white) !important; color: #fff !important;
border: 2px solid var(--ds-green-light) !important; border: 1px solid var(--chart-4) !important;
} }
.ui.green.button:hover { .ui.green.button:hover {
background: var(--ds-green-light) !important; opacity: 0.85;
color: var(--ds-navy) !important;
} }
.ui.red.button { .ui.red.button {
background: var(--ds-red) !important; background: var(--destructive) !important;
color: var(--ds-white) !important; color: #fff !important;
} }
/* Segments and boxes */ /* Segments and boxes */
@@ -164,43 +165,43 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.ui.segments, .ui.segments,
.repository, .repository,
.box { .box {
background: var(--ds-navy) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
.ui.attached.header, .ui.attached.header,
.box-header { .box-header {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
border-bottom: 2px solid var(--ds-slate) !important; border-bottom: 1px solid var(--border) !important;
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-display) !important;
font-size: 11px !important; font-size: 13px !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
} }
/* Tables */ /* Tables */
.ui.table { .ui.table {
background: var(--ds-navy) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
} }
.ui.table th { .ui.table th {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
font-size: 10px !important; font-size: 10px !important;
text-transform: uppercase !important; text-transform: uppercase !important;
border: 1px solid var(--ds-blue-light) !important; border: 1px solid var(--sidebar-accent) !important;
} }
.ui.table td { .ui.table td {
background: var(--ds-navy) !important; background: var(--card) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
border: 1px solid var(--ds-navy-light) !important; border: 1px solid var(--border) !important;
} }
.ui.table tr:hover td { .ui.table tr:hover td {
background: rgba(51, 102, 204, 0.2) !important; background: var(--secondary) !important;
} }
/* Forms */ /* Forms */
@@ -208,38 +209,70 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.ui.form textarea, .ui.form textarea,
.ui.form select, .ui.form select,
.ui.input input { .ui.input input {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
background: var(--ds-navy-mid) !important; background: var(--card) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
.ui.form input:focus, .ui.form input:focus,
.ui.form textarea:focus, .ui.form textarea:focus,
.ui.input input:focus { .ui.input input:focus {
border-color: var(--ds-blue-light) !important; border-color: var(--primary) !important;
background: var(--ds-navy-mid) !important; background: var(--card) !important;
} }
/* Labels */ /* Labels — mono for metadata labels (issue numbers, ref badges) */
.ui.label { .ui.label {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
border-radius: 4px !important; font-size: 11px !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Repo/issue titles — Playfair */
.repository .header,
.issue-title,
.repository h1,
.repository h2,
.repository h3 {
font-family: var(--font-display) !important;
font-weight: 700 !important;
}
/* PR and issue descriptions — serif body */
.comment-content,
.issue-content,
.render-content {
font-family: var(--font-body) !important;
font-size: 15px !important;
line-height: 1.65 !important;
}
/* Commit hashes, branch names, file paths — mono */
.commit-sha,
.sha,
[class*="sha"],
.branch-name,
.tag-name,
.file-name,
code.ref {
font-family: var(--font-mono) !important;
font-size: 12px !important;
} }
/* Dropdowns */ /* Dropdowns */
.ui.dropdown .menu { .ui.dropdown .menu {
background: var(--ds-navy-light) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
} }
.ui.dropdown .menu .item { .ui.dropdown .menu .item {
color: var(--ds-white) !important; color: var(--foreground) !important;
} }
.ui.dropdown .menu .item:hover { .ui.dropdown .menu .item:hover {
background: var(--ds-blue) !important; background: var(--secondary) !important;
} }
/* Code and diffs */ /* Code and diffs */
@@ -247,101 +280,101 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.file-view, .file-view,
.CodeMirror, .CodeMirror,
.highlight { .highlight {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
background: var(--ds-navy) !important; background: var(--card) !important;
} }
.diff-file-box .diff-file-header { .diff-file-box .diff-file-header {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
} }
/* Commit graph */ /* Commit graph */
.repository .commit-list .commit { .repository .commit-list .commit {
border-bottom: 1px solid var(--ds-navy-light) !important; border-bottom: 1px solid var(--border) !important;
} }
/* Issues and PRs */ /* Issues and PRs */
.issue-list .item, .issue-list .item,
.pull-list .item { .pull-list .item {
border-bottom: 1px solid var(--ds-navy-light) !important; border-bottom: 1px solid var(--border) !important;
} }
/* Footer */ /* Footer */
.footer { .footer {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
border-top: 3px solid var(--ds-slate) !important; border-top: 3px solid var(--foreground) !important;
} }
/* Cards */ /* Cards */
.ui.card, .ui.card,
.ui.cards .card { .ui.cards .card {
background: var(--ds-navy) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
/* Messages */ /* Messages */
.ui.message { .ui.message {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
.ui.positive.message, .ui.positive.message,
.ui.success.message { .ui.success.message {
background: rgba(68, 170, 68, 0.2) !important; background: color-mix(in srgb, var(--chart-4) 12%, var(--background)) !important;
border: 2px solid var(--ds-green) !important; border: 1px solid var(--chart-4) !important;
color: var(--ds-green-light) !important; color: var(--chart-4) !important;
} }
.ui.negative.message, .ui.negative.message,
.ui.error.message { .ui.error.message {
background: rgba(204, 34, 68, 0.2) !important; background: color-mix(in srgb, var(--destructive) 10%, var(--background)) !important;
border: 2px solid var(--ds-red) !important; border: 1px solid var(--destructive) !important;
color: var(--ds-red-light) !important; color: var(--destructive) !important;
} }
.ui.warning.message { .ui.warning.message {
background: rgba(221, 170, 51, 0.2) !important; background: color-mix(in srgb, var(--chart-5) 10%, var(--background)) !important;
border: 2px solid var(--ds-yellow) !important; border: 1px solid var(--chart-5) !important;
color: var(--ds-yellow) !important; color: var(--chart-5) !important;
} }
.ui.info.message { .ui.info.message {
background: rgba(85, 136, 221, 0.2) !important; background: color-mix(in srgb, var(--primary) 10%, var(--background)) !important;
border: 2px solid var(--ds-blue-light) !important; border: 1px solid var(--primary) !important;
color: var(--ds-blue-bright) !important; color: var(--primary) !important;
} }
/* Scrollbars */ /* Scrollbars */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 10px;
height: 12px; height: 10px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--ds-navy-light); background: var(--secondary);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--ds-slate); background: var(--border);
border-radius: 6px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--ds-steel); background: var(--muted-foreground);
} }
/* Selection */ /* Selection */
::selection { ::selection {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
} }
/* Links */ /* Links */
a { a {
color: var(--ds-blue-bright) !important; color: var(--primary) !important;
} }
a:hover { a:hover {
color: var(--ds-white) !important; color: var(--foreground) !important;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
/* LOCH — deep-navy design system
Single source of truth for the colour palette.
When changing a color here, propagate to the service-specific files:
inject.css, dashboard.css, gitea.css, woodpecker.css, marimo.css */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root {
--ds-navy: #0a1628;
--ds-navy-light: #162040;
--ds-navy-mid: #1e2d50;
--ds-slate: #2a3f5f;
--ds-steel: #7a90b0;
--ds-white: #f0f0f0;
--ds-blue: #4488dd;
--ds-blue-light: #5588dd;
--ds-blue-bright: #88aaee;
--ds-red: #ee5577;
--ds-red-light: #dd4466;
--ds-green: #44aa44;
--ds-green-light: #66cc66;
--ds-yellow: #ddaa33;
}
/* ── WCAG 2.2 AA — Accessibility ──────────────────────────── */
/* Focus indicators (2.4.7, 2.4.11) */
:focus-visible {
outline: 2px solid #4488dd !important;
outline-offset: 2px !important;
}
/* Reduced motion (2.3.3) */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Minimum target size (2.5.8) */
button, [role="button"], a, input, select, textarea {
min-height: 24px;
min-width: 24px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,71 @@
/* LOCH — deep-navy design system for Woodpecker CI */ /* HTI-5 — Federal Register editorial design system for Woodpecker CI */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root { :root {
/* Loch Color Palette */ /* HTI-5 Color Palette */
--ds-navy: #0a1628; --background: #F7F5F0;
--ds-navy-light: #162040; --foreground: #1A1A18;
--ds-navy-mid: #1e2d50; --card: #FAFAF7;
--ds-slate: #2a3f5f; --primary: #1C2B3A;
--ds-steel: #7a90b0; --primary-foreground: #F7F5F0;
--ds-white: #f0f0f0; --secondary: #EDEBE6;
--muted-foreground: #6B6B68;
--border: #D4D0C8;
--sidebar: #1C2B3A;
--sidebar-foreground: #B8C5D0;
--sidebar-accent: #253748;
--sidebar-accent-foreground: #EEE9E0;
--destructive: #C0392B;
--chart-4: #2E8B6E;
--chart-5: #C8702A;
--ds-blue: #4488dd; --font-display: "Playfair Display", Georgia, serif;
--ds-blue-light: #5588dd; --font-body: "Source Serif 4", Georgia, serif;
--ds-blue-bright: #88aaee; --font-mono: "JetBrains Mono", "Fira Code", monospace;
--ds-red: #ee5577; /* Woodpecker color overrides — mapped from hti5 tokens */
--ds-red-light: #dd4466; --color-wp-primary-100: #B8C5D0;
--color-wp-primary-200: #7A96AE;
--color-wp-primary-300: #1C2B3A;
--color-wp-primary-400: #162030;
--color-wp-primary-500: #0E1520;
--ds-green: #44aa44; --color-wp-success-100: #56B397;
--ds-green-light: #66cc66; --color-wp-success-200: #2E8B6E;
--ds-yellow: #ddaa33;
/* Woodpecker color overrides */ --color-wp-error-100: #D9534F;
--color-wp-primary-100: var(--ds-blue-bright); --color-wp-error-200: #C0392B;
--color-wp-primary-200: var(--ds-blue-light);
--color-wp-primary-300: var(--ds-blue);
--color-wp-primary-400: #2255aa;
--color-wp-primary-500: var(--ds-navy);
--color-wp-success-100: var(--ds-green-light); --color-wp-warning-100: #DDA060;
--color-wp-success-200: var(--ds-green); --color-wp-warning-200: #C8702A;
--color-wp-error-100: var(--ds-red-light); --color-wp-gray-100: #1A1A18;
--color-wp-error-200: var(--ds-red); --color-wp-gray-200: #6B6B68;
--color-wp-gray-300: #D4D0C8;
--color-wp-warning-100: var(--ds-yellow); --color-wp-gray-400: #EDEBE6;
--color-wp-warning-200: #bb8822; --color-wp-gray-500: #F7F5F0;
--color-wp-gray-600: #FAFAF7;
--color-wp-gray-100: var(--ds-white);
--color-wp-gray-200: var(--ds-steel);
--color-wp-gray-300: var(--ds-slate);
--color-wp-gray-400: var(--ds-navy-mid);
--color-wp-gray-500: var(--ds-navy-light);
--color-wp-gray-600: var(--ds-navy);
} }
/* Global styles */ /* Global styles */
body, body,
html { html {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
background: var(--ds-navy) !important; background: var(--background) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
} }
/* Headings */ /* Headings */
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-display) !important;
font-weight: 600 !important; font-weight: 600 !important;
letter-spacing: 0.5px !important; line-height: 1.3 !important;
line-height: 1.8 !important;
} }
h1 { font-size: 18px !important; color: var(--ds-white) !important; } h1 { font-size: 22px !important; color: var(--foreground) !important; }
h2 { font-size: 14px !important; color: var(--ds-blue-bright) !important; } h2 { font-size: 17px !important; color: var(--primary) !important; }
h3 { font-size: 12px !important; color: var(--ds-white) !important; } h3 { font-size: 14px !important; color: var(--foreground) !important; }
/* Header/Navbar */ /* Header/Navbar */
header, header,
@@ -72,16 +73,16 @@ nav,
.navbar, .navbar,
[class*="header"], [class*="header"],
[class*="Header"] { [class*="Header"] {
background: var(--ds-navy-light) !important; background: var(--sidebar) !important;
border-bottom: 3px solid var(--ds-slate) !important; border-bottom: 3px solid var(--foreground) !important;
} }
/* Sidebar */ /* Sidebar */
aside, aside,
[class*="sidebar"], [class*="sidebar"],
[class*="Sidebar"] { [class*="Sidebar"] {
background: var(--ds-navy) !important; background: var(--sidebar) !important;
border-right: 3px solid var(--ds-slate) !important; border-right: 1px solid var(--sidebar-border, var(--sidebar-accent)) !important;
} }
/* Main content area */ /* Main content area */
@@ -98,20 +99,20 @@ main,
[class*="Panel"], [class*="Panel"],
[class*="box"], [class*="box"],
[class*="Box"] { [class*="Box"] {
background: var(--ds-navy) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
/* Buttons */ /* Buttons — serif body for button labels */
button, button,
[class*="btn"], [class*="btn"],
[class*="Btn"], [class*="Btn"],
[class*="button"], [class*="button"],
[class*="Button"] { [class*="Button"] {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
font-weight: 500 !important; font-weight: 500 !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
transition: all 0.15s ease !important; transition: all 0.15s ease !important;
} }
@@ -119,16 +120,16 @@ button,
[class*="primary"], [class*="primary"],
[class*="Primary"], [class*="Primary"],
button[type="submit"] { button[type="submit"] {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
border: 2px solid var(--ds-blue-light) !important; border: 1px solid var(--primary) !important;
} }
[class*="primary"]:hover, [class*="primary"]:hover,
[class*="Primary"]:hover, [class*="Primary"]:hover,
button[type="submit"]:hover { button[type="submit"]:hover {
background: var(--ds-blue-light) !important; background: var(--sidebar-accent) !important;
color: var(--ds-white) !important; color: var(--sidebar-accent-foreground) !important;
} }
/* Success/green buttons */ /* Success/green buttons */
@@ -136,15 +137,14 @@ button[type="submit"]:hover {
[class*="Success"], [class*="Success"],
[class*="green"], [class*="green"],
[class*="Green"] { [class*="Green"] {
background: var(--ds-green) !important; background: var(--chart-4) !important;
color: var(--ds-white) !important; color: #fff !important;
border: 2px solid var(--ds-green-light) !important; border: 1px solid var(--chart-4) !important;
} }
[class*="success"]:hover, [class*="success"]:hover,
[class*="Success"]:hover { [class*="Success"]:hover {
background: var(--ds-green-light) !important; opacity: 0.85 !important;
color: var(--ds-navy) !important;
} }
/* Danger/red buttons */ /* Danger/red buttons */
@@ -154,60 +154,82 @@ button[type="submit"]:hover {
[class*="Error"], [class*="Error"],
[class*="red"], [class*="red"],
[class*="Red"] { [class*="Red"] {
background: var(--ds-red) !important; background: var(--destructive) !important;
color: var(--ds-white) !important; color: #fff !important;
} }
/* Forms */ /* Forms */
input, input,
textarea, textarea,
select { select {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
background: var(--ds-navy-mid) !important; background: var(--card) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
padding: 8px 12px !important; padding: 8px 12px !important;
} }
/* Pipeline/repo names — Playfair for titles */
[class*="pipeline-name"],
[class*="PipelineName"],
[class*="repo-name"],
[class*="RepoName"] {
font-family: var(--font-display) !important;
font-weight: 700 !important;
}
/* Commit hashes, build numbers, durations — mono */
[class*="commit-hash"],
[class*="CommitHash"],
[class*="build-number"],
[class*="BuildNumber"],
[class*="duration"],
[class*="Duration"],
[class*="timestamp"],
[class*="Timestamp"] {
font-family: var(--font-mono) !important;
font-size: 12px !important;
}
input:focus, input:focus,
textarea:focus, textarea:focus,
select:focus { select:focus {
border-color: var(--ds-blue-light) !important; border-color: var(--primary) !important;
outline: none !important; outline: none !important;
box-shadow: 0 0 0 2px rgba(85, 136, 221, 0.3) !important; box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent) !important;
} }
/* Tables */ /* Tables */
table { table {
background: var(--ds-navy) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-collapse: collapse !important; border-collapse: collapse !important;
} }
th { th {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
font-size: 10px !important; font-size: 10px !important;
text-transform: uppercase !important; text-transform: uppercase !important;
padding: 10px 12px !important; padding: 10px 12px !important;
border: 1px solid var(--ds-blue-light) !important; border: 1px solid var(--sidebar-accent) !important;
} }
td { td {
background: var(--ds-navy) !important; background: var(--card) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
padding: 8px 12px !important; padding: 8px 12px !important;
border: 1px solid var(--ds-navy-light) !important; border: 1px solid var(--border) !important;
} }
tr:nth-child(even) td { tr:nth-child(even) td {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
} }
tr:hover td { tr:hover td {
background: rgba(51, 102, 204, 0.2) !important; background: var(--secondary) !important;
} }
/* Pipeline/build status colors */ /* Pipeline/build status colors */
@@ -215,14 +237,14 @@ tr:hover td {
[class*="Running"], [class*="Running"],
[class*="pending"], [class*="pending"],
[class*="Pending"] { [class*="Pending"] {
color: var(--ds-yellow) !important; color: var(--chart-5) !important;
} }
[class*="success"], [class*="success"],
[class*="Success"], [class*="Success"],
[class*="passed"], [class*="passed"],
[class*="Passed"] { [class*="Passed"] {
color: var(--ds-green-light) !important; color: var(--chart-4) !important;
} }
[class*="failure"], [class*="failure"],
@@ -231,7 +253,7 @@ tr:hover td {
[class*="Failed"], [class*="Failed"],
[class*="error"], [class*="error"],
[class*="Error"] { [class*="Error"] {
color: var(--ds-red) !important; color: var(--destructive) !important;
} }
/* Build logs */ /* Build logs */
@@ -241,29 +263,29 @@ code,
[class*="Log"], [class*="Log"],
[class*="console"], [class*="console"],
[class*="Console"] { [class*="Console"] {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
background: var(--ds-navy) !important; background: var(--secondary) !important;
color: var(--ds-white) !important; color: var(--foreground) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
/* Inline code */ /* Inline code */
code { code {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
color: var(--ds-blue-bright) !important; color: var(--primary) !important;
padding: 2px 6px !important; padding: 2px 6px !important;
border: none !important; border: none !important;
} }
/* Links */ /* Links */
a { a {
color: var(--ds-blue-bright) !important; color: var(--primary) !important;
text-decoration: none !important; text-decoration: none !important;
} }
a:hover { a:hover {
color: var(--ds-white) !important; color: var(--foreground) !important;
text-decoration: underline !important; text-decoration: underline !important;
} }
@@ -274,16 +296,16 @@ a:hover {
[class*="Menu"], [class*="Menu"],
[class*="popover"], [class*="popover"],
[class*="Popover"] { [class*="Popover"] {
background: var(--ds-navy-light) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
[class*="dropdown"] li:hover, [class*="dropdown"] li:hover,
[class*="menu"] li:hover, [class*="menu"] li:hover,
[role="menuitem"]:hover, [role="menuitem"]:hover,
[role="option"]:hover { [role="option"]:hover {
background: var(--ds-blue) !important; background: var(--secondary) !important;
} }
/* Badges and tags */ /* Badges and tags */
@@ -293,62 +315,63 @@ a:hover {
[class*="Tag"], [class*="Tag"],
[class*="label"], [class*="label"],
[class*="Label"] { [class*="Label"] {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-mono) !important;
font-size: 11px !important; font-size: 11px !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
/* Tooltips */ /* Tooltips */
[role="tooltip"], [role="tooltip"],
[class*="tooltip"], [class*="tooltip"],
[class*="Tooltip"] { [class*="Tooltip"] {
font-family: 'IBM Plex Mono', monospace !important; font-family: var(--font-body) !important;
background: var(--ds-navy-light) !important; font-size: 13px !important;
color: var(--ds-white) !important; background: var(--card) !important;
border: 2px solid var(--ds-slate) !important; color: var(--foreground) !important;
border-radius: 4px !important; border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
} }
/* Progress bars */ /* Progress bars */
progress, progress,
[class*="progress"], [class*="progress"],
[class*="Progress"] { [class*="Progress"] {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
progress::-webkit-progress-bar { progress::-webkit-progress-bar {
background: var(--ds-navy-light) !important; background: var(--secondary) !important;
} }
progress::-webkit-progress-value { progress::-webkit-progress-value {
background: var(--ds-blue) !important; background: var(--primary) !important;
} }
/* Scrollbars */ /* Scrollbars */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 10px;
height: 12px; height: 10px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--ds-navy-light); background: var(--secondary);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--ds-slate); background: var(--border);
border-radius: 6px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--ds-steel); background: var(--muted-foreground);
} }
/* Selection */ /* Selection */
::selection { ::selection {
background: var(--ds-blue) !important; background: var(--primary) !important;
color: var(--ds-white) !important; color: var(--primary-foreground) !important;
} }
/* Icons - ensure visibility */ /* Icons - ensure visibility */
@@ -361,8 +384,8 @@ svg,
/* Avatar images */ /* Avatar images */
img[class*="avatar"], img[class*="avatar"],
[class*="avatar"] img { [class*="avatar"] img {
border: 2px solid var(--ds-slate) !important; border: 1px solid var(--border) !important;
border-radius: 4px !important; border-radius: var(--radius, 0.2rem) !important;
} }
/* Repo/pipeline list items */ /* Repo/pipeline list items */
@@ -370,10 +393,10 @@ img[class*="avatar"],
[class*="Item"], [class*="Item"],
[class*="list-item"], [class*="list-item"],
[class*="ListItem"] { [class*="ListItem"] {
border-bottom: 1px solid var(--ds-navy-light) !important; border-bottom: 1px solid var(--border) !important;
} }
[class*="item"]:hover, [class*="item"]:hover,
[class*="Item"]:hover { [class*="Item"]:hover {
background: rgba(51, 102, 204, 0.1) !important; background: var(--secondary) !important;
} }

View File

@@ -3,23 +3,32 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>ACO Pipeline DAG</title> <title>ACO Pipeline DAG</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap">
<script src="https://unpkg.com/cytoscape@3/dist/cytoscape.min.js"></script> <script src="https://unpkg.com/cytoscape@3/dist/cytoscape.min.js"></script>
<script src="https://unpkg.com/dagre@0.8/dist/dagre.min.js"></script> <script src="https://unpkg.com/dagre@0.8/dist/dagre.min.js"></script>
<script src="https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js"></script> <script src="https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js"></script>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; } /* HTI-5 palette */
:root {
--bg: #F7F5F0; --panel: #FAFAF7; --border: #D4D0C8;
--text: #1A1A18; --muted: #6B6B68; --navy: #1C2B3A;
}
body { background: var(--bg); color: var(--text); font-family: "Source Serif 4", Georgia, serif; }
#cy { width: 100vw; height: 100vh; } #cy { width: 100vw; height: 100vh; }
#controls { #controls {
position: fixed; top: 12px; left: 12px; z-index: 10; position: fixed; top: 12px; left: 12px; z-index: 10;
background: #16213e; border: 1px solid #333; border-radius: 8px; background: var(--navy); border: 1px solid #253748; border-radius: 0;
padding: 12px; max-width: 260px; font-size: 13px; padding: 12px; max-width: 260px; font-size: 13px;
color: #B8C5D0;
} }
#controls h3 { margin-bottom: 8px; font-size: 14px; color: #fff; } #controls h3 { margin-bottom: 8px; font-size: 14px; color: #EEE9E0; font-family: "Playfair Display", Georgia, serif; }
#search { #search {
width: 100%; padding: 6px 8px; border-radius: 4px; width: 100%; padding: 6px 8px; border-radius: 0;
border: 1px solid #444; background: #0f3460; color: #e0e0e0; border: 1px solid #253748; background: #253748; color: #EEE9E0;
margin-bottom: 8px; font-size: 13px; margin-bottom: 8px; font-size: 13px; font-family: "JetBrains Mono", monospace;
} }
#legend { display: flex; flex-wrap: wrap; gap: 4px; } #legend { display: flex; flex-wrap: wrap; gap: 4px; }
.legend-item { .legend-item {
@@ -33,16 +42,16 @@
display: inline-block; flex-shrink: 0; display: inline-block; flex-shrink: 0;
} }
#tooltip { #tooltip {
position: fixed; display: none; background: #16213e; position: fixed; display: none; background: var(--panel);
border: 1px solid #444; border-radius: 6px; padding: 10px; border: 1px solid var(--border); border-radius: 0; padding: 10px;
max-width: 340px; font-size: 12px; z-index: 20; max-width: 340px; font-size: 12px; z-index: 20;
pointer-events: none; line-height: 1.4; pointer-events: none; line-height: 1.4;
} }
#tooltip .tt-name { font-weight: bold; color: #fff; margin-bottom: 4px; } #tooltip .tt-name { font-weight: bold; color: var(--text); margin-bottom: 4px; font-family: "Playfair Display", Georgia, serif; }
#tooltip .tt-desc { color: #bbb; } #tooltip .tt-desc { color: var(--muted); }
#stats { #stats {
position: fixed; bottom: 12px; left: 12px; z-index: 10; position: fixed; bottom: 12px; left: 12px; z-index: 10;
font-size: 11px; color: #666; font-size: 11px; color: var(--muted); font-family: "JetBrains Mono", monospace;
} }
</style> </style>
</head> </head>
@@ -5911,12 +5920,13 @@
"background-color": "transparent", "background-color": "transparent",
"background-opacity": 0.05, "background-opacity": 0.05,
"border-width": 1, "border-width": 1,
"border-color": "#444", "border-color": "#D4D0C8",
"label": "data(label)", "label": "data(label)",
"text-valign": "top", "text-valign": "top",
"text-halign": "center", "text-halign": "center",
"font-size": "14px", "font-size": "13px",
"color": "#666", "font-family": "Playfair Display, Georgia, serif",
"color": "#6B6B68",
"padding": "16px", "padding": "16px",
"shape": "round-rectangle", "shape": "round-rectangle",
} }
@@ -5948,15 +5958,16 @@
{ {
selector: "node.external", selector: "node.external",
style: { style: {
"background-color": "#2a2a2a", "background-color": "#EDEBE6",
"border-width": 1, "border-width": 1,
"border-style": "dashed", "border-style": "dashed",
"border-color": "#666", "border-color": "#D4D0C8",
"label": "data(label)", "label": "data(label)",
"text-valign": "center", "text-valign": "center",
"text-halign": "center", "text-halign": "center",
"font-size": "8px", "font-size": "8px",
"color": "#888", "font-family": "JetBrains Mono, Fira Code, monospace",
"color": "#6B6B68",
"width": "label", "width": "label",
"height": 24, "height": 24,
"padding": "6px", "padding": "6px",
@@ -5967,8 +5978,8 @@
selector: "edge", selector: "edge",
style: { style: {
"width": 1.5, "width": 1.5,
"line-color": "#444", "line-color": "#D4D0C8",
"target-arrow-color": "#444", "target-arrow-color": "#D4D0C8",
"target-arrow-shape": "triangle", "target-arrow-shape": "triangle",
"curve-style": "bezier", "curve-style": "bezier",
"arrow-scale": 0.8, "arrow-scale": 0.8,
@@ -5977,16 +5988,16 @@
{ {
selector: ".highlighted", selector: ".highlighted",
style: { style: {
"border-color": "#fff", "border-color": "#1C2B3A",
"border-width": 3, "border-width": 3,
"color": "#fff", "color": "#1C2B3A",
} }
}, },
{ {
selector: "edge.highlighted", selector: "edge.highlighted",
style: { style: {
"line-color": "#e0e0e0", "line-color": "#1C2B3A",
"target-arrow-color": "#e0e0e0", "target-arrow-color": "#1C2B3A",
"width": 2.5, "width": 2.5,
} }
}, },

235
assets/fhirworx.py Normal file
View File

@@ -0,0 +1,235 @@
"""HTI-5 Federal Register design system — shared palette and theme.
Design: Federal Register Meets Data Journalism.
Inspired by ProPublica, The Marshall Project, and the Federal Register.
Provides:
- ``PALETTE`` — 8-colour chart sequence (indigo scale + teal + amber + plum + gray + sepia)
- ``SCHEMA_COLORS`` — pipeline schema → colour mapping for DAG viz
- ``altair_theme()`` — register-and-enable for Altair charts
- ``mpl_rc()`` — dict of matplotlib rcParams
- ``cytoscape_style()`` — Cytoscape.js stylesheet fragment
Design principles (HTI-5):
* Typography-first: Playfair Display for display, Source Serif 4 for body
* Off-white (#F7F5F0) background, near-black (#1A1A18) text
* Deep federal navy sidebar (#1C2B3A)
* Warm amber accent for opposition, teal for support
* Monospace font (JetBrains Mono) for IDs, labels, metadata
"""
from __future__ import annotations
# ── Chart colour palette ──────────────────────────────────────────────
# Eight-colour categorical sequence for multi-series charts.
# First five match the hti5 --chart-* tokens (indigo scale + teal + amber);
# remaining three extend the editorial palette for categorical variety.
PALETTE = [
"#6B7FD7", # indigo medium (chart-1)
"#4A5CB8", # indigo dark (chart-2)
"#2E3D8F", # indigo deep (chart-3)
"#2E8B6E", # teal / support (chart-4)
"#C8702A", # amber / oppose (chart-5)
"#7448C0", # plum
"#6B6B68", # neutral gray
"#A85800", # sepia / dark amber
]
# Semantic aliases
INDIGO_MED, INDIGO_DARK, INDIGO_DEEP, TEAL, AMBER, PLUM, GRAY, SEPIA = PALETTE
# ── Core palette ──────────────────────────────────────────────────────
# Backgrounds and text following hti5 design tokens
BG_LIGHT = "#F7F5F0" # --background: off-white cream
BG_CARD = "#FAFAF7" # --card: slightly warmer white
BG_DARK = "#1C2B3A" # --primary / sidebar: deep federal navy
BG_CHART = "transparent" # chart area — let container bg show through
AXIS_COLOR = "#D4D0C8" # --border: warm gray
TEXT_COLOR = "#1A1A18" # --foreground: near-black
TITLE_COLOR = "#1A1A18" # --foreground for titles
MUTED_COLOR = "#6B6B68" # --muted-foreground
# ── Typography ────────────────────────────────────────────────────────
FONT_DISPLAY = "Playfair Display, Georgia, serif"
FONT_BODY = "Source Serif 4, Georgia, serif"
FONT_MONO = "JetBrains Mono, Fira Code, monospace"
FONT_FAMILY = FONT_BODY # default for charts
FONT_SIZE_BODY = 11 # pt — readable at screen DPI
FONT_SIZE_AXIS = 11 # pt
FONT_SIZE_TITLE = 13 # pt
FONT_SIZE_LABEL = 12 # pt
LINE_WIDTH = 0.75 # pt — axis/tick stroke
TICK_SIZE = 4 # pt
# ── Figure dimensions (single-column width: 89 mm) ───────────────────
FIG_WIDTH_MM = 89
FIG_HEIGHT_MM = 60
FIG_WIDTH_IN = FIG_WIDTH_MM / 25.4
FIG_HEIGHT_IN = FIG_HEIGHT_MM / 25.4
DPI = 150 # screen-appropriate DPI
# ── Pipeline schema colours (for DAG / lineage viz) ──────────────────
SCHEMA_COLORS: dict[str, str] = {
"core": INDIGO_DEEP,
"input_layer": TEAL,
"cclf": INDIGO_DARK,
"claims_preprocessing": AMBER,
"readmissions": TEAL,
"ahrq_measures": INDIGO_MED,
"pharmacy": "#7448C0", # purple
"quality_measures": "#C0392B", # destructive / red
"hcc_suspecting": "#A85800", # warm amber-brown
"provider_attribution": MUTED_COLOR,
"main": INDIGO_MED,
"data_quality": INDIGO_DARK,
}
EXTERNAL_COLOR = "#9B9B98"
def schema_color(schema: str) -> str:
"""Return the palette colour for a pipeline schema."""
return SCHEMA_COLORS.get(schema, EXTERNAL_COLOR)
# ── Altair theme ──────────────────────────────────────────────────────
def _altair_theme_config() -> dict:
"""Vega-Lite config dict conforming to HTI-5 editorial specs."""
return {
"config": {
"background": BG_LIGHT,
"font": FONT_BODY,
"title": {
"font": FONT_DISPLAY,
"fontSize": FONT_SIZE_TITLE,
"fontWeight": "bold",
"color": TITLE_COLOR,
"anchor": "start",
},
"axis": {
"labelFont": FONT_MONO,
"labelFontSize": FONT_SIZE_AXIS,
"labelColor": MUTED_COLOR,
"titleFont": FONT_BODY,
"titleFontSize": FONT_SIZE_AXIS,
"titleColor": TEXT_COLOR,
"gridColor": AXIS_COLOR,
"gridOpacity": 0.5,
"domainColor": AXIS_COLOR,
"domainWidth": LINE_WIDTH,
"tickColor": AXIS_COLOR,
"tickSize": TICK_SIZE,
"tickWidth": LINE_WIDTH,
},
"legend": {
"labelFont": FONT_MONO,
"labelFontSize": FONT_SIZE_BODY,
"labelColor": TEXT_COLOR,
"titleFont": FONT_BODY,
"titleFontSize": FONT_SIZE_BODY,
"titleColor": TEXT_COLOR,
"symbolSize": 60,
},
"view": {
"stroke": AXIS_COLOR,
"strokeWidth": LINE_WIDTH,
"fill": BG_CARD,
},
"range": {
"category": PALETTE,
},
"mark": {
"color": PALETTE[0],
},
"bar": {
"color": PALETTE[0],
},
"arc": {
"stroke": BG_LIGHT,
"strokeWidth": 1,
},
"line": {
"strokeWidth": 1.5,
},
"point": {
"size": 40,
},
"area": {
"opacity": 0.7,
},
}
}
def altair_theme() -> None:
"""Register and enable the fhirworx Altair chart theme.
Uses the Altair 5.5+ ``alt.theme.register`` decorator API.
"""
import altair as alt
@alt.theme.register("fhirworx", enable=True)
def _fhirworx_theme() -> dict:
return _altair_theme_config()
# ── Matplotlib rcParams ──────────────────────────────────────────────
def mpl_rc() -> dict:
"""Return an rcParams dict for ``matplotlib.rcParams.update()``."""
return {
"font.family": "serif",
"font.serif": ["Source Serif 4", "Georgia", "DejaVu Serif"],
"font.size": FONT_SIZE_BODY,
"axes.titlesize": FONT_SIZE_TITLE,
"axes.titleweight": "bold",
"axes.titlefontproperties": {"family": "serif"},
"axes.labelsize": FONT_SIZE_AXIS,
"axes.linewidth": LINE_WIDTH,
"axes.edgecolor": AXIS_COLOR,
"axes.labelcolor": TEXT_COLOR,
"axes.facecolor": BG_CARD,
"axes.grid": True,
"axes.grid.axis": "y",
"axes.prop_cycle": __import__("cycler").cycler(color=PALETTE),
"grid.color": AXIS_COLOR,
"grid.linewidth": 0.5,
"grid.alpha": 0.6,
"xtick.major.size": TICK_SIZE,
"xtick.major.width": LINE_WIDTH,
"xtick.color": AXIS_COLOR,
"xtick.labelsize": FONT_SIZE_BODY,
"ytick.major.size": TICK_SIZE,
"ytick.major.width": LINE_WIDTH,
"ytick.color": AXIS_COLOR,
"ytick.labelsize": FONT_SIZE_BODY,
"figure.figsize": (FIG_WIDTH_IN, FIG_HEIGHT_IN),
"figure.dpi": DPI,
"figure.facecolor": BG_LIGHT,
"savefig.dpi": DPI,
"savefig.facecolor": BG_LIGHT,
"legend.fontsize": FONT_SIZE_BODY,
"legend.frameon": False,
"text.color": TEXT_COLOR,
}
# ── Cytoscape.js style helpers ───────────────────────────────────────
def cytoscape_node_style() -> dict:
"""Base Cytoscape.js node style properties (HTI-5 editorial style)."""
return {
"font-family": FONT_MONO,
"font-size": "9px",
"text-valign": "center",
"text-halign": "center",
"shape": "round-rectangle",
"background-color": BG_CARD,
"border-color": AXIS_COLOR,
"color": TEXT_COLOR,
}

View File

@@ -1,209 +0,0 @@
"""Nature Research figure standards — shared palette and theme.
Implements the specifications from Nature's "Preparing Figures" guide
(bib key ``9ASETLJ4``). Provides:
- ``PALETTE`` — 8-colour accessible sequence (colorblind-safe)
- ``SCHEMA_COLORS`` — pipeline schema → colour mapping for DAG viz
- ``altair_theme()`` — register-and-enable for Altair charts
- ``mpl_rc()`` — dict of matplotlib rcParams
- ``cytoscape_style()`` — Cytoscape.js stylesheet fragment
Key rules (Nature):
* Sans-serif typeface (Helvetica, Arial)
* Body text 57 pt; panel labels 8 pt bold lowercase
* Axis lines and tick marks mandatory
* No background gridlines, drop shadows, or decorative elements
* RGB colour space; accessible palette
* Units in parentheses on axis labels
"""
from __future__ import annotations
# ── Accessible 8-colour palette ──────────────────────────────────────
# Adapted from Paul Tol's "bright" qualitative scheme — verified
# colourblind-safe via Coblis and WCAG contrast tools.
PALETTE = [
"#4477AA", # blue
"#EE6677", # red / rose
"#228833", # green
"#CCBB44", # yellow
"#66CCEE", # cyan
"#AA3377", # purple
"#BBBBBB", # grey
"#EE8866", # orange
]
# Semantic aliases
BLUE, RED, GREEN, YELLOW, CYAN, PURPLE, GREY, ORANGE = PALETTE
# ── Typography ────────────────────────────────────────────────────────
FONT_FAMILY = "Helvetica, Arial, sans-serif"
FONT_SIZE_BODY = 7 # pt — Nature range 5-7
FONT_SIZE_AXIS = 7 # pt
FONT_SIZE_TITLE = 8 # pt — panel label size
FONT_SIZE_LABEL = 8 # pt — bold lowercase panel labels
LINE_WIDTH = 0.75 # pt — axis/tick stroke
TICK_SIZE = 4 # pt
# ── Figure dimensions (single-column Nature width: 89 mm) ───────────
FIG_WIDTH_MM = 89
FIG_HEIGHT_MM = 60
FIG_WIDTH_IN = FIG_WIDTH_MM / 25.4
FIG_HEIGHT_IN = FIG_HEIGHT_MM / 25.4
DPI = 450 # Nature min for exported figures
# ── Background ────────────────────────────────────────────────────────
BG_DARK = "#1a1a2e" # app/notebook dark background
BG_CHART = "transparent" # chart area — let container bg show through
AXIS_COLOR = "#cccccc" # axis lines, ticks
TEXT_COLOR = "#e0e0e0" # labels on dark bg
TITLE_COLOR = "#ffffff" # title / panel label
# ── Pipeline schema colours (for DAG / lineage viz) ──────────────────
SCHEMA_COLORS: dict[str, str] = {
"core": BLUE,
"input_layer": CYAN,
"cclf": "#5588CC",
"claims_preprocessing": ORANGE,
"readmissions": GREEN,
"ahrq_measures": YELLOW,
"pharmacy": PURPLE,
"quality_measures": RED,
"hcc_suspecting": "#AA7744",
"provider_attribution": GREY,
"main": "#55AA99",
"data_quality": "#5588CC",
}
EXTERNAL_COLOR = "#888888"
def schema_color(schema: str) -> str:
"""Return the palette colour for a pipeline schema."""
return SCHEMA_COLORS.get(schema, EXTERNAL_COLOR)
# ── Altair theme ──────────────────────────────────────────────────────
def _altair_theme() -> dict:
"""Vega-Lite config dict conforming to Nature specs."""
return {
"config": {
"background": BG_CHART,
"font": FONT_FAMILY,
"title": {
"font": FONT_FAMILY,
"fontSize": FONT_SIZE_TITLE,
"fontWeight": "bold",
"color": TITLE_COLOR,
"anchor": "start",
},
"axis": {
"labelFont": FONT_FAMILY,
"labelFontSize": FONT_SIZE_AXIS,
"labelColor": TEXT_COLOR,
"titleFont": FONT_FAMILY,
"titleFontSize": FONT_SIZE_AXIS,
"titleColor": TEXT_COLOR,
"gridColor": "transparent",
"gridOpacity": 0,
"domainColor": AXIS_COLOR,
"domainWidth": LINE_WIDTH,
"tickColor": AXIS_COLOR,
"tickSize": TICK_SIZE,
"tickWidth": LINE_WIDTH,
},
"legend": {
"labelFont": FONT_FAMILY,
"labelFontSize": FONT_SIZE_BODY,
"labelColor": TEXT_COLOR,
"titleFont": FONT_FAMILY,
"titleFontSize": FONT_SIZE_BODY,
"titleColor": TEXT_COLOR,
"symbolSize": 60,
},
"view": {
"stroke": "transparent",
},
"range": {
"category": PALETTE,
},
"mark": {
"color": PALETTE[0],
},
"bar": {
"color": PALETTE[0],
},
"arc": {
"stroke": BG_DARK,
"strokeWidth": 1,
},
"line": {
"strokeWidth": 1.5,
},
"point": {
"size": 40,
},
"area": {
"opacity": 0.7,
},
}
}
def altair_theme() -> None:
"""Register and enable the Nature theme for Altair."""
import altair as alt
alt.themes.register("nature", _altair_theme)
alt.themes.enable("nature")
# ── Matplotlib rcParams ──────────────────────────────────────────────
def mpl_rc() -> dict:
"""Return an rcParams dict for ``matplotlib.rcParams.update()``."""
return {
"font.family": "sans-serif",
"font.sans-serif": ["Helvetica", "Arial", "DejaVu Sans"],
"font.size": FONT_SIZE_BODY,
"axes.titlesize": FONT_SIZE_TITLE,
"axes.titleweight": "bold",
"axes.labelsize": FONT_SIZE_AXIS,
"axes.linewidth": LINE_WIDTH,
"axes.edgecolor": AXIS_COLOR,
"axes.labelcolor": TEXT_COLOR,
"axes.facecolor": "none",
"axes.grid": False,
"axes.prop_cycle": __import__("cycler").cycler(color=PALETTE),
"xtick.major.size": TICK_SIZE,
"xtick.major.width": LINE_WIDTH,
"xtick.color": AXIS_COLOR,
"xtick.labelsize": FONT_SIZE_BODY,
"ytick.major.size": TICK_SIZE,
"ytick.major.width": LINE_WIDTH,
"ytick.color": AXIS_COLOR,
"ytick.labelsize": FONT_SIZE_BODY,
"figure.figsize": (FIG_WIDTH_IN, FIG_HEIGHT_IN),
"figure.dpi": DPI,
"figure.facecolor": BG_DARK,
"savefig.dpi": DPI,
"savefig.facecolor": BG_DARK,
"legend.fontsize": FONT_SIZE_BODY,
"legend.frameon": False,
"text.color": TEXT_COLOR,
}
# ── Cytoscape.js style helpers ───────────────────────────────────────
def cytoscape_node_style() -> dict:
"""Base Cytoscape.js node style properties (Nature-conformant)."""
return {
"font-family": FONT_FAMILY,
"font-size": "9px",
"text-valign": "center",
"text-halign": "center",
"shape": "round-rectangle",
}

View File

@@ -3,20 +3,25 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>PFS Equation Explorer</title> <title>PFS Equation Explorer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500;600&display=swap">
<link rel="stylesheet" href="https://unpkg.com/katex@0.16/dist/katex.min.css"> <link rel="stylesheet" href="https://unpkg.com/katex@0.16/dist/katex.min.css">
<script src="https://unpkg.com/katex@0.16/dist/katex.min.js"></script> <script src="https://unpkg.com/katex@0.16/dist/katex.min.js"></script>
<style> <style>
:root { :root {
--bg: #1a1a2e; --panel: #16213e; --panel-hi: #0f3460; /* HTI-5 palette */
--border: #333; --text: #e0e0e0; --muted: #888; --dim: #666; --bg: #F7F5F0; --panel: #FAFAF7; --panel-hi: #EDEBE6;
--c-mul: #e15759; --c-dot: #59a14f; --c-add: #4e79a7; --border: #D4D0C8; --text: #1A1A18; --muted: #6B6B68; --dim: #9B9B98;
--c-vec: #edc948; --c-scalar: #b07aa1; --c-ref: #76b7b2; /* Node-kind colours — Tableau-style but on light bg */
--c-fn: #f28e2b; --c-cond: #ff9da7; --c-mul: #C0392B; --c-dot: #2E8B6E; --c-add: #2E3D8F;
--c-vec: #A85800; --c-scalar: #7448C0; --c-ref: #1A7090;
--c-fn: #C8702A; --c-cond: #9B2E5A;
} }
* { margin:0; padding:0; box-sizing:border-box; } * { margin:0; padding:0; box-sizing:border-box; }
body { body {
background: var(--bg); color: var(--text); background: var(--bg); color: var(--text);
font-family: 'Inter', system-ui, -apple-system, sans-serif; font-family: "Source Serif 4", Georgia, serif;
overflow-x: hidden; overflow-x: hidden;
} }
@@ -26,7 +31,8 @@
display: flex; align-items: baseline; gap: 16px; display: flex; align-items: baseline; gap: 16px;
} }
header h1 { header h1 {
font-size: 20px; font-weight: 600; color: #fff; font-size: 20px; font-weight: 700; color: var(--text);
font-family: "Playfair Display", Georgia, serif;
letter-spacing: -0.3px; letter-spacing: -0.3px;
} }
header .subtitle { header .subtitle {
@@ -46,7 +52,7 @@
} }
.tab:hover { color: var(--text); background: var(--panel); } .tab:hover { color: var(--text); background: var(--panel); }
.tab.active { .tab.active {
color: #fff; background: var(--panel); color: var(--text); background: var(--panel);
border-color: var(--border); border-color: var(--border);
} }
@@ -114,7 +120,7 @@
color: var(--dim); font-size: 13px; padding: 40px 0; text-align: center; color: var(--dim); font-size: 13px; padding: 40px 0; text-align: center;
} }
.detail h3 { .detail h3 {
font-size: 14px; color: #fff; margin-bottom: 12px; font-size: 14px; color: var(--text); margin-bottom: 12px;
font-weight: 600; letter-spacing: -0.2px; font-weight: 600; letter-spacing: -0.2px;
} }
.detail-latex { .detail-latex {
@@ -122,7 +128,7 @@
margin-bottom: 14px; text-align: center; overflow-x: auto; margin-bottom: 14px; text-align: center; overflow-x: auto;
} }
.detail-doc { .detail-doc {
font-size: 13px; color: #bbb; line-height: 1.6; margin-bottom: 12px; font-size: 13px; color: var(--muted); line-height: 1.6; margin-bottom: 12px;
} }
.detail-meta { font-size: 12px; color: var(--muted); } .detail-meta { font-size: 12px; color: var(--muted); }
.detail-meta div { margin-bottom: 5px; } .detail-meta div { margin-bottom: 5px; }
@@ -148,7 +154,7 @@
border-radius: 10px; padding: 20px; margin-top: 20px; border-radius: 10px; padding: 20px; margin-top: 20px;
} }
.evaluator h3 { .evaluator h3 {
font-size: 14px; color: #fff; margin-bottom: 14px; font-weight: 600; font-size: 14px; color: var(--text); margin-bottom: 14px; font-weight: 600;
} }
.eval-grid { .eval-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
@@ -169,7 +175,7 @@
outline: none; border-color: var(--c-add); outline: none; border-color: var(--c-add);
} }
.eval-result { .eval-result {
font-size: 22px; font-weight: 600; color: #fff; font-size: 22px; font-weight: 600; color: var(--text);
display: flex; align-items: baseline; gap: 8px; display: flex; align-items: baseline; gap: 8px;
} }
.eval-result small { .eval-result small {

View File

@@ -165,13 +165,13 @@ services:
- GITEA__server__ROOT_URL=https://git.${DOMAIN:-fhirworx.io}/ - GITEA__server__ROOT_URL=https://git.${DOMAIN:-fhirworx.io}/
- GITEA__server__SSH_DOMAIN=git.${DOMAIN:-fhirworx.io} - GITEA__server__SSH_DOMAIN=git.${DOMAIN:-fhirworx.io}
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16 - GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,loch - GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,fhirworx
- GITEA__ui__DEFAULT_THEME=loch - GITEA__ui__DEFAULT_THEME=fhirworx
volumes: volumes:
- gitea_data:/var/lib/gitea - gitea_data:/var/lib/gitea
- gitea_config:/etc/gitea - gitea_config:/etc/gitea
- ./infra/gitea/custom:/var/lib/gitea/custom - ./infra/gitea/custom:/var/lib/gitea/custom
- ./assets/css/gitea.css:/var/lib/gitea/custom/public/assets/css/theme-loch.css:ro - ./assets/css/gitea.css:/var/lib/gitea/custom/public/assets/css/theme-fhirworx.css:ro
ports: ports:
- "2222:2222" - "2222:2222"
- "3000:3000" - "3000:3000"
@@ -459,7 +459,7 @@ services:
- ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./infra/nginx/index.html:/usr/share/nginx/html/index.html: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/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
- ./assets/css/inject.css:/usr/share/nginx/html/loch.css:ro - ./assets/css/inject.css:/usr/share/nginx/html/fhirworx.css:ro
- ./assets/icons/fav32.png:/usr/share/nginx/html/fav32.png:ro - ./assets/icons/fav32.png:/usr/share/nginx/html/fav32.png:ro
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true

View File

@@ -1,4 +1,4 @@
/* LOCH — deep-navy design system — matches assets/css/inject.css */ /* fhirworx design system — matches assets/css/inject.css */
:root { :root {
--ifm-color-primary: #4488dd; --ifm-color-primary: #4488dd;

View File

@@ -1,59 +1,105 @@
{{template "base/head" .}} {{template "base/head" .}}
<style> <style>
/* HTI-5 dashboard tokens — defined locally so the inline template is self-contained.
gitea.css maps Gitea's --color-* vars to hti5 tokens at load time. */
.homelab-dashboard { .homelab-dashboard {
max-width: 1400px; max-width: 1180px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 3rem 1.5rem;
font-family: "Source Serif 4", Georgia, serif;
} }
.homelab-dashboard .hdr {
text-align: center; /* Rule lines */
padding: 40px 20px; .homelab-dashboard .rule-line {
border-bottom: 4px solid var(--color-red); border: none;
border-top: 3px solid var(--color-text);
margin-bottom: 1rem;
} }
.homelab-dashboard .hdr h1 { .homelab-dashboard .rule-line-thin {
font-family: 'IBM Plex Mono', monospace !important; border: none;
font-size: 2rem !important; border-top: 1px solid var(--color-border);
margin-bottom: 1rem;
}
/* Masthead */
.homelab-dashboard .masthead {
margin-bottom: 1.5rem;
}
.homelab-dashboard .masthead-row {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.homelab-dashboard .masthead-row h1 {
font-family: "Playfair Display", Georgia, serif !important;
font-size: clamp(2.5rem, 5vw, 3.5rem) !important;
font-weight: 800 !important;
color: var(--color-text) !important; color: var(--color-text) !important;
letter-spacing: 6px !important; letter-spacing: -0.02em !important;
text-shadow: 3px 3px 0 var(--color-body) !important; line-height: 1.1 !important;
line-height: 1.4 !important;
border: none !important; border: none !important;
} }
.homelab-dashboard .subtitle { .homelab-dashboard .masthead-eyebrow {
font-family: 'IBM Plex Mono', monospace; font-family: "JetBrains Mono", "Fira Code", monospace;
color: var(--color-primary); font-size: 0.7rem;
font-size: 0.55rem; text-transform: uppercase;
margin-top: 15px; letter-spacing: 0.12em;
letter-spacing: 2px; color: var(--color-text-light);
align-self: flex-end;
padding-bottom: 0.35rem;
} }
.homelab-dashboard .score-bar { .homelab-dashboard .tagline {
display: flex; font-family: "Source Serif 4", Georgia, serif;
justify-content: center; font-size: 1.125rem;
gap: 40px; color: var(--color-text-light);
margin-top: 20px; max-width: 65ch;
font-family: 'IBM Plex Mono', monospace; line-height: 1.65;
font-size: 0.5rem; margin-bottom: 1.5rem;
} }
.homelab-dashboard .score-item {
color: var(--color-text); /* Section header */
background: var(--color-primary); .homelab-dashboard .section-header {
padding: 6px 14px; margin-bottom: 1.5rem;
border: 2px solid var(--color-primary-light-2);
} }
.homelab-dashboard .section-eyebrow {
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--color-text-light);
margin-bottom: 0.4rem;
display: block;
}
.homelab-dashboard .section-header h2 {
font-family: "Playfair Display", Georgia, serif !important;
font-size: 1.75rem !important;
font-weight: 700 !important;
letter-spacing: -0.01em !important;
color: var(--color-text) !important;
line-height: 1.2 !important;
border: none !important;
}
/* Grid */
.homelab-dashboard .grid { .homelab-dashboard .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px; gap: 1.25rem;
padding: 20px 0; margin-bottom: 3rem;
margin-top: 20px;
} }
/* Tiles */
.homelab-dashboard .tile { .homelab-dashboard .tile {
background: var(--color-box-body); background: var(--color-box-body);
border: 3px solid var(--color-secondary-dark-2) !important; border: 1px solid var(--color-border) !important;
padding: 25px; border-top: 3px solid var(--color-text) !important;
padding: 1.5rem;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.2s ease; transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer; cursor: pointer;
text-decoration: none !important; text-decoration: none !important;
display: block; display: block;
@@ -64,97 +110,143 @@
background: var(--color-body); background: var(--color-body);
} }
.homelab-dashboard .tile .icon { .homelab-dashboard .tile .icon {
font-size: 2.5rem; font-size: 1.5rem;
margin-bottom: 15px; margin-bottom: 0.75rem;
display: block; display: block;
line-height: 1;
} }
.homelab-dashboard .tile.storage .icon, .homelab-dashboard .tile.storage .icon,
.homelab-dashboard .tile.database .icon { color: var(--color-red); } .homelab-dashboard .tile.database .icon { color: var(--color-warning); }
.homelab-dashboard .tile.git .icon { color: var(--color-primary-light-2); } .homelab-dashboard .tile.git .icon { color: var(--color-primary); }
.homelab-dashboard .tile.ci .icon { color: var(--color-primary-light-1); } .homelab-dashboard .tile.ci .icon { color: var(--color-primary); }
.homelab-dashboard .tile.notebook .icon, .homelab-dashboard .tile.notebook .icon,
.homelab-dashboard .tile.research .icon { color: var(--color-text); } .homelab-dashboard .tile.research .icon { color: var(--color-text); }
.homelab-dashboard .tile.catalog .icon { color: var(--color-primary-light-2); } .homelab-dashboard .tile.catalog .icon { color: var(--color-primary); }
.homelab-dashboard .tile.query .icon { color: var(--color-primary-light-1); } .homelab-dashboard .tile.query .icon { color: var(--color-primary); }
.homelab-dashboard .tile.observability .icon { color: var(--color-red); } .homelab-dashboard .tile.observability .icon { color: var(--color-warning); }
.homelab-dashboard .tile.metrics .icon { color: var(--color-primary-light-2); } .homelab-dashboard .tile.metrics .icon { color: var(--color-primary); }
.homelab-dashboard .tile.logs .icon { color: var(--color-primary-light-1); } .homelab-dashboard .tile.logs .icon { color: var(--color-primary); }
.homelab-dashboard .tile.proxy .icon { color: var(--color-text); } .homelab-dashboard .tile.proxy .icon { color: var(--color-text); }
.homelab-dashboard .tile-title {
font-family: 'IBM Plex Mono', monospace !important; /* Tile eyebrow — mono metadata label */
font-size: 0.7rem !important; .homelab-dashboard .tile-eyebrow {
color: var(--color-text) !important; font-family: "JetBrains Mono", "Fira Code", monospace;
margin-bottom: 10px;
letter-spacing: 1px;
}
.homelab-dashboard .tile-desc {
font-size: 0.75rem;
color: var(--color-text-light-2);
line-height: 1.6;
margin-bottom: 15px;
}
.homelab-dashboard .tile-port {
font-size: 0.7rem; font-size: 0.7rem;
color: var(--color-primary-light-2); text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--color-text-light);
margin-bottom: 0.25rem;
}
/* Tile title — Playfair Display */
.homelab-dashboard .tile-title {
font-family: "Playfair Display", Georgia, serif !important;
font-size: 1.25rem !important;
font-weight: 700 !important;
line-height: 1.2 !important;
letter-spacing: -0.01em !important;
color: var(--color-text) !important;
margin: 0.25rem 0 0.5rem !important;
border: none !important;
text-transform: none !important;
transition: color 0.15s ease;
}
.homelab-dashboard .tile:hover .tile-title {
color: var(--color-primary) !important;
}
/* Tile description — Source Serif 4 */
.homelab-dashboard .tile-desc {
font-family: "Source Serif 4", Georgia, serif;
font-size: 0.95rem;
color: var(--color-text-light);
line-height: 1.55;
margin-bottom: 1rem;
}
/* Tile port — mono metadata */
.homelab-dashboard .tile-port {
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 0.65rem;
color: var(--color-text-light);
opacity: 0.7;
display: inline-block; display: inline-block;
} }
/* Status dot */
.homelab-dashboard .status-dot { .homelab-dashboard .status-dot {
position: absolute; position: absolute;
top: 15px; top: 1rem;
right: 15px; right: 1rem;
width: 10px; width: 8px;
height: 10px; height: 8px;
background: var(--color-green); background: var(--color-success);
border-radius: 50%; border-radius: 50%;
} }
/* Badge */
.homelab-dashboard .tile-badge {
position: absolute;
top: -1px;
left: 1.25rem;
font-family: "JetBrains Mono", "Fira Code", monospace;
color: var(--color-primary-foreground, #F7F5F0);
font-size: 0.6rem;
padding: 3px 7px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.homelab-dashboard .tile-badge.new { background: var(--color-warning); }
.homelab-dashboard .tile-badge.gpu { background: var(--color-primary); }
.homelab-dashboard .tile-badge.data { background: var(--color-primary); }
/* Bottom accent */
.homelab-dashboard .stripe { .homelab-dashboard .stripe {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 3px; height: 2px;
background: var(--color-red); background: var(--color-border);
} }
.homelab-dashboard .hdr-badge {
position: absolute; /* Footer */
top: -1px;
left: 20px;
font-family: 'IBM Plex Mono', monospace;
color: var(--color-text);
font-size: 0.35rem;
padding: 4px 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.homelab-dashboard .hdr-badge.new { background: var(--color-red); }
.homelab-dashboard .hdr-badge.gpu { background: var(--color-primary); }
.homelab-dashboard .hdr-badge.data { background: var(--color-primary); }
.homelab-dashboard .ftr { .homelab-dashboard .ftr {
text-align: center; padding-top: 1rem;
padding: 40px; border-top: 3px solid var(--color-text);
color: var(--color-text-light-2); color: var(--color-text-light);
font-size: 0.7rem; font-family: "JetBrains Mono", "Fira Code", monospace;
border-top: 3px solid var(--color-secondary-dark-2); font-size: 0.65rem;
margin-top: 20px; letter-spacing: 0.08em;
} }
.homelab-dashboard .ftr span { color: var(--color-red); } .homelab-dashboard .ftr span { color: var(--color-warning); }
</style> </style>
<div class="homelab-dashboard"> <div class="homelab-dashboard">
<div class="hdr"> <div class="masthead">
<h1>HOMELAB</h1> <div class="rule-line"></div>
<p class="subtitle">COMMAND CENTER</p> <div class="masthead-row">
<div class="score-bar"> <h1>Homelab</h1>
<span class="score-item">SERVICES: 15</span> <span class="masthead-eyebrow">git.fhirworx.io</span>
<span class="score-item">STATUS: ONLINE</span>
</div> </div>
<p class="tagline">
Self-hosted infrastructure for FHIR data engineering — version control, CI/CD,
notebooks, and the full lakehouse stack.
</p>
<div class="rule-line-thin"></div>
</div>
<div class="section-header">
<span class="section-eyebrow">Services</span>
<h2>Infrastructure</h2>
</div> </div>
<div class="grid"> <div class="grid">
<a data-subdomain="traefik" target="_blank" class="tile proxy"> <a data-subdomain="traefik" target="_blank" class="tile proxy">
<span class="hdr-badge new">NEW</span> <span class="tile-badge new">New</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128279;</span> <span class="icon">&#128279;</span>
<h2 class="tile-title">TRAEFIK</h2> <div class="tile-eyebrow">Networking</div>
<div class="tile-title">Traefik</div>
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p> <p class="tile-desc">Cloud-native reverse proxy and load balancer</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -163,7 +255,8 @@
<a data-subdomain="s3console" target="_blank" class="tile storage"> <a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128190;</span> <span class="icon">&#128190;</span>
<h2 class="tile-title">RUSTFS</h2> <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> <p class="tile-desc">S3-compatible object storage for all your data needs</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -172,8 +265,9 @@
<a data-subdomain="git" class="tile git"> <a data-subdomain="git" class="tile git">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128025;</span> <span class="icon">&#128025;</span>
<h2 class="tile-title">GITEA</h2> <div class="tile-eyebrow">Version Control</div>
<p class="tile-desc">Self-hosted Git service with packages & CI</p> <div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -181,68 +275,75 @@
<a data-subdomain="ci" target="_blank" class="tile ci"> <a data-subdomain="ci" target="_blank" class="tile ci">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128296;</span> <span class="icon">&#128296;</span>
<h2 class="tile-title">WOODPECKER</h2> <div class="tile-eyebrow">CI/CD</div>
<div class="tile-title">Woodpecker</div>
<p class="tile-desc">Simple yet powerful CI/CD engine</p> <p class="tile-desc">Simple yet powerful CI/CD engine</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="notebooks" target="_blank" class="tile notebook"> <a data-subdomain="notebooks" target="_blank" class="tile notebook">
<span class="hdr-badge gpu">GPU</span> <span class="tile-badge gpu">GPU</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128209;</span> <span class="icon">&#128209;</span>
<h2 class="tile-title">NOTEBOOKS</h2> <div class="tile-eyebrow">Analysis</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA</p> <div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="zotero" target="_blank" class="tile research"> <a data-subdomain="zotero" target="_blank" class="tile research">
<span class="hdr-badge gpu">GPU</span> <span class="tile-badge gpu">GPU</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128218;</span> <span class="icon">&#128218;</span>
<h2 class="tile-title">ZOTERO</h2> <div class="tile-eyebrow">Research</div>
<div class="tile-title">Zotero</div>
<p class="tile-desc">Research reference manager in the cloud</p> <p class="tile-desc">Research reference manager in the cloud</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="docs" target="_blank" class="tile research"> <a data-subdomain="docs" target="_blank" class="tile research">
<span class="hdr-badge new">NEW</span> <span class="tile-badge new">New</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128214;</span> <span class="icon">&#128214;</span>
<h2 class="tile-title">DOCS</h2> <div class="tile-eyebrow">Documentation</div>
<div class="tile-title">Docs</div>
<p class="tile-desc">API reference and CMS bibliography browser</p> <p class="tile-desc">API reference and CMS bibliography browser</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog"> <a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog">
<span class="hdr-badge data">DATA</span> <span class="tile-badge data">Data</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#127795;</span> <span class="icon">&#127795;</span>
<h2 class="tile-title">NESSIE</h2> <div class="tile-eyebrow">Catalog</div>
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p> <div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="trino" target="_blank" class="tile query"> <a data-subdomain="trino" target="_blank" class="tile query">
<span class="hdr-badge data">DATA</span> <span class="tile-badge data">Data</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128269;</span> <span class="icon">&#128269;</span>
<h2 class="tile-title">TRINO</h2> <div class="tile-eyebrow">Query</div>
<div class="tile-title">Trino</div>
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p> <p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="polaris" target="_blank" class="tile catalog"> <a data-subdomain="polaris" target="_blank" class="tile catalog">
<span class="hdr-badge new">NEW</span> <span class="tile-badge new">New</span>
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#11088;</span> <span class="icon">&#11088;</span>
<h2 class="tile-title">POLARIS</h2> <div class="tile-eyebrow">Catalog</div>
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p> <div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -250,8 +351,9 @@
<a data-subdomain="grafana" target="_blank" class="tile observability"> <a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128202;</span> <span class="icon">&#128202;</span>
<h2 class="tile-title">GRAFANA</h2> <div class="tile-eyebrow">Observability</div>
<p class="tile-desc">Observability dashboards for logs and traces</p> <div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -259,7 +361,8 @@
<a data-subdomain="jaeger" target="_blank" class="tile metrics"> <a data-subdomain="jaeger" target="_blank" class="tile metrics">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128065;</span> <span class="icon">&#128065;</span>
<h2 class="tile-title">JAEGER</h2> <div class="tile-eyebrow">Tracing</div>
<div class="tile-title">Jaeger</div>
<p class="tile-desc">Distributed tracing for microservices</p> <p class="tile-desc">Distributed tracing for microservices</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -268,7 +371,8 @@
<a data-subdomain="prometheus" target="_blank" class="tile logs"> <a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128200;</span> <span class="icon">&#128200;</span>
<h2 class="tile-title">PROMETHEUS</h2> <div class="tile-eyebrow">Metrics</div>
<div class="tile-title">Prometheus</div>
<p class="tile-desc">Metrics collection and alerting</p> <p class="tile-desc">Metrics collection and alerting</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -277,7 +381,8 @@
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database"> <a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
<span class="status-dot"></span> <span class="status-dot"></span>
<span class="icon">&#128203;</span> <span class="icon">&#128203;</span>
<h2 class="tile-title">LOKI</h2> <div class="tile-eyebrow">Logs</div>
<div class="tile-title">Loki</div>
<p class="tile-desc">Log aggregation system by Grafana</p> <p class="tile-desc">Log aggregation system by Grafana</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -285,14 +390,11 @@
</div> </div>
<div class="ftr"> <div class="ftr">
MADE WITH <span>&#9829;</span> FOR THE HOMELAB <span>fhirworx.io</span> &mdash; self-hosted data lakehouse &mdash; &copy; 2025
</div> </div>
</div> </div>
<script> <script>
(function() { (function() {
// Strip the leading subdomain (e.g. "git.") to get the base domain.
// Works for any depth: "git.fhirworx.io" -> "fhirworx.io",
// "git.homelab.fhirworx.io" -> "homelab.fhirworx.io".
var parts = location.hostname.split('.'); var parts = location.hostname.split('.');
var base = parts.length > 2 ? parts.slice(1).join('.') : location.hostname; var base = parts.length > 2 ? parts.slice(1).join('.') : location.hostname;
document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) { document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) {

View File

@@ -3,27 +3,40 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FHIRWORX COMMAND CENTER</title> <title>Fhirworx — Data Infrastructure</title>
<link rel="icon" type="image/png" sizes="32x32" href="/fav32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/fav32.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="/dashboard.css"> <link rel="stylesheet" href="/dashboard.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header>
<h1>FHIRWORX</h1> <div class="masthead">
<p class="subtitle">COMMAND CENTER</p> <div class="rule-line"></div>
<div class="score-bar"> <div class="masthead-row">
<span class="score-item">SERVICES: 15</span> <h1>Fhirworx</h1>
<span class="score-item">STATUS: ONLINE</span> <span class="masthead-eyebrow">dashboard.fhirworx.io</span>
</div>
<p class="tagline">
A self-hosted data lakehouse for FHIR analytics — version-controlled storage,
distributed SQL, and end-to-end observability in a single stack.
</p>
<div class="rule-line-thin"></div>
</div>
<div class="section-header">
<span class="section-eyebrow">Services</span>
<h2>Infrastructure</h2>
</div> </div>
</header>
<div class="grid"> <div class="grid">
<a data-subdomain="traefik" target="_blank" class="tile proxy"> <a data-subdomain="traefik" target="_blank" class="tile proxy">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128279;</span> <span class="icon">&#128279;</span>
<h2 class="tile-title">TRAEFIK</h2> <div class="tile-eyebrow">Networking</div>
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p> <div class="tile-title">Traefik</div>
<p class="tile-desc">Cloud-native reverse proxy and load balancer with automatic TLS</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -31,7 +44,8 @@
<a data-subdomain="s3console" target="_blank" class="tile storage"> <a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128190;</span> <span class="icon">&#128190;</span>
<h2 class="tile-title">RUSTFS</h2> <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> <p class="tile-desc">S3-compatible object storage for all your data needs</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -40,8 +54,9 @@
<a data-subdomain="git" target="_blank" class="tile git"> <a data-subdomain="git" target="_blank" class="tile git">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128025;</span> <span class="icon">&#128025;</span>
<h2 class="tile-title">GITEA</h2> <div class="tile-eyebrow">Version Control</div>
<p class="tile-desc">Self-hosted Git service with packages & CI</p> <div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -49,7 +64,8 @@
<a data-subdomain="ci" target="_blank" class="tile ci"> <a data-subdomain="ci" target="_blank" class="tile ci">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128296;</span> <span class="icon">&#128296;</span>
<h2 class="tile-title">WOODPECKER</h2> <div class="tile-eyebrow">CI/CD</div>
<div class="tile-title">Woodpecker</div>
<p class="tile-desc">Simple yet powerful CI/CD engine</p> <p class="tile-desc">Simple yet powerful CI/CD engine</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -59,8 +75,9 @@
<span class="badge gpu">GPU</span> <span class="badge gpu">GPU</span>
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128209;</span> <span class="icon">&#128209;</span>
<h2 class="tile-title">NOTEBOOKS</h2> <div class="tile-eyebrow">Analysis</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA</p> <div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -69,7 +86,8 @@
<span class="badge gpu">GPU</span> <span class="badge gpu">GPU</span>
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128218;</span> <span class="icon">&#128218;</span>
<h2 class="tile-title">ZOTERO</h2> <div class="tile-eyebrow">Research</div>
<div class="tile-title">Zotero</div>
<p class="tile-desc">Research reference manager in the cloud</p> <p class="tile-desc">Research reference manager in the cloud</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -78,27 +96,30 @@
<a data-subdomain="docs" target="_blank" class="tile research"> <a data-subdomain="docs" target="_blank" class="tile research">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128214;</span> <span class="icon">&#128214;</span>
<h2 class="tile-title">DOCS</h2> <div class="tile-eyebrow">Documentation</div>
<div class="tile-title">Docs</div>
<p class="tile-desc">API reference and CMS bibliography browser</p> <p class="tile-desc">API reference and CMS bibliography browser</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog"> <a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog">
<span class="badge data">DATA</span> <span class="badge data">Data</span>
<span class="status"></span> <span class="status"></span>
<span class="icon">&#127795;</span> <span class="icon">&#127795;</span>
<h2 class="tile-title">NESSIE</h2> <div class="tile-eyebrow">Catalog</div>
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p> <div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
<a data-subdomain="trino" target="_blank" class="tile query"> <a data-subdomain="trino" target="_blank" class="tile query">
<span class="badge data">DATA</span> <span class="badge data">Data</span>
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128269;</span> <span class="icon">&#128269;</span>
<h2 class="tile-title">TRINO</h2> <div class="tile-eyebrow">Query</div>
<div class="tile-title">Trino</div>
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p> <p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -107,8 +128,9 @@
<a data-subdomain="polaris" target="_blank" class="tile catalog"> <a data-subdomain="polaris" target="_blank" class="tile catalog">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#11088;</span> <span class="icon">&#11088;</span>
<h2 class="tile-title">POLARIS</h2> <div class="tile-eyebrow">Catalog</div>
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p> <div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access control</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -116,8 +138,9 @@
<a data-subdomain="grafana" target="_blank" class="tile observability"> <a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128202;</span> <span class="icon">&#128202;</span>
<h2 class="tile-title">GRAFANA</h2> <div class="tile-eyebrow">Observability</div>
<p class="tile-desc">Observability dashboards for logs and traces</p> <div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
</a> </a>
@@ -125,7 +148,8 @@
<a data-subdomain="jaeger" target="_blank" class="tile metrics"> <a data-subdomain="jaeger" target="_blank" class="tile metrics">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128065;</span> <span class="icon">&#128065;</span>
<h2 class="tile-title">JAEGER</h2> <div class="tile-eyebrow">Tracing</div>
<div class="tile-title">Jaeger</div>
<p class="tile-desc">Distributed tracing for microservices</p> <p class="tile-desc">Distributed tracing for microservices</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -134,7 +158,8 @@
<a data-subdomain="prometheus" target="_blank" class="tile logs"> <a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128200;</span> <span class="icon">&#128200;</span>
<h2 class="tile-title">PROMETHEUS</h2> <div class="tile-eyebrow">Metrics</div>
<div class="tile-title">Prometheus</div>
<p class="tile-desc">Metrics collection and alerting</p> <p class="tile-desc">Metrics collection and alerting</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -143,7 +168,8 @@
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database"> <a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128203;</span> <span class="icon">&#128203;</span>
<h2 class="tile-title">LOKI</h2> <div class="tile-eyebrow">Logs</div>
<div class="tile-title">Loki</div>
<p class="tile-desc">Log aggregation system by Grafana</p> <p class="tile-desc">Log aggregation system by Grafana</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -152,7 +178,8 @@
<a data-subdomain="api" data-path="/health" target="_blank" class="tile proxy"> <a data-subdomain="api" data-path="/health" target="_blank" class="tile proxy">
<span class="status"></span> <span class="status"></span>
<span class="icon">&#128268;</span> <span class="icon">&#128268;</span>
<h2 class="tile-title">API</h2> <div class="tile-eyebrow">API</div>
<div class="tile-title">Platform API</div>
<p class="tile-desc">FastAPI platform backend</p> <p class="tile-desc">FastAPI platform backend</p>
<span class="tile-port"></span> <span class="tile-port"></span>
<div class="stripe"></div> <div class="stripe"></div>
@@ -160,12 +187,11 @@
</div> </div>
<footer> <footer>
MADE WITH <span>&#9829;</span> FOR FHIRWORX <span>fhirworx.io</span> &mdash; self-hosted data lakehouse &mdash; &copy; 2025
</footer> </footer>
</div> </div>
<script> <script>
// All tiles use https://{subdomain}.fhirworx.io
(function() { (function() {
var base = location.hostname.replace(/^dashboard\./, ''); var base = location.hostname.replace(/^dashboard\./, '');
document.querySelectorAll('[data-subdomain]').forEach(function(el) { document.querySelectorAll('[data-subdomain]').forEach(function(el) {

View File

@@ -44,13 +44,13 @@ http:
referrerPolicy: "strict-origin-when-cross-origin" referrerPolicy: "strict-origin-when-cross-origin"
customResponseHeaders: customResponseHeaders:
X-Robots-Tag: "noindex, nofollow" X-Robots-Tag: "noindex, nofollow"
inject-loch: inject-fhirworx:
plugin: plugin:
rewrite-body: rewrite-body:
lastModified: true lastModified: true
rewrites: rewrites:
- regex: "</head>" - regex: "</head>"
replacement: '<link rel="icon" type="image/png" sizes="32x32" href="//dashboard.{{ $domain }}/fav32.png"><link rel="stylesheet" type="text/css" href="//dashboard.{{ $domain }}/loch.css"></head>' replacement: '<link rel="icon" type="image/png" sizes="32x32" href="//dashboard.{{ $domain }}/fav32.png"><link rel="stylesheet" type="text/css" href="//dashboard.{{ $domain }}/fhirworx.css"></head>'
routers: routers:
# auth.DOMAIN — oauth2-proxy public endpoints (login, callback, sign_out) # auth.DOMAIN — oauth2-proxy public endpoints (login, callback, sign_out)
auth-host: auth-host:
@@ -71,7 +71,7 @@ http:
entryPoints: entryPoints:
- web - web
{{- $mwList := list -}} {{- $mwList := list -}}
{{- if get $svc "theme" }}{{ $mwList = append $mwList "inject-loch" }}{{ end -}} {{- if get $svc "theme" }}{{ $mwList = append $mwList "inject-fhirworx" }}{{ end -}}
{{- $extra := get $svc "mw" | default "" -}} {{- $extra := get $svc "mw" | default "" -}}
{{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}} {{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}}
{{- if $mwList }} {{- if $mwList }}
@@ -100,7 +100,7 @@ http:
entryPoints: entryPoints:
- web - web
{{- $mwList := list -}} {{- $mwList := list -}}
{{- if get $svc "theme" }}{{ $mwList = append $mwList "inject-loch" }}{{ end -}} {{- if get $svc "theme" }}{{ $mwList = append $mwList "inject-fhirworx" }}{{ end -}}
{{- $extra := get $svc "mw" | default "" -}} {{- $extra := get $svc "mw" | default "" -}}
{{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}} {{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}}
{{- if $mwList }} {{- if $mwList }}
@@ -128,7 +128,7 @@ http:
entryPoints: entryPoints:
- web - web
middlewares: middlewares:
- inject-loch - inject-fhirworx
- gitea-sso - gitea-sso
- infra-headers - infra-headers
traefik-dashboard-tls: traefik-dashboard-tls:
@@ -138,7 +138,7 @@ http:
- websecure - websecure
tls: {} tls: {}
middlewares: middlewares:
- inject-loch - inject-fhirworx
- gitea-sso - gitea-sso
- infra-headers - infra-headers
services: services:

View File

@@ -35,7 +35,7 @@ def _():
connect.theme() connect.theme()
_s3 = connect.obstore() # noqa: F841 — marimo auto-discovers in Files _s3 = connect.obstore() # noqa: F841 — marimo auto-discovers in Files
from nature import PALETTE # noqa: E402 — needs theme() to add styles to path from fhirworx import PALETTE # noqa: E402 — needs theme() to add styles to path
con = connect.duckdb() con = connect.duckdb()

View File

@@ -35,9 +35,9 @@ _assets_dir = str(_Path(__file__).resolve().parents[2] / "assets")
if _assets_dir not in _sys.path: if _assets_dir not in _sys.path:
_sys.path.insert(0, _assets_dir) _sys.path.insert(0, _assets_dir)
from nature import EXTERNAL_COLOR as _EXTERNAL_COLOR # noqa: E402, I001 from fhirworx import EXTERNAL_COLOR as _EXTERNAL_COLOR # noqa: E402, I001
from nature import SCHEMA_COLORS as _SCHEMA_COLORS # noqa: E402, F401 from fhirworx import SCHEMA_COLORS as _SCHEMA_COLORS # noqa: E402, F401
from nature import schema_color as _color # noqa: E402 from fhirworx import schema_color as _color # noqa: E402
from aco.pipe.base import Pipeline # noqa: E402 from aco.pipe.base import Pipeline # noqa: E402

View File

@@ -152,12 +152,12 @@ def obstore(*, bucket: str = "lakehouse") -> Any:
def theme() -> None: def theme() -> None:
"""Activate the Nature/loch Altair chart theme.""" """Activate the fhirworx Altair chart theme."""
import sys import sys
assets_dir = str(ROOT / "assets") assets_dir = str(ROOT / "assets")
if assets_dir not in sys.path: if assets_dir not in sys.path:
sys.path.insert(0, assets_dir) sys.path.insert(0, assets_dir)
from nature import altair_theme from fhirworx import altair_theme
altair_theme() altair_theme()

View File

@@ -188,4 +188,4 @@ class TestTheme:
connect.theme() connect.theme()
import altair as alt import altair as alt
assert alt.themes.active == "nature" assert alt.theme.active == "fhirworx"

View File

@@ -99,9 +99,9 @@ class TestInfraMarimo:
def test_home_page_js_exists(self): def test_home_page_js_exists(self):
assert (INFRA_MARIMO / "home-page-patched.js").is_file() assert (INFRA_MARIMO / "home-page-patched.js").is_file()
def test_no_loch_css_duplicate(self): def test_no_fhirworx_css_duplicate(self):
"""loch.css should NOT be in infra/marimo/ — served from assets/css/.""" """fhirworx.css should NOT be in infra/marimo/ — served from assets/css/."""
assert not (INFRA_MARIMO / "loch.css").exists() assert not (INFRA_MARIMO / "fhirworx.css").exists()
class TestNoStaleReferences: class TestNoStaleReferences: