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
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
@@ -491,7 +491,7 @@ stack/
│ └── zot/
├── infra/ Service configs and Dockerfiles
│ ├── 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
│ ├── prometheus/ Metrics collection
│ ├── loki/ Log aggregation
@@ -499,7 +499,7 @@ stack/
├── assets/ Branding, styles, generated artifacts
│ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker)
│ ├── icons/ Favicons, logos, coverage badge
│ └── nature.py Nature-conformant chart palette
│ └── fhirworx.py fhirworx chart palette and Altair theme
├── cloud/ Cloud provider deployment guides
│ ├── self-hosted/ Docker Compose (default)
│ ├── 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
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
@@ -472,7 +472,7 @@ stack/
{% endfor %}
├── infra/ Service configs and Dockerfiles
│ ├── 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
│ ├── prometheus/ Metrics collection
│ ├── loki/ Log aggregation
@@ -480,7 +480,7 @@ stack/
├── assets/ Branding, styles, generated artifacts
│ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker)
│ ├── icons/ Favicons, logos, coverage badge
│ └── nature.py Nature-conformant chart palette
│ └── fhirworx.py fhirworx chart palette and Altair theme
├── cloud/ Cloud provider deployment guides
│ ├── self-hosted/ Docker Compose (default)
│ ├── 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 {
--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;
--background: #F7F5F0;
--foreground: #1A1A18;
--card: #FAFAF7;
--card-foreground: #1A1A18;
--primary: #1C2B3A;
--primary-foreground: #F7F5F0;
--secondary: #EDEBE6;
--muted-foreground: #6B6B68;
--border: #D4D0C8;
--sidebar: #1C2B3A;
--sidebar-foreground: #B8C5D0;
--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 {
font-family: 'IBM Plex Mono', 'Courier New', monospace;
background: var(--ds-navy);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.65;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
overflow-x: hidden;
color: var(--ds-white);
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1400px;
max-width: 1180px;
margin: 0 auto;
padding: 20px;
padding: 3rem 1.5rem;
}
header {
text-align: center;
padding: 40px 20px;
border-bottom: 4px solid var(--ds-red);
/* ── Masthead ──────────────────────────────────────────────── */
.masthead {
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 {
font-family: 'IBM Plex Mono', monospace;
font-weight: 600;
font-size: 2rem;
color: var(--ds-white);
letter-spacing: 6px;
text-shadow: 3px 3px 0 var(--ds-navy-mid);
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(2.5rem, 5vw, 3.5rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--foreground);
}
.subtitle {
font-family: 'IBM Plex Mono', monospace;
color: var(--ds-blue-bright);
font-size: 0.55rem;
margin-top: 15px;
letter-spacing: 2px;
.masthead-eyebrow {
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted-foreground);
align-self: flex-end;
padding-bottom: 0.35rem;
}
.score-bar {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 20px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.5rem;
.tagline {
font-family: var(--font-body);
font-size: 1.125rem;
color: var(--muted-foreground);
max-width: 65ch;
line-height: 1.65;
margin-bottom: 1.5rem;
}
.score-item {
color: var(--ds-white);
background: var(--ds-blue);
padding: 6px 14px;
border: 2px solid var(--ds-blue-light);
/* ── Services section ─────────────────────────────────────── */
.section-header {
margin-bottom: 1.5rem;
}
.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 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
padding: 20px 0;
margin-top: 20px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
padding: 0;
margin-top: 0;
}
.tile {
background: var(--ds-navy-light);
border: 3px solid var(--ds-slate);
padding: 25px;
background: var(--card);
border: 1px solid var(--border);
border-top: 3px solid var(--foreground);
padding: 1.5rem;
position: relative;
overflow: hidden;
transition: all 0.2s ease;
transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer;
text-decoration: none;
display: block;
}
.tile:hover {
border-color: var(--ds-blue);
background: var(--ds-navy-mid);
border-color: var(--primary);
background: var(--card);
}
/* All tile variants use the same base style */
.tile .icon {
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;
.tile:hover .tile-title {
color: var(--primary);
}
/* Status dot */
.status {
position: absolute;
top: 15px;
right: 15px;
width: 10px;
height: 10px;
background: var(--ds-green);
top: 1rem;
right: 1rem;
width: 8px;
height: 8px;
background: var(--chart-4);
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 {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--ds-red);
height: 2px;
background: var(--border);
}
footer {
text-align: center;
padding: 40px;
color: var(--ds-slate);
font-size: 0.7rem;
border-top: 3px solid var(--ds-slate);
margin-top: 20px;
}
/* ── Badge labels ─────────────────────────────────────────── */
footer span {
color: var(--ds-red);
}
/* Badges */
.badge {
position: absolute;
top: -1px;
left: 20px;
font-family: 'IBM Plex Mono', monospace;
color: var(--ds-white);
font-size: 0.35rem;
padding: 4px 8px;
left: 1.25rem;
font-family: var(--font-mono);
color: var(--primary-foreground);
font-size: 0.6rem;
padding: 3px 7px;
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: 0.08em;
}
.badge.new { background: var(--ds-red); }
.badge.gpu { background: var(--ds-blue); }
.badge.data { background: var(--ds-blue); }
.badge.new { background: var(--chart-5); }
.badge.gpu { background: var(--primary); }
.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 */
gitea-theme-meta-info {
--theme-display-name: "Loch";
--theme-display-name: "Fhirworx";
}
:root {
--is-dark-theme: true;
--is-dark-theme: false;
/* Loch Color Palette */
--ds-navy: #0a1628;
--ds-navy-light: #162040;
--ds-navy-mid: #1e2d50;
--ds-slate: #2a3f5f;
--ds-steel: #7a90b0;
--ds-white: #f0f0f0;
/* HTI-5 Color Palette */
--background: #F7F5F0;
--foreground: #1A1A18;
--card: #FAFAF7;
--primary: #1C2B3A;
--primary-foreground: #F7F5F0;
--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;
--ds-blue-light: #5588dd;
--ds-blue-bright: #88aaee;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--ds-red: #ee5577;
--ds-red-light: #dd4466;
/* Gitea color mappings — derived from hti5 tokens */
--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;
--ds-green-light: #66cc66;
--ds-yellow: #ddaa33;
/* 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);
--color-secondary: var(--muted-foreground);
--color-secondary-light: #9B9B98;
--color-secondary-dark: var(--foreground);
/* Background colors */
--color-body: var(--ds-navy);
--color-box-body: var(--ds-navy);
--color-box-header: var(--ds-navy-light);
--color-body: var(--background);
--color-box-body: var(--card);
--color-box-header: var(--secondary);
/* Text colors */
--color-text: var(--ds-white);
--color-text-light: var(--ds-steel);
--color-text-dark: var(--ds-white);
--color-text: var(--foreground);
--color-text-light: var(--muted-foreground);
--color-text-dark: var(--foreground);
/* Input colors */
--color-input-text: var(--ds-white);
--color-input-background: var(--ds-navy-mid);
--color-input-border: var(--ds-slate);
--color-input-text: var(--foreground);
--color-input-background: var(--card);
--color-input-border: var(--border);
/* Status colors */
--color-success: var(--ds-green);
--color-success-text: var(--ds-white);
--color-warning: var(--ds-yellow);
--color-warning-text: var(--ds-navy);
--color-error: var(--ds-red);
--color-error-text: var(--ds-white);
--color-info: var(--ds-blue-light);
--color-info-text: var(--ds-white);
--color-success: var(--chart-4);
--color-success-text: #fff;
--color-warning: var(--chart-5);
--color-warning-text: #fff;
--color-error: var(--destructive);
--color-error-text: #fff;
--color-info: var(--primary);
--color-info-text: var(--primary-foreground);
/* Border */
--color-border: var(--ds-slate);
--color-border: var(--border);
/* Link colors */
--color-link: var(--ds-blue-bright);
--color-link-hover: var(--ds-white);
--color-link: var(--primary);
--color-link-hover: var(--foreground);
/* Code */
--color-code-bg: var(--ds-navy-light);
--color-code-border: var(--ds-slate);
--color-code-bg: var(--secondary);
--color-code-border: var(--border);
/* Shadows */
--color-shadow: rgba(0, 0, 0, 0.4);
--color-shadow: rgba(0, 0, 0, 0.1);
}
/* Global styles */
body {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy) !important;
font-family: var(--font-body) !important;
background: var(--background) !important;
color: var(--foreground) !important;
}
/* Headings */
h1, h2, h3, h4, h5, h6,
.header-wrapper .header {
font-family: 'IBM Plex Mono', monospace !important;
font-family: var(--font-display) !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; }
h2 { font-size: 14px !important; color: var(--ds-blue-bright) !important; }
h3 { font-size: 12px !important; color: var(--ds-white) !important; }
h1 { font-size: 22px !important; color: var(--foreground) !important; }
h2 { font-size: 17px !important; color: var(--primary) !important; }
h3 { font-size: 14px !important; color: var(--foreground) !important; }
/* Navbar */
.ui.secondary.menu.navbar,
.navbar {
background: var(--ds-navy-light) !important;
border-bottom: 3px solid var(--ds-slate) !important;
background: var(--sidebar) !important;
border-bottom: 3px solid var(--foreground) !important;
}
.navbar .item {
font-family: 'IBM Plex Mono', monospace !important;
color: var(--ds-white) !important;
font-family: var(--font-body) !important;
color: var(--sidebar-foreground) !important;
}
.navbar .item:hover {
background: var(--ds-navy-mid) !important;
color: var(--ds-blue-bright) !important;
background: var(--sidebar-accent) !important;
color: var(--sidebar-accent-foreground, #EEE9E0) !important;
}
.navbar .item.active {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
background: var(--sidebar-accent) !important;
color: #fff !important;
}
/* Buttons */
.ui.button,
.button {
font-family: 'IBM Plex Mono', monospace !important;
font-family: var(--font-body) !important;
font-weight: 500 !important;
border-radius: 4px !important;
border-radius: var(--radius, 0.2rem) !important;
transition: all 0.15s ease !important;
}
.ui.primary.button,
.ui.blue.button {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-blue-light) !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
border: 1px solid var(--primary) !important;
}
.ui.primary.button:hover,
.ui.blue.button:hover {
background: var(--ds-blue-light) !important;
color: var(--ds-white) !important;
background: var(--sidebar-accent) !important;
color: #fff !important;
}
.ui.green.button {
background: var(--ds-green) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-green-light) !important;
background: var(--chart-4) !important;
color: #fff !important;
border: 1px solid var(--chart-4) !important;
}
.ui.green.button:hover {
background: var(--ds-green-light) !important;
color: var(--ds-navy) !important;
opacity: 0.85;
}
.ui.red.button {
background: var(--ds-red) !important;
color: var(--ds-white) !important;
background: var(--destructive) !important;
color: #fff !important;
}
/* Segments and boxes */
@@ -164,43 +165,43 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.ui.segments,
.repository,
.box {
background: var(--ds-navy) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
.ui.attached.header,
.box-header {
background: var(--ds-navy-light) !important;
border-bottom: 2px solid var(--ds-slate) !important;
font-family: 'IBM Plex Mono', monospace !important;
font-size: 11px !important;
color: var(--ds-white) !important;
background: var(--secondary) !important;
border-bottom: 1px solid var(--border) !important;
font-family: var(--font-display) !important;
font-size: 13px !important;
color: var(--foreground) !important;
}
/* Tables */
.ui.table {
background: var(--ds-navy) !important;
border: 2px solid var(--ds-slate) !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
}
.ui.table th {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
font-family: 'IBM Plex Mono', monospace !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
font-family: var(--font-mono) !important;
font-size: 10px !important;
text-transform: uppercase !important;
border: 1px solid var(--ds-blue-light) !important;
border: 1px solid var(--sidebar-accent) !important;
}
.ui.table td {
background: var(--ds-navy) !important;
color: var(--ds-white) !important;
border: 1px solid var(--ds-navy-light) !important;
background: var(--card) !important;
color: var(--foreground) !important;
border: 1px solid var(--border) !important;
}
.ui.table tr:hover td {
background: rgba(51, 102, 204, 0.2) !important;
background: var(--secondary) !important;
}
/* Forms */
@@ -208,38 +209,70 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.ui.form textarea,
.ui.form select,
.ui.input input {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy-mid) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
font-family: var(--font-body) !important;
background: var(--card) !important;
color: var(--foreground) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
.ui.form input:focus,
.ui.form textarea:focus,
.ui.input input:focus {
border-color: var(--ds-blue-light) !important;
background: var(--ds-navy-mid) !important;
border-color: var(--primary) !important;
background: var(--card) !important;
}
/* Labels */
/* Labels — mono for metadata labels (issue numbers, ref badges) */
.ui.label {
font-family: 'IBM Plex Mono', monospace !important;
border-radius: 4px !important;
font-family: var(--font-mono) !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 */
.ui.dropdown .menu {
background: var(--ds-navy-light) !important;
border: 2px solid var(--ds-slate) !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
}
.ui.dropdown .menu .item {
color: var(--ds-white) !important;
color: var(--foreground) !important;
}
.ui.dropdown .menu .item:hover {
background: var(--ds-blue) !important;
background: var(--secondary) !important;
}
/* Code and diffs */
@@ -247,101 +280,101 @@ h3 { font-size: 12px !important; color: var(--ds-white) !important; }
.file-view,
.CodeMirror,
.highlight {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy) !important;
font-family: var(--font-mono) !important;
background: var(--card) !important;
}
.diff-file-box .diff-file-header {
background: var(--ds-navy-light) !important;
background: var(--secondary) !important;
}
/* Commit graph */
.repository .commit-list .commit {
border-bottom: 1px solid var(--ds-navy-light) !important;
border-bottom: 1px solid var(--border) !important;
}
/* Issues and PRs */
.issue-list .item,
.pull-list .item {
border-bottom: 1px solid var(--ds-navy-light) !important;
border-bottom: 1px solid var(--border) !important;
}
/* Footer */
.footer {
background: var(--ds-navy-light) !important;
border-top: 3px solid var(--ds-slate) !important;
background: var(--secondary) !important;
border-top: 3px solid var(--foreground) !important;
}
/* Cards */
.ui.card,
.ui.cards .card {
background: var(--ds-navy) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Messages */
.ui.message {
font-family: 'IBM Plex Mono', monospace !important;
border-radius: 4px !important;
font-family: var(--font-body) !important;
border-radius: var(--radius, 0.2rem) !important;
}
.ui.positive.message,
.ui.success.message {
background: rgba(68, 170, 68, 0.2) !important;
border: 2px solid var(--ds-green) !important;
color: var(--ds-green-light) !important;
background: color-mix(in srgb, var(--chart-4) 12%, var(--background)) !important;
border: 1px solid var(--chart-4) !important;
color: var(--chart-4) !important;
}
.ui.negative.message,
.ui.error.message {
background: rgba(204, 34, 68, 0.2) !important;
border: 2px solid var(--ds-red) !important;
color: var(--ds-red-light) !important;
background: color-mix(in srgb, var(--destructive) 10%, var(--background)) !important;
border: 1px solid var(--destructive) !important;
color: var(--destructive) !important;
}
.ui.warning.message {
background: rgba(221, 170, 51, 0.2) !important;
border: 2px solid var(--ds-yellow) !important;
color: var(--ds-yellow) !important;
background: color-mix(in srgb, var(--chart-5) 10%, var(--background)) !important;
border: 1px solid var(--chart-5) !important;
color: var(--chart-5) !important;
}
.ui.info.message {
background: rgba(85, 136, 221, 0.2) !important;
border: 2px solid var(--ds-blue-light) !important;
color: var(--ds-blue-bright) !important;
background: color-mix(in srgb, var(--primary) 10%, var(--background)) !important;
border: 1px solid var(--primary) !important;
color: var(--primary) !important;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 12px;
height: 12px;
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--ds-navy-light);
background: var(--secondary);
}
::-webkit-scrollbar-thumb {
background: var(--ds-slate);
border-radius: 6px;
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--ds-steel);
background: var(--muted-foreground);
}
/* Selection */
::selection {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
}
/* Links */
a {
color: var(--ds-blue-bright) !important;
color: var(--primary) !important;
}
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 {
/* Loch Color Palette */
--ds-navy: #0a1628;
--ds-navy-light: #162040;
--ds-navy-mid: #1e2d50;
--ds-slate: #2a3f5f;
--ds-steel: #7a90b0;
--ds-white: #f0f0f0;
/* HTI-5 Color Palette */
--background: #F7F5F0;
--foreground: #1A1A18;
--card: #FAFAF7;
--primary: #1C2B3A;
--primary-foreground: #F7F5F0;
--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;
--ds-blue-light: #5588dd;
--ds-blue-bright: #88aaee;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--ds-red: #ee5577;
--ds-red-light: #dd4466;
/* Woodpecker color overrides — mapped from hti5 tokens */
--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;
--ds-green-light: #66cc66;
--ds-yellow: #ddaa33;
--color-wp-success-100: #56B397;
--color-wp-success-200: #2E8B6E;
/* Woodpecker color overrides */
--color-wp-primary-100: var(--ds-blue-bright);
--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-error-100: #D9534F;
--color-wp-error-200: #C0392B;
--color-wp-success-100: var(--ds-green-light);
--color-wp-success-200: var(--ds-green);
--color-wp-warning-100: #DDA060;
--color-wp-warning-200: #C8702A;
--color-wp-error-100: var(--ds-red-light);
--color-wp-error-200: var(--ds-red);
--color-wp-warning-100: var(--ds-yellow);
--color-wp-warning-200: #bb8822;
--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);
--color-wp-gray-100: #1A1A18;
--color-wp-gray-200: #6B6B68;
--color-wp-gray-300: #D4D0C8;
--color-wp-gray-400: #EDEBE6;
--color-wp-gray-500: #F7F5F0;
--color-wp-gray-600: #FAFAF7;
}
/* Global styles */
body,
html {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy) !important;
color: var(--ds-white) !important;
font-family: var(--font-body) !important;
background: var(--background) !important;
color: var(--foreground) !important;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
font-family: 'IBM Plex Mono', monospace !important;
font-family: var(--font-display) !important;
font-weight: 600 !important;
letter-spacing: 0.5px !important;
line-height: 1.8 !important;
line-height: 1.3 !important;
}
h1 { font-size: 18px !important; color: var(--ds-white) !important; }
h2 { font-size: 14px !important; color: var(--ds-blue-bright) !important; }
h3 { font-size: 12px !important; color: var(--ds-white) !important; }
h1 { font-size: 22px !important; color: var(--foreground) !important; }
h2 { font-size: 17px !important; color: var(--primary) !important; }
h3 { font-size: 14px !important; color: var(--foreground) !important; }
/* Header/Navbar */
header,
@@ -72,16 +73,16 @@ nav,
.navbar,
[class*="header"],
[class*="Header"] {
background: var(--ds-navy-light) !important;
border-bottom: 3px solid var(--ds-slate) !important;
background: var(--sidebar) !important;
border-bottom: 3px solid var(--foreground) !important;
}
/* Sidebar */
aside,
[class*="sidebar"],
[class*="Sidebar"] {
background: var(--ds-navy) !important;
border-right: 3px solid var(--ds-slate) !important;
background: var(--sidebar) !important;
border-right: 1px solid var(--sidebar-border, var(--sidebar-accent)) !important;
}
/* Main content area */
@@ -98,20 +99,20 @@ main,
[class*="Panel"],
[class*="box"],
[class*="Box"] {
background: var(--ds-navy) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Buttons */
/* Buttons — serif body for button labels */
button,
[class*="btn"],
[class*="Btn"],
[class*="button"],
[class*="Button"] {
font-family: 'IBM Plex Mono', monospace !important;
font-family: var(--font-body) !important;
font-weight: 500 !important;
border-radius: 4px !important;
border-radius: var(--radius, 0.2rem) !important;
transition: all 0.15s ease !important;
}
@@ -119,16 +120,16 @@ button,
[class*="primary"],
[class*="Primary"],
button[type="submit"] {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-blue-light) !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
border: 1px solid var(--primary) !important;
}
[class*="primary"]:hover,
[class*="Primary"]:hover,
button[type="submit"]:hover {
background: var(--ds-blue-light) !important;
color: var(--ds-white) !important;
background: var(--sidebar-accent) !important;
color: var(--sidebar-accent-foreground) !important;
}
/* Success/green buttons */
@@ -136,15 +137,14 @@ button[type="submit"]:hover {
[class*="Success"],
[class*="green"],
[class*="Green"] {
background: var(--ds-green) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-green-light) !important;
background: var(--chart-4) !important;
color: #fff !important;
border: 1px solid var(--chart-4) !important;
}
[class*="success"]:hover,
[class*="Success"]:hover {
background: var(--ds-green-light) !important;
color: var(--ds-navy) !important;
opacity: 0.85 !important;
}
/* Danger/red buttons */
@@ -154,60 +154,82 @@ button[type="submit"]:hover {
[class*="Error"],
[class*="red"],
[class*="Red"] {
background: var(--ds-red) !important;
color: var(--ds-white) !important;
background: var(--destructive) !important;
color: #fff !important;
}
/* Forms */
input,
textarea,
select {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy-mid) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
font-family: var(--font-body) !important;
background: var(--card) !important;
color: var(--foreground) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !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,
textarea:focus,
select:focus {
border-color: var(--ds-blue-light) !important;
border-color: var(--primary) !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 */
table {
background: var(--ds-navy) !important;
border: 2px solid var(--ds-slate) !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-collapse: collapse !important;
}
th {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
font-family: 'IBM Plex Mono', monospace !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
font-family: var(--font-mono) !important;
font-size: 10px !important;
text-transform: uppercase !important;
padding: 10px 12px !important;
border: 1px solid var(--ds-blue-light) !important;
border: 1px solid var(--sidebar-accent) !important;
}
td {
background: var(--ds-navy) !important;
color: var(--ds-white) !important;
background: var(--card) !important;
color: var(--foreground) !important;
padding: 8px 12px !important;
border: 1px solid var(--ds-navy-light) !important;
border: 1px solid var(--border) !important;
}
tr:nth-child(even) td {
background: var(--ds-navy-light) !important;
background: var(--secondary) !important;
}
tr:hover td {
background: rgba(51, 102, 204, 0.2) !important;
background: var(--secondary) !important;
}
/* Pipeline/build status colors */
@@ -215,14 +237,14 @@ tr:hover td {
[class*="Running"],
[class*="pending"],
[class*="Pending"] {
color: var(--ds-yellow) !important;
color: var(--chart-5) !important;
}
[class*="success"],
[class*="Success"],
[class*="passed"],
[class*="Passed"] {
color: var(--ds-green-light) !important;
color: var(--chart-4) !important;
}
[class*="failure"],
@@ -231,7 +253,7 @@ tr:hover td {
[class*="Failed"],
[class*="error"],
[class*="Error"] {
color: var(--ds-red) !important;
color: var(--destructive) !important;
}
/* Build logs */
@@ -241,29 +263,29 @@ code,
[class*="Log"],
[class*="console"],
[class*="Console"] {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
font-family: var(--font-mono) !important;
background: var(--secondary) !important;
color: var(--foreground) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Inline code */
code {
background: var(--ds-navy-light) !important;
color: var(--ds-blue-bright) !important;
background: var(--secondary) !important;
color: var(--primary) !important;
padding: 2px 6px !important;
border: none !important;
}
/* Links */
a {
color: var(--ds-blue-bright) !important;
color: var(--primary) !important;
text-decoration: none !important;
}
a:hover {
color: var(--ds-white) !important;
color: var(--foreground) !important;
text-decoration: underline !important;
}
@@ -274,16 +296,16 @@ a:hover {
[class*="Menu"],
[class*="popover"],
[class*="Popover"] {
background: var(--ds-navy-light) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
[class*="dropdown"] li:hover,
[class*="menu"] li:hover,
[role="menuitem"]:hover,
[role="option"]:hover {
background: var(--ds-blue) !important;
background: var(--secondary) !important;
}
/* Badges and tags */
@@ -293,62 +315,63 @@ a:hover {
[class*="Tag"],
[class*="label"],
[class*="Label"] {
font-family: 'IBM Plex Mono', monospace !important;
font-family: var(--font-mono) !important;
font-size: 11px !important;
border-radius: 4px !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Tooltips */
[role="tooltip"],
[class*="tooltip"],
[class*="Tooltip"] {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--ds-navy-light) !important;
color: var(--ds-white) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
font-family: var(--font-body) !important;
font-size: 13px !important;
background: var(--card) !important;
color: var(--foreground) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Progress bars */
progress,
[class*="progress"],
[class*="Progress"] {
background: var(--ds-navy-light) !important;
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
background: var(--secondary) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
progress::-webkit-progress-bar {
background: var(--ds-navy-light) !important;
background: var(--secondary) !important;
}
progress::-webkit-progress-value {
background: var(--ds-blue) !important;
background: var(--primary) !important;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 12px;
height: 12px;
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--ds-navy-light);
background: var(--secondary);
}
::-webkit-scrollbar-thumb {
background: var(--ds-slate);
border-radius: 6px;
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--ds-steel);
background: var(--muted-foreground);
}
/* Selection */
::selection {
background: var(--ds-blue) !important;
color: var(--ds-white) !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
}
/* Icons - ensure visibility */
@@ -361,8 +384,8 @@ svg,
/* Avatar images */
img[class*="avatar"],
[class*="avatar"] img {
border: 2px solid var(--ds-slate) !important;
border-radius: 4px !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius, 0.2rem) !important;
}
/* Repo/pipeline list items */
@@ -370,10 +393,10 @@ img[class*="avatar"],
[class*="Item"],
[class*="list-item"],
[class*="ListItem"] {
border-bottom: 1px solid var(--ds-navy-light) !important;
border-bottom: 1px solid var(--border) !important;
}
[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>
<meta charset="utf-8">
<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/dagre@0.8/dist/dagre.min.js"></script>
<script src="https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js"></script>
<style>
* { 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; }
#controls {
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;
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 {
width: 100%; padding: 6px 8px; border-radius: 4px;
border: 1px solid #444; background: #0f3460; color: #e0e0e0;
margin-bottom: 8px; font-size: 13px;
width: 100%; padding: 6px 8px; border-radius: 0;
border: 1px solid #253748; background: #253748; color: #EEE9E0;
margin-bottom: 8px; font-size: 13px; font-family: "JetBrains Mono", monospace;
}
#legend { display: flex; flex-wrap: wrap; gap: 4px; }
.legend-item {
@@ -33,16 +42,16 @@
display: inline-block; flex-shrink: 0;
}
#tooltip {
position: fixed; display: none; background: #16213e;
border: 1px solid #444; border-radius: 6px; padding: 10px;
position: fixed; display: none; background: var(--panel);
border: 1px solid var(--border); border-radius: 0; padding: 10px;
max-width: 340px; font-size: 12px; z-index: 20;
pointer-events: none; line-height: 1.4;
}
#tooltip .tt-name { font-weight: bold; color: #fff; margin-bottom: 4px; }
#tooltip .tt-desc { color: #bbb; }
#tooltip .tt-name { font-weight: bold; color: var(--text); margin-bottom: 4px; font-family: "Playfair Display", Georgia, serif; }
#tooltip .tt-desc { color: var(--muted); }
#stats {
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>
</head>
@@ -5911,12 +5920,13 @@
"background-color": "transparent",
"background-opacity": 0.05,
"border-width": 1,
"border-color": "#444",
"border-color": "#D4D0C8",
"label": "data(label)",
"text-valign": "top",
"text-halign": "center",
"font-size": "14px",
"color": "#666",
"font-size": "13px",
"font-family": "Playfair Display, Georgia, serif",
"color": "#6B6B68",
"padding": "16px",
"shape": "round-rectangle",
}
@@ -5948,15 +5958,16 @@
{
selector: "node.external",
style: {
"background-color": "#2a2a2a",
"background-color": "#EDEBE6",
"border-width": 1,
"border-style": "dashed",
"border-color": "#666",
"border-color": "#D4D0C8",
"label": "data(label)",
"text-valign": "center",
"text-halign": "center",
"font-size": "8px",
"color": "#888",
"font-family": "JetBrains Mono, Fira Code, monospace",
"color": "#6B6B68",
"width": "label",
"height": 24,
"padding": "6px",
@@ -5967,8 +5978,8 @@
selector: "edge",
style: {
"width": 1.5,
"line-color": "#444",
"target-arrow-color": "#444",
"line-color": "#D4D0C8",
"target-arrow-color": "#D4D0C8",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
"arrow-scale": 0.8,
@@ -5977,16 +5988,16 @@
{
selector: ".highlighted",
style: {
"border-color": "#fff",
"border-color": "#1C2B3A",
"border-width": 3,
"color": "#fff",
"color": "#1C2B3A",
}
},
{
selector: "edge.highlighted",
style: {
"line-color": "#e0e0e0",
"target-arrow-color": "#e0e0e0",
"line-color": "#1C2B3A",
"target-arrow-color": "#1C2B3A",
"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>
<meta charset="utf-8">
<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">
<script src="https://unpkg.com/katex@0.16/dist/katex.min.js"></script>
<style>
:root {
--bg: #1a1a2e; --panel: #16213e; --panel-hi: #0f3460;
--border: #333; --text: #e0e0e0; --muted: #888; --dim: #666;
--c-mul: #e15759; --c-dot: #59a14f; --c-add: #4e79a7;
--c-vec: #edc948; --c-scalar: #b07aa1; --c-ref: #76b7b2;
--c-fn: #f28e2b; --c-cond: #ff9da7;
/* HTI-5 palette */
--bg: #F7F5F0; --panel: #FAFAF7; --panel-hi: #EDEBE6;
--border: #D4D0C8; --text: #1A1A18; --muted: #6B6B68; --dim: #9B9B98;
/* Node-kind colours — Tableau-style but on light bg */
--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; }
body {
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;
}
@@ -26,7 +31,8 @@
display: flex; align-items: baseline; gap: 16px;
}
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;
}
header .subtitle {
@@ -46,7 +52,7 @@
}
.tab:hover { color: var(--text); background: var(--panel); }
.tab.active {
color: #fff; background: var(--panel);
color: var(--text); background: var(--panel);
border-color: var(--border);
}
@@ -114,7 +120,7 @@
color: var(--dim); font-size: 13px; padding: 40px 0; text-align: center;
}
.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;
}
.detail-latex {
@@ -122,7 +128,7 @@
margin-bottom: 14px; text-align: center; overflow-x: auto;
}
.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 div { margin-bottom: 5px; }
@@ -148,7 +154,7 @@
border-radius: 10px; padding: 20px; margin-top: 20px;
}
.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 {
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
@@ -169,7 +175,7 @@
outline: none; border-color: var(--c-add);
}
.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;
}
.eval-result small {

View File

@@ -165,13 +165,13 @@ services:
- GITEA__server__ROOT_URL=https://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__ui__THEMES=gitea-auto,gitea-light,gitea-dark,loch
- GITEA__ui__DEFAULT_THEME=loch
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,fhirworx
- GITEA__ui__DEFAULT_THEME=fhirworx
volumes:
- gitea_data:/var/lib/gitea
- gitea_config:/etc/gitea
- ./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:
- "2222:2222"
- "3000:3000"
@@ -459,7 +459,7 @@ services:
- ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./infra/nginx/index.html:/usr/share/nginx/html/index.html:ro
- ./assets/css/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
- ./assets/css/inject.css:/usr/share/nginx/html/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
security_opt:
- 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 {
--ifm-color-primary: #4488dd;

View File

@@ -1,59 +1,105 @@
{{template "base/head" .}}
<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 {
max-width: 1400px;
max-width: 1180px;
margin: 0 auto;
padding: 20px;
padding: 3rem 1.5rem;
font-family: "Source Serif 4", Georgia, serif;
}
.homelab-dashboard .hdr {
text-align: center;
padding: 40px 20px;
border-bottom: 4px solid var(--color-red);
/* Rule lines */
.homelab-dashboard .rule-line {
border: none;
border-top: 3px solid var(--color-text);
margin-bottom: 1rem;
}
.homelab-dashboard .hdr h1 {
font-family: 'IBM Plex Mono', monospace !important;
font-size: 2rem !important;
.homelab-dashboard .rule-line-thin {
border: none;
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;
letter-spacing: 6px !important;
text-shadow: 3px 3px 0 var(--color-body) !important;
line-height: 1.4 !important;
letter-spacing: -0.02em !important;
line-height: 1.1 !important;
border: none !important;
}
.homelab-dashboard .subtitle {
font-family: 'IBM Plex Mono', monospace;
color: var(--color-primary);
font-size: 0.55rem;
margin-top: 15px;
letter-spacing: 2px;
.homelab-dashboard .masthead-eyebrow {
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--color-text-light);
align-self: flex-end;
padding-bottom: 0.35rem;
}
.homelab-dashboard .score-bar {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 20px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.5rem;
.homelab-dashboard .tagline {
font-family: "Source Serif 4", Georgia, serif;
font-size: 1.125rem;
color: var(--color-text-light);
max-width: 65ch;
line-height: 1.65;
margin-bottom: 1.5rem;
}
.homelab-dashboard .score-item {
color: var(--color-text);
background: var(--color-primary);
padding: 6px 14px;
border: 2px solid var(--color-primary-light-2);
/* Section header */
.homelab-dashboard .section-header {
margin-bottom: 1.5rem;
}
.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 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
padding: 20px 0;
margin-top: 20px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
margin-bottom: 3rem;
}
/* Tiles */
.homelab-dashboard .tile {
background: var(--color-box-body);
border: 3px solid var(--color-secondary-dark-2) !important;
padding: 25px;
border: 1px solid var(--color-border) !important;
border-top: 3px solid var(--color-text) !important;
padding: 1.5rem;
position: relative;
overflow: hidden;
transition: all 0.2s ease;
transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer;
text-decoration: none !important;
display: block;
@@ -64,97 +110,143 @@
background: var(--color-body);
}
.homelab-dashboard .tile .icon {
font-size: 2.5rem;
margin-bottom: 15px;
font-size: 1.5rem;
margin-bottom: 0.75rem;
display: block;
line-height: 1;
}
.homelab-dashboard .tile.storage .icon,
.homelab-dashboard .tile.database .icon { color: var(--color-red); }
.homelab-dashboard .tile.git .icon { color: var(--color-primary-light-2); }
.homelab-dashboard .tile.ci .icon { color: var(--color-primary-light-1); }
.homelab-dashboard .tile.database .icon { color: var(--color-warning); }
.homelab-dashboard .tile.git .icon { color: var(--color-primary); }
.homelab-dashboard .tile.ci .icon { color: var(--color-primary); }
.homelab-dashboard .tile.notebook .icon,
.homelab-dashboard .tile.research .icon { color: var(--color-text); }
.homelab-dashboard .tile.catalog .icon { color: var(--color-primary-light-2); }
.homelab-dashboard .tile.query .icon { color: var(--color-primary-light-1); }
.homelab-dashboard .tile.observability .icon { color: var(--color-red); }
.homelab-dashboard .tile.metrics .icon { color: var(--color-primary-light-2); }
.homelab-dashboard .tile.logs .icon { color: var(--color-primary-light-1); }
.homelab-dashboard .tile.catalog .icon { color: var(--color-primary); }
.homelab-dashboard .tile.query .icon { color: var(--color-primary); }
.homelab-dashboard .tile.observability .icon { color: var(--color-warning); }
.homelab-dashboard .tile.metrics .icon { color: var(--color-primary); }
.homelab-dashboard .tile.logs .icon { color: var(--color-primary); }
.homelab-dashboard .tile.proxy .icon { color: var(--color-text); }
.homelab-dashboard .tile-title {
font-family: 'IBM Plex Mono', monospace !important;
font-size: 0.7rem !important;
color: var(--color-text) !important;
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 {
/* Tile eyebrow — mono metadata label */
.homelab-dashboard .tile-eyebrow {
font-family: "JetBrains Mono", "Fira Code", monospace;
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;
}
/* Status dot */
.homelab-dashboard .status-dot {
position: absolute;
top: 15px;
right: 15px;
width: 10px;
height: 10px;
background: var(--color-green);
top: 1rem;
right: 1rem;
width: 8px;
height: 8px;
background: var(--color-success);
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 {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--color-red);
height: 2px;
background: var(--color-border);
}
.homelab-dashboard .hdr-badge {
position: absolute;
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); }
/* Footer */
.homelab-dashboard .ftr {
text-align: center;
padding: 40px;
color: var(--color-text-light-2);
font-size: 0.7rem;
border-top: 3px solid var(--color-secondary-dark-2);
margin-top: 20px;
padding-top: 1rem;
border-top: 3px solid var(--color-text);
color: var(--color-text-light);
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 0.65rem;
letter-spacing: 0.08em;
}
.homelab-dashboard .ftr span { color: var(--color-red); }
.homelab-dashboard .ftr span { color: var(--color-warning); }
</style>
<div class="homelab-dashboard">
<div class="hdr">
<h1>HOMELAB</h1>
<p class="subtitle">COMMAND CENTER</p>
<div class="score-bar">
<span class="score-item">SERVICES: 15</span>
<span class="score-item">STATUS: ONLINE</span>
<div class="masthead">
<div class="rule-line"></div>
<div class="masthead-row">
<h1>Homelab</h1>
<span class="masthead-eyebrow">git.fhirworx.io</span>
</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 class="grid">
<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="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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -163,7 +255,8 @@
<a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="status-dot"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -172,8 +265,9 @@
<a data-subdomain="git" class="tile git">
<span class="status-dot"></span>
<span class="icon">&#128025;</span>
<h2 class="tile-title">GITEA</h2>
<p class="tile-desc">Self-hosted Git service with packages & CI</p>
<div class="tile-eyebrow">Version Control</div>
<div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -181,68 +275,75 @@
<a data-subdomain="ci" target="_blank" class="tile ci">
<span class="status-dot"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="icon">&#128209;</span>
<h2 class="tile-title">NOTEBOOKS</h2>
<p class="tile-desc">Marimo reactive notebooks with CUDA</p>
<div class="tile-eyebrow">Analysis</div>
<div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="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>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="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>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="icon">&#127795;</span>
<h2 class="tile-title">NESSIE</h2>
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="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>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="icon">&#11088;</span>
<h2 class="tile-title">POLARIS</h2>
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -250,8 +351,9 @@
<a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="status-dot"></span>
<span class="icon">&#128202;</span>
<h2 class="tile-title">GRAFANA</h2>
<p class="tile-desc">Observability dashboards for logs and traces</p>
<div class="tile-eyebrow">Observability</div>
<div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -259,7 +361,8 @@
<a data-subdomain="jaeger" target="_blank" class="tile metrics">
<span class="status-dot"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -268,7 +371,8 @@
<a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="status-dot"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -277,7 +381,8 @@
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
<span class="status-dot"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -285,14 +390,11 @@
</div>
<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>
<script>
(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 base = parts.length > 2 ? parts.slice(1).join('.') : location.hostname;
document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) {

View File

@@ -3,27 +3,40 @@
<head>
<meta charset="UTF-8">
<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="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="/dashboard.css">
</head>
<body>
<div class="container">
<header>
<h1>FHIRWORX</h1>
<p class="subtitle">COMMAND CENTER</p>
<div class="score-bar">
<span class="score-item">SERVICES: 15</span>
<span class="score-item">STATUS: ONLINE</span>
<div class="masthead">
<div class="rule-line"></div>
<div class="masthead-row">
<h1>Fhirworx</h1>
<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>
</header>
<div class="grid">
<a data-subdomain="traefik" target="_blank" class="tile proxy">
<span class="status"></span>
<span class="icon">&#128279;</span>
<h2 class="tile-title">TRAEFIK</h2>
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p>
<div class="tile-eyebrow">Networking</div>
<div class="tile-title">Traefik</div>
<p class="tile-desc">Cloud-native reverse proxy and load balancer with automatic TLS</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -31,7 +44,8 @@
<a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -40,8 +54,9 @@
<a data-subdomain="git" target="_blank" class="tile git">
<span class="status"></span>
<span class="icon">&#128025;</span>
<h2 class="tile-title">GITEA</h2>
<p class="tile-desc">Self-hosted Git service with packages & CI</p>
<div class="tile-eyebrow">Version Control</div>
<div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -49,7 +64,8 @@
<a data-subdomain="ci" target="_blank" class="tile ci">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -59,8 +75,9 @@
<span class="badge gpu">GPU</span>
<span class="status"></span>
<span class="icon">&#128209;</span>
<h2 class="tile-title">NOTEBOOKS</h2>
<p class="tile-desc">Marimo reactive notebooks with CUDA</p>
<div class="tile-eyebrow">Analysis</div>
<div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -69,7 +86,8 @@
<span class="badge gpu">GPU</span>
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -78,27 +96,30 @@
<a data-subdomain="docs" target="_blank" class="tile research">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<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="icon">&#127795;</span>
<h2 class="tile-title">NESSIE</h2>
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="trino" target="_blank" class="tile query">
<span class="badge data">DATA</span>
<span class="badge data">Data</span>
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -107,8 +128,9 @@
<a data-subdomain="polaris" target="_blank" class="tile catalog">
<span class="status"></span>
<span class="icon">&#11088;</span>
<h2 class="tile-title">POLARIS</h2>
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access control</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -116,8 +138,9 @@
<a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="status"></span>
<span class="icon">&#128202;</span>
<h2 class="tile-title">GRAFANA</h2>
<p class="tile-desc">Observability dashboards for logs and traces</p>
<div class="tile-eyebrow">Observability</div>
<div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
@@ -125,7 +148,8 @@
<a data-subdomain="jaeger" target="_blank" class="tile metrics">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -134,7 +158,8 @@
<a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -143,7 +168,8 @@
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -152,7 +178,8 @@
<a data-subdomain="api" data-path="/health" target="_blank" class="tile proxy">
<span class="status"></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>
<span class="tile-port"></span>
<div class="stripe"></div>
@@ -160,12 +187,11 @@
</div>
<footer>
MADE WITH <span>&#9829;</span> FOR FHIRWORX
<span>fhirworx.io</span> &mdash; self-hosted data lakehouse &mdash; &copy; 2025
</footer>
</div>
<script>
// All tiles use https://{subdomain}.fhirworx.io
(function() {
var base = location.hostname.replace(/^dashboard\./, '');
document.querySelectorAll('[data-subdomain]').forEach(function(el) {

View File

@@ -44,13 +44,13 @@ http:
referrerPolicy: "strict-origin-when-cross-origin"
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow"
inject-loch:
inject-fhirworx:
plugin:
rewrite-body:
lastModified: true
rewrites:
- 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:
# auth.DOMAIN — oauth2-proxy public endpoints (login, callback, sign_out)
auth-host:
@@ -71,7 +71,7 @@ http:
entryPoints:
- web
{{- $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 "" -}}
{{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}}
{{- if $mwList }}
@@ -100,7 +100,7 @@ http:
entryPoints:
- web
{{- $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 "" -}}
{{- if ne $extra "" }}{{ range splitList "," $extra }}{{ $mwList = append $mwList (trim .) }}{{ end }}{{ end -}}
{{- if $mwList }}
@@ -128,7 +128,7 @@ http:
entryPoints:
- web
middlewares:
- inject-loch
- inject-fhirworx
- gitea-sso
- infra-headers
traefik-dashboard-tls:
@@ -138,7 +138,7 @@ http:
- websecure
tls: {}
middlewares:
- inject-loch
- inject-fhirworx
- gitea-sso
- infra-headers
services:

View File

@@ -35,7 +35,7 @@ def _():
connect.theme()
_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()

View File

@@ -35,9 +35,9 @@ _assets_dir = str(_Path(__file__).resolve().parents[2] / "assets")
if _assets_dir not in _sys.path:
_sys.path.insert(0, _assets_dir)
from nature import EXTERNAL_COLOR as _EXTERNAL_COLOR # noqa: E402, I001
from nature import SCHEMA_COLORS as _SCHEMA_COLORS # noqa: E402, F401
from nature import schema_color as _color # noqa: E402
from fhirworx import EXTERNAL_COLOR as _EXTERNAL_COLOR # noqa: E402, I001
from fhirworx import SCHEMA_COLORS as _SCHEMA_COLORS # noqa: E402, F401
from fhirworx import schema_color as _color # 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:
"""Activate the Nature/loch Altair chart theme."""
"""Activate the fhirworx Altair chart theme."""
import sys
assets_dir = str(ROOT / "assets")
if assets_dir not in sys.path:
sys.path.insert(0, assets_dir)
from nature import altair_theme
from fhirworx import altair_theme
altair_theme()

View File

@@ -188,4 +188,4 @@ class TestTheme:
connect.theme()
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):
assert (INFRA_MARIMO / "home-page-patched.js").is_file()
def test_no_loch_css_duplicate(self):
"""loch.css should NOT be in infra/marimo/ — served from assets/css/."""
assert not (INFRA_MARIMO / "loch.css").exists()
def test_no_fhirworx_css_duplicate(self):
"""fhirworx.css should NOT be in infra/marimo/ — served from assets/css/."""
assert not (INFRA_MARIMO / "fhirworx.css").exists()
class TestNoStaleReferences: