feat(gitea): homelab landing as the Gitea home and dashboard

The home page, signed-in dashboard and navbar render the homelab
landing template (service panels with health badges) instead of
Gitea's stock home.
This commit is contained in:
kert
2026-11-11 12:57:06 -05:00
parent 607dd40e62
commit 835ae41e88
5 changed files with 696 additions and 421 deletions

View File

@@ -0,0 +1,182 @@
{{/* fhirworx copy of Gitea 1.25.4 templates/base/head_navbar.tmpl.
Home (custom/extra_links) is the first link, before Explore.
Re-diff against the new version whenever the Gitea image changes. */}}
<nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
<div class="ui secondary menu navbar-mobile-right only-mobile">
{{template "base/head_navbar_icons" dict "PageGlobalData" .PageGlobalData}}
<button class="item ui icon mini button tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
</div>
<!-- navbar links non-mobile. Home is first. -->
{{template "custom/extra_links" .}}
{{if and .IsSigned .MustChangePassword}}
{{/* No links */}}
{{else if .IsSigned}}
{{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
<a class="item{{if .PageIsIssues}} active{{end}}" href="{{AppSubUrl}}/issues">{{ctx.Locale.Tr "issues"}}</a>
{{end}}
{{if not ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}}
<a class="item{{if .PageIsPulls}} active{{end}}" href="{{AppSubUrl}}/pulls">{{ctx.Locale.Tr "pull_requests"}}</a>
{{end}}
{{if not (and ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled)}}
{{if .ShowMilestonesDashboardPage}}
<a class="item{{if .PageIsMilestonesDashboard}} active{{end}}" href="{{AppSubUrl}}/milestones">{{ctx.Locale.Tr "milestones"}}</a>
{{end}}
{{end}}
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore"}}</a>
{{else if .IsLandingPageOrganizations}}
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/organizations">{{ctx.Locale.Tr "explore"}}</a>
{{else}}
<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore"}}</a>
{{end}}
{{if not .IsSigned}}
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">{{ctx.Locale.Tr "help"}}</a>
{{end}}
</div>
<!-- the full dropdown menus -->
<div class="navbar-right">
{{if and .IsSigned .MustChangePassword}}
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
<span class="text">
{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
<span class="only-mobile">{{.SignedUser.Name}}</span>
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
</span>
<div class="menu user-menu">
<div class="header">
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
</div>
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
</div><!-- end content avatar menu -->
</div><!-- end dropdown avatar menu -->
{{else if .IsSigned}}
{{template "base/head_navbar_icons" dict "ItemExtraClass" "not-mobile" "PageGlobalData" .PageGlobalData}}
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
<span class="text">
{{svg "octicon-plus"}}
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
</span>
<div class="menu">
<a class="item" href="{{AppSubUrl}}/repo/create">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "new_repo"}}
</a>
{{if not .DisableMigrations}}
<a class="item" href="{{AppSubUrl}}/repo/migrate">
{{svg "octicon-repo-push"}} {{ctx.Locale.Tr "new_migrate"}}
</a>
{{end}}
{{if .SignedUser.CanCreateOrganization}}
<a class="item" href="{{AppSubUrl}}/org/create">
{{svg "octicon-organization"}} {{ctx.Locale.Tr "new_org"}}
</a>
{{end}}
</div><!-- end content create new menu -->
</div><!-- end dropdown menu create new -->
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
<span class="text">
{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
<span class="only-mobile">{{.SignedUser.Name}}</span>
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
</span>
<div class="menu user-menu">
<div class="header">
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
</div>
<div class="divider"></div>
<a class="item" href="{{.SignedUser.HomeLink}}">
{{svg "octicon-person"}}
{{ctx.Locale.Tr "your_profile"}}
</a>
{{if not .DisableStars}}
<a class="item" href="{{.SignedUser.HomeLink}}?tab=stars">
{{svg "octicon-star"}}
{{ctx.Locale.Tr "your_starred"}}
</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/notifications/subscriptions">
{{svg "octicon-bell"}}
{{ctx.Locale.Tr "notification.subscriptions"}}
</a>
<a class="{{if .PageIsUserSettings}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
{{svg "octicon-tools"}}
{{ctx.Locale.Tr "your_settings"}}
</a>
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">
{{svg "octicon-question"}}
{{ctx.Locale.Tr "help"}}
</a>
{{if .IsAdmin}}
<div class="divider"></div>
<a class="{{if .PageIsAdmin}}active {{end}}item" href="{{AppSubUrl}}/-/admin">
{{svg "octicon-server"}}
{{ctx.Locale.Tr "admin_panel"}}
</a>
{{end}}
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
</div><!-- end content avatar menu -->
</div><!-- end dropdown avatar menu -->
{{else}}
{{if .ShowRegistrationButton}}
<a class="item{{if .PageIsSignUp}} active{{end}}" href="{{AppSubUrl}}/user/sign_up">
{{svg "octicon-person"}}
<span class="tw-ml-1">{{ctx.Locale.Tr "register"}}</span>
</a>
{{end}}
<a class="item{{if .PageIsSignIn}} active{{end}}" rel="nofollow" href="{{AppSubUrl}}/user/login{{if not .PageIsSignIn}}?redirect_to={{.CurrentURL}}{{end}}">
{{svg "octicon-sign-in"}}
<span class="tw-ml-1">{{ctx.Locale.Tr "sign_in"}}</span>
</a>
{{end}}
</div><!-- end full right menu -->
{{$activeStopwatch := and .PageGlobalData (call .PageGlobalData.GetActiveStopwatch)}}
{{if $activeStopwatch}}
<div class="active-stopwatch-popup tippy-target">
<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{$activeStopwatch.IssueLink}}">
{{svg "octicon-issue-opened" 16}}
<span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span>
</a>
<div class="tw-flex tw-gap-1">
<form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/stop">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
</div>
</div>
{{end}}
</nav>

View File

@@ -0,0 +1 @@
<a class="item{{if or .PageIsHome .PageIsDashboard}} active{{end}}" href="{{AppSubUrl}}/">Home</a>

View File

@@ -1,423 +1,3 @@
{{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: 1180px;
margin: 0 auto;
padding: 3rem 1.5rem;
font-family: "Source Serif 4", Georgia, serif;
}
/* Rule lines */
.homelab-dashboard .rule-line {
border: none;
border-top: 3px solid var(--color-text);
margin-bottom: 1rem;
}
.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-brand {
display: flex;
align-items: center;
gap: 1rem;
}
.homelab-dashboard .masthead-logo {
width: clamp(48px, 6vw, 80px);
height: clamp(48px, 6vw, 80px);
display: block;
flex: 0 0 auto;
}
.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: -0.02em !important;
line-height: 1.1 !important;
border: none !important;
margin: 0 !important;
}
.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 .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;
}
/* Section header */
.homelab-dashboard .section-header {
margin-bottom: 1.5rem;
background: transparent !important;
padding: 0 !important;
border: none !important;
}
.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;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
display: block !important;
}
/* Grid */
.homelab-dashboard .grid {
display: grid;
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: 1px solid var(--color-border) !important;
border-top: 3px solid var(--color-text) !important;
padding: 1.5rem;
position: relative;
overflow: hidden;
transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer;
text-decoration: none !important;
display: block;
border-radius: 0 !important;
}
.homelab-dashboard .tile:hover {
border-color: var(--color-primary) !important;
background: var(--color-body);
}
.homelab-dashboard .tile .icon {
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-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); }
.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); }
/* Tile eyebrow — mono metadata label */
.homelab-dashboard .tile-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.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: 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: 2px;
background: var(--color-border);
}
/* Footer */
.homelab-dashboard .ftr {
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-warning); }
</style>
<div class="homelab-dashboard">
<div class="masthead">
<div class="rule-line"></div>
<div class="masthead-row">
<div class="masthead-brand">
<img src="/assets/img/logo.svg" alt="fhirworx" class="masthead-logo">
<h1>Homelab</h1>
</div>
<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="tile-badge new">New</span>
<span class="status-dot"></span>
<span class="icon">&#128279;</span>
<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>
</a>
<a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="status-dot"></span>
<span class="icon">&#128190;</span>
<div class="tile-eyebrow">Storage</div>
<div class="tile-title">RustFS</div>
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="git" class="tile git">
<span class="status-dot"></span>
<span class="icon">&#128025;</span>
<div class="tile-eyebrow">Version Control</div>
<div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="notebooks" target="_blank" class="tile notebook">
<span class="tile-badge gpu">GPU</span>
<span class="status-dot"></span>
<span class="icon">&#128209;</span>
<div class="tile-eyebrow">Analysis</div>
<div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="zotero" target="_blank" class="tile research">
<span class="tile-badge gpu">GPU</span>
<span class="status-dot"></span>
<span class="icon">&#128218;</span>
<div class="tile-eyebrow">Research</div>
<div class="tile-title">Zotero</div>
<p class="tile-desc">Research reference manager in the cloud</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="docs" target="_blank" class="tile research">
<span class="tile-badge new">New</span>
<span class="status-dot"></span>
<span class="icon">&#128214;</span>
<div class="tile-eyebrow">Documentation</div>
<div class="tile-title">Docs</div>
<p class="tile-desc">API reference and CMS bibliography browser</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog">
<span class="tile-badge data">Data</span>
<span class="status-dot"></span>
<span class="icon">&#127795;</span>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="trino" target="_blank" class="tile query">
<span class="tile-badge data">Data</span>
<span class="status-dot"></span>
<span class="icon">&#128269;</span>
<div class="tile-eyebrow">Query</div>
<div class="tile-title">Trino</div>
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="polaris" target="_blank" class="tile catalog">
<span class="tile-badge new">New</span>
<span class="status-dot"></span>
<span class="icon">&#11088;</span>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="status-dot"></span>
<span class="icon">&#128202;</span>
<div class="tile-eyebrow">Observability</div>
<div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="tempo" target="_blank" class="tile metrics">
<span class="status-dot"></span>
<span class="icon">&#128065;</span>
<div class="tile-eyebrow">Tracing</div>
<div class="tile-title">Tempo</div>
<p class="tile-desc">Distributed tracing for microservices</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="status-dot"></span>
<span class="icon">&#128200;</span>
<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>
</a>
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
<span class="status-dot"></span>
<span class="icon">&#128203;</span>
<div class="tile-eyebrow">Logs</div>
<div class="tile-title">Loki</div>
<p class="tile-desc">Log aggregation system by Grafana</p>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
</div>
<div class="ftr">
<span>fhirworx.io</span> &mdash; self-hosted data lakehouse &mdash; &copy; 2025
</div>
</div>
<script>
(function() {
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) {
var sub = el.dataset.subdomain;
var path = el.dataset.path || '';
var fqdn = sub + '.' + base;
el.href = location.protocol + '//' + fqdn + path;
el.target = '_blank';
var port = el.querySelector('.tile-port');
if (port) port.textContent = fqdn;
});
})();
</script>
{{template "homelab/landing" .}}
{{template "base/footer" .}}

View File

@@ -0,0 +1,509 @@
<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: 1180px;
margin: 0 auto;
padding: 3rem 1.5rem;
font-family: "Source Serif 4", Georgia, serif;
}
/* Rule lines */
.homelab-dashboard .rule-line {
border: none;
border-top: 3px solid var(--color-text);
margin-bottom: 1rem;
}
.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-brand {
display: flex;
align-items: center;
gap: 1rem;
}
.homelab-dashboard .masthead-logo {
width: clamp(48px, 6vw, 80px);
height: clamp(48px, 6vw, 80px);
display: block;
flex: 0 0 auto;
}
.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: -0.02em !important;
line-height: 1.1 !important;
border: none !important;
margin: 0 !important;
}
.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 .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;
}
/* Section header */
.homelab-dashboard .section-header {
margin-bottom: 1.5rem;
background: transparent !important;
padding: 0 !important;
border: none !important;
}
.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;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
display: block !important;
}
/* Grid */
.homelab-dashboard .grid {
display: grid;
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: 1px solid var(--color-border) !important;
border-top: 3px solid var(--color-text) !important;
padding: 1.5rem;
position: relative;
overflow: hidden;
transition: border-color 0.15s ease, background 0.15s ease;
cursor: pointer;
text-decoration: none !important;
display: block;
border-radius: 0 !important;
}
.homelab-dashboard .tile:hover {
border-color: var(--color-primary) !important;
background: var(--color-body);
}
.homelab-dashboard .tile .icon {
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-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); }
.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); }
/* Tile eyebrow — mono metadata label */
.homelab-dashboard .tile-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.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;
}
/* Health, inside the tile. Same shape as the corwins.media status badge:
a label, a status word, and a left edge that carries the state.
The state colours come after the border so they win. */
.homelab-dashboard .health {
display: flex;
flex-direction: column;
margin: 0 0 0.85rem;
padding: 8px 10px;
border: 1px solid var(--color-border);
border-left-width: 4px;
background: var(--color-body);
}
.homelab-dashboard .health-k {
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-light);
}
.homelab-dashboard .health-v {
font-size: 18px;
font-weight: 600;
line-height: 1.2;
color: var(--color-text);
}
.homelab-dashboard .health-ok { border-left-color: var(--color-green, var(--color-success)) !important; }
.homelab-dashboard .health-warn { border-left-color: var(--color-yellow, var(--color-warning)) !important; }
.homelab-dashboard .health-bad { border-left-color: var(--color-red, var(--color-error)) !important; }
.homelab-dashboard .health-unknown { border-left-color: var(--color-text-light) !important; }
/* 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: 2px;
background: var(--color-border);
}
/* Footer */
.homelab-dashboard .ftr {
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-warning); }
</style>
<div class="homelab-dashboard">
<div class="masthead">
<div class="rule-line"></div>
<div class="masthead-row">
<div class="masthead-brand">
<img src="/assets/img/logo.svg" alt="fhirworx" class="masthead-logo">
<h1>Homelab</h1>
</div>
<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" data-health-origin="https://fhirworx.io">
<a data-subdomain="llm" target="_blank" class="tile notebook">
<span class="tile-badge gpu">GPU</span>
<span class="icon">&#128172;</span>
<div class="tile-eyebrow">Chat</div>
<div class="tile-title">Comment Chat</div>
<p class="tile-desc">RAG chat over indexed CMS rulemaking comments, with cited sources</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="bcda" target="_blank" class="tile notebook">
<span class="icon">&#128196;</span>
<div class="tile-eyebrow">Claims</div>
<div class="tile-title">BCDA Manager</div>
<p class="tile-desc">Beneficiary Claims Data API v3 — exports, store, load, crosswalk</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="lead" target="_blank" class="tile notebook">
<span class="icon">&#128200;</span>
<div class="tile-eyebrow">LEAD</div>
<div class="tile-title">Public mirror</div>
<p class="tile-desc">Read-only explorer over the public-data DuckDB mirror</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="reach" target="_blank" class="tile research">
<span class="icon">&#128202;</span>
<div class="tile-eyebrow">REACH</div>
<div class="tile-title">Public mirror</div>
<p class="tile-desc">Read-only explorer over the public-data DuckDB mirror</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="traefik" target="_blank" class="tile proxy">
<span class="tile-badge new">New</span>
<span class="icon">&#128279;</span>
<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="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="s3console" target="_blank" class="tile storage">
<span class="icon">&#128190;</span>
<div class="tile-eyebrow">Storage</div>
<div class="tile-title">RustFS</div>
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="git" class="tile git">
<span class="icon">&#128025;</span>
<div class="tile-eyebrow">Version Control</div>
<div class="tile-title">Gitea</div>
<p class="tile-desc">Self-hosted Git service with packages and CI</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="notebooks" target="_blank" class="tile notebook">
<span class="tile-badge gpu">GPU</span>
<span class="icon">&#128209;</span>
<div class="tile-eyebrow">Analysis</div>
<div class="tile-title">Notebooks</div>
<p class="tile-desc">Marimo reactive notebooks with CUDA acceleration</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="zotero" target="_blank" class="tile research">
<span class="tile-badge gpu">GPU</span>
<span class="icon">&#128218;</span>
<div class="tile-eyebrow">Research</div>
<div class="tile-title">Zotero</div>
<p class="tile-desc">Research reference manager in the cloud</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="docs" target="_blank" class="tile research">
<span class="tile-badge new">New</span>
<span class="icon">&#128214;</span>
<div class="tile-eyebrow">Documentation</div>
<div class="tile-title">Docs</div>
<p class="tile-desc">API reference and CMS bibliography browser</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="nessie" target="_blank" class="tile catalog">
<span class="tile-badge data">Data</span>
<span class="icon">&#127795;</span>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Nessie</div>
<p class="tile-desc">Git-like versioned data catalog for Apache Iceberg</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="trino" target="_blank" class="tile query">
<span class="tile-badge data">Data</span>
<span class="icon">&#128269;</span>
<div class="tile-eyebrow">Query</div>
<div class="tile-title">Trino</div>
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="polaris" target="_blank" class="tile catalog">
<span class="tile-badge new">New</span>
<span class="icon">&#11088;</span>
<div class="tile-eyebrow">Catalog</div>
<div class="tile-title">Polaris</div>
<p class="tile-desc">Apache Iceberg catalog with governance and Unity-style access</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="grafana" target="_blank" class="tile observability">
<span class="icon">&#128202;</span>
<div class="tile-eyebrow">Observability</div>
<div class="tile-title">Grafana</div>
<p class="tile-desc">Observability dashboards for metrics, logs, and traces</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="tempo" target="_blank" class="tile metrics">
<span class="icon">&#128065;</span>
<div class="tile-eyebrow">Tracing</div>
<div class="tile-title">Tempo</div>
<p class="tile-desc">Distributed tracing for microservices</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="prometheus" target="_blank" class="tile logs">
<span class="icon">&#128200;</span>
<div class="tile-eyebrow">Metrics</div>
<div class="tile-title">Prometheus</div>
<p class="tile-desc">Metrics collection and alerting</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="loki" target="_blank" class="tile database">
<span class="icon">&#128203;</span>
<div class="tile-eyebrow">Logs</div>
<div class="tile-title">Loki</div>
<p class="tile-desc">Log aggregation system by Grafana</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
<a data-subdomain="api" target="_blank" class="tile proxy">
<span class="icon">&#9881;</span>
<div class="tile-eyebrow">API</div>
<div class="tile-title">Platform API</div>
<p class="tile-desc">FastAPI platform backend</p>
<span class="health health-unknown"><span class="health-k">health</span><span class="health-v">checking</span></span>
<span class="tile-port"></span>
<div class="stripe"></div>
</a>
</div>
<div class="ftr">
<span>fhirworx.io</span> &mdash; self-hosted data lakehouse &mdash; &copy; 2025
</div>
</div>
<script>
function paintHealth(el, origin, sub, fqdn) {
var box = el.querySelector('.health');
if (!box) return;
var value = box.querySelector('.health-v');
function paint(kind, text) {
box.className = 'health health-' + kind;
value.textContent = text;
}
fetch(origin + '/healthz/' + sub, {cache: 'no-store'}).then(function (r) {
if (r.status === 503) paint('warn', 'not ready');
else if (r.ok) paint('ok', 'up');
else paint('bad', 'down');
}, function () {
paint('bad', 'down');
});
}
(function() {
var root = document.querySelector('[data-health-origin]');
var origin = root ? (root.getAttribute('data-health-origin') || '') : '';
var parts = location.hostname.split('.');
var base = parts.length > 2 ? parts.slice(1).join('.') : location.hostname;
document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) {
var sub = el.dataset.subdomain;
var fqdn = sub + '.' + base;
el.href = 'https://' + fqdn + '/';
el.target = '_blank';
var port = el.querySelector('.tile-port');
if (port) port.textContent = fqdn;
paintHealth(el, origin, sub, fqdn);
});
})();
</script>

View File

@@ -0,0 +1,3 @@
{{template "base/head" .}}
{{template "homelab/landing" .}}
{{template "base/footer" .}}