first commit

This commit is contained in:
Blake Ridgway
2026-03-07 21:16:51 -06:00
parent 21bd542469
commit 03fcf37beb
33 changed files with 3532 additions and 0 deletions

52
templates/about.html Normal file
View File

@@ -0,0 +1,52 @@
{{define "title"}}About — Ridgway Systems{{end}}
{{define "meta-desc"}}About Ridgway Systems — a personal OpenBSD homelab project.{{end}}
{{define "content"}}
<div class="page-header">
<h1>About</h1>
</div>
<div class="prose">
<p>
Ridgway Systems is a personal homelab project built entirely on OpenBSD. The goal is to self-host
as many services as practical on owned hardware, with a focus on simplicity, security, and
understanding every layer of the stack.
</p>
<p>
This site documents the build — hardware choices, configuration decisions, and things learned
along the way. If you're setting up your own homelab or migrating to OpenBSD, hopefully something
here is useful.
</p>
<h2>Why OpenBSD?</h2>
<ul>
<li>Security-first design. <code>pledge(2)</code> and <code>unveil(2)</code> are excellent.</li>
<li>Clean, minimal base system. No surprises.</li>
<li><code>pf(4)</code> is the best firewall I've used.</li>
<li>Documentation is thorough and accurate. The man pages are genuinely good.</li>
<li>Deliberate, careful development. The OpenBSD team doesn't chase hype.</li>
</ul>
<h2>What's Running</h2>
<p>
See the <a href="/infrastructure">infrastructure page</a> for the full hardware and service list.
Briefly: a SuperMicro 1U as the firewall/router, a Dell R720 as the primary server, and a
Dell R710 for backup and game servers. Everything is managed with Ansible.
</p>
<h2>Can I see the Ansible playbooks?</h2>
<p>
Eventually. The playbooks are on the <a href="https://git.ridgwaysystems.org">Gitea instance</a>
(private for now while things are in flux). Plan is to open them up once they're in a state
I'm not embarrassed by.
</p>
<h2>Contact</h2>
<ul class="contact-list">
<li>Email: <a href="mailto:bridgway@ridgwaysystems.org">bridgway@ridgwaysystems.org</a></li>
<li>Gitea: <a href="https://git.ridgwaysystems.org">git.ridgwaysystems.org</a></li>
<li>Mastodon: <a href="https://mastodon.social/@bridgway" rel="me">@bridgway@mastodon.social</a></li>
</ul>
</div>
{{end}}

View File

@@ -0,0 +1,55 @@
{{define "title"}}Admin Dashboard &mdash; Ridgway Systems{{end}}
{{define "content"}}
<div class="admin-wrap">
<div class="admin-header">
<h1>Admin Dashboard</h1>
<div class="admin-actions">
<a href="/admin/new" class="btn">New Post</a>
<a href="/admin/status" class="btn btn-outline">Edit Status</a>
<a href="/" class="btn btn-outline">View Site</a>
<form method="POST" action="/admin/logout" class="inline-form">
<button type="submit" class="btn btn-outline">Logout</button>
</form>
</div>
</div>
{{if .Flash}}
<p class="flash-msg">{{.Flash}}</p>
{{end}}
<h2>Posts</h2>
{{if .Posts}}
<table class="admin-table">
<thead>
<tr>
<th>Title</th>
<th>Date</th>
<th>Status</th>
<th>Tags</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Posts}}
<tr>
<td><a href="/blog/{{.Slug}}">{{.Title}}</a></td>
<td>{{formatDate .ParsedDate}}</td>
<td>{{if .Draft}}<span class="draft-badge">draft</span>{{else}}<span class="pub-badge">published</span>{{end}}</td>
<td class="tags-cell">{{range .Tags}}<span class="tag">#{{.}}</span> {{end}}</td>
<td class="actions-cell">
<a href="/admin/edit/{{.Slug}}" class="btn btn-sm">Edit</a>
<form method="POST" action="/admin/delete/{{.Slug}}" class="inline-form"
onsubmit="return confirm('Delete {{.Title}}?')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="empty-state">No posts yet. <a href="/admin/new">Create the first one.</a></p>
{{end}}
</div>
{{end}}

104
templates/admin/editor.html Normal file
View File

@@ -0,0 +1,104 @@
{{define "title"}}{{if .IsNew}}New Post{{else}}Edit Post{{end}} &mdash; Admin{{end}}
{{define "content"}}
<div class="admin-wrap">
<div class="admin-header">
<h1>{{if .IsNew}}New Post{{else}}Edit: {{if .Post}}{{.Post.Title}}{{end}}{{end}}</h1>
<div class="admin-actions">
<a href="/admin" class="btn btn-outline">Back to Dashboard</a>
</div>
</div>
{{if .Error}}
<p class="form-error">{{.Error}}</p>
{{end}}
<form method="POST" action="{{if .IsNew}}/admin/new{{else}}/admin/edit/{{if .Post}}{{.Post.Slug}}{{end}}{{end}}" class="editor-form" id="post-form">
{{if .IsNew}}
<div class="form-row">
<label for="slug">Slug (filename, no .md)</label>
<input type="text" id="slug" name="slug" placeholder="my-post-slug" pattern="[a-z0-9\-_]+" required>
</div>
{{end}}
<div class="editor-toolbar">
<input type="file" id="img-file" accept="image/*" style="display:none">
<button type="button" id="upload-btn" class="btn btn-sm btn-outline">Insert Image</button>
<span id="upload-status" class="upload-status"></span>
</div>
<div class="editor-layout">
<div class="editor-pane">
<label for="content">Markdown</label>
<textarea id="content" name="content" class="editor-textarea" spellcheck="false">{{.Raw}}</textarea>
</div>
<div class="preview-pane">
<div class="preview-label">Preview <button type="button" id="preview-btn" class="btn btn-sm">Refresh</button></div>
<div id="preview-output" class="preview-output prose"></div>
</div>
</div>
<div class="editor-footer">
<button type="submit" class="btn">Save</button>
<a href="/admin" class="btn btn-outline">Cancel</a>
</div>
</form>
</div>
<script>
(function() {
var previewBtn = document.getElementById('preview-btn');
var uploadBtn = document.getElementById('upload-btn');
var imgFile = document.getElementById('img-file');
var textarea = document.getElementById('content');
var output = document.getElementById('preview-output');
var uploadStatus = document.getElementById('upload-status');
// --- Preview ---
function refreshPreview() {
var fd = new FormData();
fd.append('content', textarea.value);
fetch('/admin/preview', { method: 'POST', body: fd })
.then(function(r) { return r.text(); })
.then(function(html) { output.innerHTML = html; })
.catch(function() { output.innerHTML = '<p class="form-error">Preview failed.</p>'; });
}
previewBtn.addEventListener('click', refreshPreview);
if (textarea.value.trim()) { refreshPreview(); }
// --- Image upload ---
uploadBtn.addEventListener('click', function() { imgFile.click(); });
imgFile.addEventListener('change', function() {
if (!this.files.length) return;
var file = this.files[0];
var fd = new FormData();
fd.append('image', file);
uploadStatus.textContent = 'Uploading…';
uploadBtn.disabled = true;
fetch('/admin/upload', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
uploadStatus.textContent = 'Error: ' + data.error;
return;
}
// Insert markdown at cursor position
var pos = textarea.selectionStart;
var before = textarea.value.substring(0, pos);
var after = textarea.value.substring(textarea.selectionEnd);
textarea.value = before + data.markdown + after;
textarea.selectionStart = textarea.selectionEnd = pos + data.markdown.length;
textarea.focus();
uploadStatus.textContent = 'Inserted: ' + data.url;
setTimeout(function() { uploadStatus.textContent = ''; }, 3000);
})
.catch(function() { uploadStatus.textContent = 'Upload failed.'; })
.finally(function() { uploadBtn.disabled = false; imgFile.value = ''; });
});
})();
</script>
{{end}}

View File

@@ -0,0 +1,15 @@
{{define "title"}}Admin Login &mdash; Ridgway Systems{{end}}
{{define "content"}}
<div class="admin-login-wrap">
<h1>Admin</h1>
{{if .Error}}
<p class="form-error">{{.Error}}</p>
{{end}}
<form method="POST" action="/admin/login" class="login-form">
<label for="password">Password</label>
<input type="password" id="password" name="password" autofocus required>
<button type="submit">Login</button>
</form>
</div>
{{end}}

View File

@@ -0,0 +1,33 @@
{{define "title"}}Edit Status &mdash; Admin{{end}}
{{define "content"}}
<div class="admin-wrap">
<div class="admin-header">
<h1>Edit Service Status</h1>
<div class="admin-actions">
<a href="/admin" class="btn btn-outline">Back</a>
<a href="/status" class="btn btn-outline">View Status Page</a>
</div>
</div>
{{if .Flash}}
<p class="flash-msg">{{.Flash}}</p>
{{end}}
{{if .Error}}
<p class="form-error">{{.Error}}</p>
{{end}}
<p class="page-desc">
Edit the raw JSON below. Valid status values: <code>up</code>, <code>degraded</code>,
<code>down</code>, <code>unknown</code>.
</p>
<form method="POST" action="/admin/status">
<textarea name="json" class="json-editor" rows="30" spellcheck="false">{{.JSON}}</textarea>
<div class="editor-footer">
<button type="submit" class="btn">Save</button>
</div>
</form>
</div>
{{end}}

49
templates/base.html Normal file
View File

@@ -0,0 +1,49 @@
{{define "base"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}Ridgway Systems{{end}}</title>
<meta name="description" content="{{block "meta-desc" .}}A homelab built on OpenBSD from firewall to git server.{{end}}">
<!-- OpenGraph -->
<meta property="og:site_name" content="Ridgway Systems">
<meta property="og:title" content="{{block "og-title" .}}Ridgway Systems{{end}}">
<meta property="og:description" content="{{block "og-desc" .}}A homelab built on OpenBSD from firewall to git server.{{end}}">
<meta property="og:type" content="{{block "og-type" .}}website{{end}}">
<meta property="og:url" content="{{block "og-url" .}}https://ridgwaysystems.org{{end}}">
<!-- Twitter/X card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{block "tw-title" .}}Ridgway Systems{{end}}">
<meta name="twitter:description" content="{{block "tw-desc" .}}A homelab built on OpenBSD from firewall to git server.{{end}}">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/syntax.css">
<link rel="alternate" type="application/rss+xml" title="Ridgway Systems" href="/blog/feed.xml">
</head>
<body>
<header class="site-header">
<nav class="nav">
<a href="/" class="nav-brand">ridgwaysystems.org</a>
<ul class="nav-links">
<li><a href="/blog">blog</a></li>
<li><a href="/infrastructure">infrastructure</a></li>
<li><a href="/status">status</a></li>
<li><a href="/about">about</a></li>
</ul>
</nav>
</header>
<main class="main-content">
{{block "content" .}}{{end}}
</main>
<footer class="site-footer">
<p>
<a href="/">ridgwaysystems.org</a> &mdash;
running OpenBSD &mdash;
<a href="/blog/feed.xml">RSS</a> &mdash;
<a href="https://git.ridgwaysystems.org">gitea</a>
</p>
</footer>
</body>
</html>
{{end}}

67
templates/blog.html Normal file
View File

@@ -0,0 +1,67 @@
{{define "title"}}Build Log &mdash; Ridgway Systems{{end}}
{{define "meta-desc"}}OpenBSD homelab build log — documenting decisions, problems, and solutions.{{end}}
{{define "content"}}
<div class="page-header">
<h1>Build Log</h1>
<p class="page-desc">Documenting the OpenBSD homelab migration: what was built, how, and why.</p>
</div>
<div class="blog-controls">
<form action="/blog" method="GET" class="search-form" role="search">
{{if .ActiveTag}}<input type="hidden" name="tag" value="{{.ActiveTag}}">{{end}}
<input type="search" name="q" value="{{.SearchQuery}}" placeholder="Search posts…" aria-label="Search posts">
<button type="submit" class="btn btn-sm">Search</button>
{{if .SearchQuery}}<a href="/blog{{if .ActiveTag}}?tag={{.ActiveTag}}{{end}}" class="btn btn-sm btn-outline">Clear</a>{{end}}
</form>
{{if .Tags}}
<div class="tag-filter">
<span class="tag-filter-label">filter:</span>
<a href="/blog{{if .SearchQuery}}?q={{.SearchQuery}}{{end}}" class="tag{{if eq .ActiveTag ""}} tag-active{{end}}">#all</a>
{{range .Tags}}
<a href="/blog?tag={{.}}{{if $.SearchQuery}}&q={{$.SearchQuery}}{{end}}" class="tag{{if eq . $.ActiveTag}} tag-active{{end}}">#{{.}}</a>
{{end}}
</div>
{{end}}
</div>
{{if and .SearchQuery (not .Posts)}}
<p class="empty-state">No results for &ldquo;{{.SearchQuery}}&rdquo;. <a href="/blog">Clear search.</a></p>
{{else if .Posts}}
<ul class="post-list post-list-full">
{{range .Posts}}
<li class="post-item">
<div class="post-meta">
<span class="post-date">{{formatDate .ParsedDate}}</span>
{{if .Draft}}<span class="draft-badge">draft</span>{{end}}
</div>
<a href="/blog/{{.Slug}}" class="post-title">{{.Title}}</a>
{{if .Description}}<p class="post-desc">{{.Description}}</p>{{end}}
{{if .Tags}}
<span class="post-tags">
{{range .Tags}}<a href="/blog?tag={{.}}" class="tag">#{{.}}</a> {{end}}
</span>
{{end}}
</li>
{{end}}
</ul>
{{if gt .TotalPages 1}}
<nav class="pagination" aria-label="Page navigation">
{{if .HasPrev}}
<a href="/blog?page={{.PrevPage}}{{if .ActiveTag}}&tag={{.ActiveTag}}{{end}}{{if .SearchQuery}}&q={{.SearchQuery}}{{end}}" class="btn btn-outline btn-sm">&larr; Newer</a>
{{end}}
<span class="page-indicator">page {{.Page}} of {{.TotalPages}}</span>
{{if .HasNext}}
<a href="/blog?page={{.NextPage}}{{if .ActiveTag}}&tag={{.ActiveTag}}{{end}}{{if .SearchQuery}}&q={{.SearchQuery}}{{end}}" class="btn btn-outline btn-sm">Older &rarr;</a>
{{end}}
</nav>
{{end}}
{{else}}
<p class="empty-state">No posts yet.
{{if .ActiveTag}}Try <a href="/blog">removing the filter</a>.{{end}}
</p>
{{end}}
{{end}}

63
templates/index.html Normal file
View File

@@ -0,0 +1,63 @@
{{define "title"}}Ridgway Systems{{end}}
{{define "content"}}
<section class="hero">
<h1>Ridgway Systems</h1>
<p class="tagline">A homelab built on OpenBSD &mdash; from firewall to git server.</p>
<p class="hero-desc">
A self-hosted infrastructure project running entirely on OpenBSD. This site documents the build:
hardware decisions, network configuration, service deployments, and everything learned along the way.
</p>
<div class="hero-links">
<a href="/blog" class="btn">build log</a>
<a href="/infrastructure" class="btn btn-outline">infrastructure</a>
<a href="/status" class="btn btn-outline">status</a>
</div>
</section>
<section class="infra-summary">
<h2>What's Running</h2>
<div class="infra-grid">
<div class="infra-card">
<div class="infra-host">SuperMicro 1U</div>
<div class="infra-role">Firewall &bull; Router &bull; VPN &bull; Reverse Proxy</div>
<div class="infra-detail">OpenBSD &bull; pf &bull; relayd &bull; WireGuard</div>
</div>
<div class="infra-card">
<div class="infra-host">Dell R720</div>
<div class="infra-role">Primary Server</div>
<div class="infra-detail">Gitea &bull; Web &bull; Mail &bull; Monitoring &bull; Chat</div>
</div>
<div class="infra-card">
<div class="infra-host">Dell R710</div>
<div class="infra-role">Backup &bull; Game Servers</div>
<div class="infra-detail">DNS &bull; Linux VMs &bull; Secondary services</div>
</div>
<div class="infra-card">
<div class="infra-host">Desktop</div>
<div class="infra-role">Daily Driver &bull; Ansible Control</div>
<div class="infra-detail">Development &bull; Playbook management</div>
</div>
</div>
</section>
{{if .RecentPosts}}
<section class="recent-posts">
<h2>Recent Posts</h2>
<ul class="post-list">
{{range .RecentPosts}}
<li class="post-item">
<span class="post-date">{{formatDate .ParsedDate}}</span>
<a href="/blog/{{.Slug}}" class="post-title">{{.Title}}</a>
{{if .Tags}}
<span class="post-tags">
{{range .Tags}}<a href="/blog?tag={{.}}" class="tag">#{{.}}</a> {{end}}
</span>
{{end}}
</li>
{{end}}
</ul>
<a href="/blog" class="all-posts-link">All posts &rarr;</a>
</section>
{{end}}
{{end}}

View File

@@ -0,0 +1,123 @@
{{define "title"}}Infrastructure &mdash; Ridgway Systems{{end}}
{{define "meta-desc"}}Hardware inventory and network diagram for the Ridgway Systems OpenBSD homelab.{{end}}
{{define "content"}}
<div class="page-header">
<h1>Infrastructure</h1>
<p class="page-desc">Physical hardware, network layout, and service placement.</p>
</div>
<section class="infra-section">
<h2>Hardware</h2>
<table class="hw-table">
<thead>
<tr>
<th>Host</th>
<th>Hardware</th>
<th>OS</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td class="hw-name">fw01</td>
<td>SuperMicro 1U<br><span class="hw-spec">E3-1230v2 &bull; 16 GB RAM</span></td>
<td>OpenBSD</td>
<td>Firewall, router, VPN, reverse proxy<br><span class="hw-spec">pf &bull; relayd &bull; WireGuard &bull; unbound</span></td>
</tr>
<tr>
<td class="hw-name">srv01</td>
<td>Dell R720<br><span class="hw-spec">Xeon E5-2600 &bull; 64 GB RAM</span></td>
<td>OpenBSD</td>
<td>Primary server<br><span class="hw-spec">Gitea &bull; httpd &bull; OpenSMTPD &bull; Prometheus &bull; Grafana &bull; Matrix</span></td>
</tr>
<tr>
<td class="hw-name">srv02</td>
<td>Dell R710<br><span class="hw-spec">Xeon 5500/5600 &bull; 48 GB RAM</span></td>
<td>OpenBSD + Linux VMs</td>
<td>Backup, game servers<br><span class="hw-spec">nsd &bull; vmm &bull; Jellyfin &bull; secondary DNS</span></td>
</tr>
<tr>
<td class="hw-name">ws01</td>
<td>Desktop<br><span class="hw-spec">Ryzen &bull; 32 GB RAM</span></td>
<td>Linux</td>
<td>Daily driver, Ansible control node<br><span class="hw-spec">Development &bull; playbook management</span></td>
</tr>
</tbody>
</table>
</section>
<section class="infra-section">
<h2>Network Diagram</h2>
<pre class="network-diagram">
Internet
|
[WAN interface]
|
+=================+
| fw01 | SuperMicro 1U
| OpenBSD | pf firewall
| relayd | WireGuard VPN
+=====+===========+
|
+-- [Management VLAN 1] -- fw01, switches, OOB
|
+-- [Servers VLAN 10] -- srv01, srv02
| |
| +-- srv01 (R720)
| | httpd / relayd (external traffic routed here)
| | Gitea, mail, monitoring, Matrix
| |
| +-- srv02 (R710)
| DNS (nsd), Jellyfin, game VMs
|
+-- [Desktop VLAN 20] -- ws01, personal devices
|
+-- [Game VLAN 30] -- game clients, gaming VMs
|
+-- [IoT/Guest VLAN 40] -- untrusted devices
External traffic flow:
Internet --&gt; fw01 (relayd) --&gt; srv01 (httpd/app)
VPN:
WireGuard on fw01 --&gt; routed to server VLANs
</pre>
</section>
<section class="infra-section">
<h2>Services</h2>
<table class="hw-table">
<thead>
<tr><th>Service</th><th>Host</th><th>URL</th></tr>
</thead>
<tbody>
<tr><td>Web / httpd</td><td>srv01</td><td>ridgwaysystems.org</td></tr>
<tr><td>Gitea</td><td>srv01</td><td>git.ridgwaysystems.org</td></tr>
<tr><td>Email (OpenSMTPD)</td><td>srv01</td><td>&mdash;</td></tr>
<tr><td>DNS (unbound)</td><td>fw01</td><td>internal resolver</td></tr>
<tr><td>DNS (nsd)</td><td>srv02</td><td>authoritative</td></tr>
<tr><td>Prometheus + Grafana</td><td>srv01</td><td>monitoring.ridgwaysystems.org</td></tr>
<tr><td>Matrix</td><td>srv01</td><td>matrix.ridgwaysystems.org</td></tr>
<tr><td>Jellyfin</td><td>srv02</td><td>jellyfin.ridgwaysystems.org</td></tr>
<tr><td>WireGuard VPN</td><td>fw01</td><td>vpn.ridgwaysystems.org</td></tr>
</tbody>
</table>
</section>
<section class="infra-section">
<h2>VLAN Layout</h2>
<table class="hw-table">
<thead>
<tr><th>VLAN</th><th>ID</th><th>Subnet</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td>Management</td><td>1</td><td>10.0.1.0/24</td><td>Switches, OOB, firewall management</td></tr>
<tr><td>Servers</td><td>10</td><td>10.0.10.0/24</td><td>srv01, srv02 — all hosted services</td></tr>
<tr><td>Desktop</td><td>20</td><td>10.0.20.0/24</td><td>ws01 and personal devices</td></tr>
<tr><td>Game</td><td>30</td><td>10.0.30.0/24</td><td>Gaming VMs and clients</td></tr>
<tr><td>IoT/Guest</td><td>40</td><td>10.0.40.0/24</td><td>Untrusted / isolated devices</td></tr>
</tbody>
</table>
</section>
{{end}}

29
templates/post.html Normal file
View File

@@ -0,0 +1,29 @@
{{define "title"}}{{.Title}} &mdash; Ridgway Systems{{end}}
{{define "meta-desc"}}{{.Description}}{{end}}
{{define "og-title"}}{{.Title}} &mdash; Ridgway Systems{{end}}
{{define "og-desc"}}{{.Description}}{{end}}
{{define "og-type"}}article{{end}}
{{define "og-url"}}https://ridgwaysystems.org/blog/{{.Slug}}{{end}}
{{define "tw-title"}}{{.Title}} &mdash; Ridgway Systems{{end}}
{{define "tw-desc"}}{{.Description}}{{end}}
{{define "content"}}
<article class="post">
<header class="post-header">
<h1>{{.Title}}</h1>
<div class="post-meta">
<time datetime="{{.ParsedDate.Format "2006-01-02"}}">{{formatDate .ParsedDate}}</time>
{{if .Tags}}
&mdash;
{{range .Tags}}<a href="/blog?tag={{.}}" class="tag">#{{.}}</a> {{end}}
{{end}}
</div>
</header>
<div class="post-content">
{{.Content}}
</div>
<footer class="post-footer">
<a href="/blog">&larr; Back to build log</a>
</footer>
</article>
{{end}}

36
templates/status.html Normal file
View File

@@ -0,0 +1,36 @@
{{define "title"}}Service Status &mdash; Ridgway Systems{{end}}
{{define "meta-desc"}}Live status of services running on the Ridgway Systems homelab.{{end}}
{{define "content"}}
<div class="page-header">
<h1>Service Status</h1>
{{if .LastChecked}}
<p class="page-desc">Last updated: <time>{{.LastChecked}}</time></p>
{{end}}
</div>
{{if .Page.Services}}
<ul class="status-list">
{{range .Page.Services}}
<li class="status-item status-{{.Status}}">
<span class="status-indicator" aria-label="{{.Status}}"></span>
<div class="status-info">
<span class="status-name">{{.Name}}</span>
{{if .Description}}<span class="status-desc">{{.Description}}</span>{{end}}
{{if .Note}}<span class="status-note">{{.Note}}</span>{{end}}
</div>
<span class="status-badge status-badge-{{.Status}}">{{.Status}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="empty-state">No services configured.</p>
{{end}}
<div class="status-legend">
<span class="status-badge status-badge-up">up</span> operational &nbsp;
<span class="status-badge status-badge-degraded">degraded</span> reduced capacity &nbsp;
<span class="status-badge status-badge-down">down</span> unavailable &nbsp;
<span class="status-badge status-badge-unknown">unknown</span> not checked
</div>
{{end}}