Rebuild portfolio as infrastructure control plane
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Keith Casko
2026-06-21 12:42:17 -04:00
parent b8029feb45
commit 76bea35c9c
3 changed files with 1499 additions and 935 deletions
+359 -218
View File
@@ -3,279 +3,420 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ByteGeist is Keith Casko's self-hosted infrastructure lab and engineering case study." /> <meta name="description" content="ByteGeist infrastructure control plane — a live view into Keith Casko's self-hosted cloud environment." />
<title>ByteGeist — Self-hosted infrastructure</title> <title>ByteGeist / Control Plane</title>
<link rel="icon" href="assets/images/bytegeist-logo.png" /> <link rel="icon" href="assets/images/bytegeist-logo.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
<script src="script.js" defer></script>
</head> </head>
<body> <body>
<header class="site-header"> <div class="scanline" aria-hidden="true"></div>
<a class="brand" href="#top" aria-label="ByteGeist home">
<header class="control-bar">
<a class="wordmark" href="#overview" aria-label="ByteGeist control plane">
<img src="assets/images/bytegeist-logo.png" alt="" /> <img src="assets/images/bytegeist-logo.png" alt="" />
<span>ByteGeist</span> <span>BYTEGEIST</span>
<small>/ CONTROL PLANE</small>
</a> </a>
<nav aria-label="Primary navigation"> <div class="control-state">
<a href="#architecture">Architecture</a> <span class="status-dot"></span>
<a href="#systems">Systems</a> <span>INFRASTRUCTURE NOMINAL</span>
<a href="#lessons">Lessons</a> </div>
<nav aria-label="Control plane navigation">
<a href="#overview">Overview</a>
<a href="#topology">Topology</a>
<a href="#observability">Observability</a>
<a href="#services">Services</a>
<a href="#security">Security</a>
</nav> </nav>
<a class="text-link header-link" href="https://wiki.casko.dev">Read the docs <span>↗</span></a> <div class="clock" aria-label="Current local time">
<span id="local-time">--:--:--</span>
<small>AMERICA / NEW YORK</small>
</div>
</header> </header>
<main id="top"> <main>
<section class="hero"> <section id="overview" class="command-grid">
<div class="hero-copy"> <aside class="system-rail" aria-label="System status">
<p class="kicker"><span></span> Infrastructure lab / ongoing project</p> <div class="rail-heading">
<h1>A production-minded cloud, running on Hetzner.</h1> <span>NODE</span>
<p class="hero-intro"> <strong>BG-CORE-01</strong>
ByteGeist is where I test infrastructure decisions for real: identity, observability, </div>
deployments, security, and recovery. It currently runs 26 Docker containers on Ubuntu at scale.
<dl class="rail-data">
<div><dt>Provider</dt><dd>Hetzner</dd></div>
<div><dt>OS</dt><dd>Ubuntu 24.04</dd></div>
<div><dt>Runtime</dt><dd>Docker</dd></div>
<div><dt>Region</dt><dd>US-East</dd></div>
<div><dt>Ingress</dt><dd>NPM / TLS</dd></div>
<div><dt>Identity</dt><dd>Authentik</dd></div>
</dl>
<div class="rail-health">
<span>HOST HEALTH</span>
<div class="health-bar"><i style="--value: 92%"></i></div>
<strong>92%</strong>
</div>
<a href="https://wiki.casko.dev">OPEN RUNBOOKS <span>↗</span></a>
<a href="https://git.casko.dev">GIT SERVER <span>↗</span></a>
</aside>
<div class="overview-console">
<div class="console-label">
<span>OVERVIEW / LIVE</span>
<span id="refresh-indicator">UPDATED 00s AGO</span>
</div>
<div class="identity-block">
<div>
<p class="overline">SELF-HOSTED INFRASTRUCTURE ENVIRONMENT</p>
<h1>Personal cloud.<br />Operational by design.</h1>
</div>
<p class="operator-note">
A working infrastructure lab for identity, observability, delivery, security,
and recovery. Built and maintained as an evolving system—not a static demo.
</p> </p>
</div>
<div class="actions"> <div class="metric-board" aria-label="Infrastructure metrics">
<a href="#architecture" class="button primary">See how it works</a> <article class="metric metric-primary">
<a href="https://git.casko.dev" class="text-link">Browse the Git server <span>↗</span></a> <span>SERVICES ONLINE</span>
<strong data-count="20">20</strong><small>/ 20</small>
<div class="spark-bars" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
</article>
<article class="metric">
<span>UPTIME</span>
<strong>99.98</strong><small>%</small>
<p>30 DAY WINDOW</p>
</article>
<article class="metric">
<span>CONTAINERS</span>
<strong data-count="26">26</strong>
<p>26 RUNNING / 0 DEGRADED</p>
</article>
<article class="metric">
<span>ACTIVE DOMAINS</span>
<strong data-count="11">11</strong>
<p>TLS VALID / PROXIED</p>
</article>
<article class="metric">
<span>CPU LOAD</span>
<strong>16.2</strong><small>%</small>
<div class="meter"><i style="--value: 16.2%"></i></div>
</article>
<article class="metric">
<span>MEMORY</span>
<strong>61.6</strong><small>%</small>
<div class="meter warning"><i style="--value: 61.6%"></i></div>
</article>
</div>
<div class="operations-window">
<div class="window-bar">
<span><i class="live-dot"></i> GRAFANA / BYTEGEIST OPERATIONS</span>
<a href="assets/images/portfolio-grafana-operations.png" target="_blank">FULL RES ↗</a>
</div>
<img src="assets/images/portfolio-grafana-operations.png" alt="Grafana operations dashboard showing ByteGeist infrastructure health" />
</div> </div>
</div> </div>
<figure class="hero-visual"> <aside class="activity-panel">
<img src="assets/images/portfolio-grafana-operations.png" alt="ByteGeist operations dashboard in Grafana" /> <div class="panel-title">
<span>EVENT STREAM</span>
<small>LIVE</small>
</div>
<ol class="event-list">
<li>
<time>22:14:08</time>
<span class="event-ok">DEPLOY</span>
<p>portfolio/main completed</p>
</li>
<li>
<time>21:48:32</time>
<span class="event-info">AUTH</span>
<p>OIDC session issued</p>
</li>
<li>
<time>20:03:17</time>
<span class="event-ok">BACKUP</span>
<p>volume snapshot verified</p>
</li>
<li>
<time>18:42:55</time>
<span class="event-warn">SEC</span>
<p>CrowdSec decision added</p>
</li>
<li>
<time>16:11:09</time>
<span class="event-info">DNS</span>
<p>certificate renewed</p>
</li>
</ol>
<div class="deployment-status">
<span>LAST DEPLOYMENT</span>
<strong>SUCCESS</strong>
<p>Woodpecker pipeline #184</p>
<div><i></i><i></i><i></i><i></i></div>
</div>
<div class="terminal-mini" aria-label="Recent terminal output">
<p><span>$</span> docker ps --format status</p>
<p class="terminal-output">26 containers running</p>
<p><span>$</span> systemctl is-system-running</p>
<p class="terminal-output">running<span class="cursor">_</span></p>
</div>
</aside>
</section>
<section id="topology" class="module topology-module">
<header class="module-header">
<div>
<span class="module-id">SYS.01</span>
<h2>Infrastructure topology</h2>
</div>
<p>Traffic, identity, workloads, telemetry, and recovery paths on the current host.</p>
<div class="module-state"><i></i> DOCUMENTED</div>
</header>
<div class="topology-layout">
<div class="stack-index">
<div>
<span>EDGE</span>
<strong>Nginx Proxy Manager</strong>
<small>TLS termination / routing</small>
</div>
<div>
<span>IDENTITY</span>
<strong>Authentik</strong>
<small>OIDC / SSO / 2FA</small>
</div>
<div>
<span>COMPUTE</span>
<strong>Docker Compose</strong>
<small>26 active containers</small>
</div>
<div>
<span>TELEMETRY</span>
<strong>Prometheus + Loki</strong>
<small>Metrics / logs / alerts</small>
</div>
</div>
<figure class="system-map">
<a href="assets/images/bytegeist-architecture.png" target="_blank">
<img src="assets/images/bytegeist-architecture.png" alt="ByteGeist infrastructure architecture diagram" />
</a>
<figcaption> <figcaption>
<span>Live operations view</span> <span>BYTEGEIST-CLOUD / REV 04</span>
Grafana / Prometheus / cAdvisor <span>SELECT DIAGRAM TO INSPECT</span>
</figcaption> </figcaption>
</figure> </figure>
</section>
<section class="project-facts" aria-label="Project facts">
<div><span>01</span><strong>26 containers</strong><small>deployed with Compose</small></div>
<div><span>02</span><strong>Central identity</strong><small>SSO and 2FA via Authentik</small></div>
<div><span>03</span><strong>Full observability</strong><small>metrics, logs, and uptime</small></div>
<div><span>04</span><strong>Documented recovery</strong><small>tested restore procedures</small></div>
</section>
<div class="logo-divider" aria-hidden="true">
<img src="assets/images/bytegeist-logo.png" alt="" />
</div> </div>
</section>
<section id="architecture" class="architecture section-rule"> <section id="observability" class="module evidence-module">
<div class="section-heading"> <header class="module-header">
<p class="section-index">01 / Architecture</p>
<div> <div>
<h2>One server, treated like a platform.</h2> <span class="module-id">OBS.02</span>
<p> <h2>Monitoring stack</h2>
Public traffic terminates at Nginx Proxy Manager. Authentik handles identity before
requests reach protected services. Metrics, logs, CI, and backups remain separate
concerns, so each can fail or change without taking the whole stack with it.
</p>
</div>
</div> </div>
<p>Host metrics, container telemetry, centralized logs, and independent uptime checks.</p>
<div class="module-state"><i></i> COVERAGE ACTIVE</div>
</header>
<figure class="architecture-figure"> <div class="evidence-grid">
<img src="assets/images/bytegeist-architecture.png" alt="Detailed ByteGeist infrastructure architecture diagram" /> <figure class="evidence evidence-wide">
<figcaption>Current architecture — updated as services are added or retired.</figcaption> <figcaption>
</figure> <span>LOG PIPELINE</span>
<strong>Loki / Grafana Alloy</strong>
<div class="principles"> <small>Container output searchable by service and host</small>
<article> </figcaption>
<span>Access</span>
<h3>Authenticate once</h3>
<p>OIDC where supported; forward auth for services that do not speak it natively.</p>
</article>
<article>
<span>Operations</span>
<h3>Make failures visible</h3>
<p>Host and container metrics, centralized logs, and independent uptime checks.</p>
</article>
<article>
<span>Recovery</span>
<h3>Assume the host is replaceable</h3>
<p>Configuration lives in Git. Persistent data is backed up with rebuild notes.</p>
</article>
</div>
</section>
<section id="systems" class="systems section-rule">
<div class="section-heading compact">
<p class="section-index">02 / Selected systems</p>
<div>
<h2>The parts I spend time on.</h2>
<p>Not a catalog of logos—a record of what is running, why it exists, and what I learned building it.</p>
</div>
</div>
<article class="system-story">
<div class="story-copy">
<p class="story-number">01</p>
<p class="story-label">Observability</p>
<h3>Useful signals before pretty dashboards.</h3>
<p>
Prometheus collects host and container metrics while Loki receives logs through
Grafana Alloy. The dashboards answer practical questions: what is down, what changed,
and which container is consuming the host.
</p>
<ul>
<li>Grafana</li><li>Prometheus</li><li>Loki</li><li>Alloy</li>
</ul>
</div>
<div class="story-images">
<figure class="screenshot-exhibit">
<figcaption><span>Infrastructure monitoring</span><small>Grafana operations dashboard</small></figcaption>
<a href="assets/images/portfolio-grafana-operations.png" target="_blank">
<img src="assets/images/portfolio-grafana-operations.png" alt="Grafana infrastructure metrics dashboard" />
</a>
</figure>
<figure class="screenshot-exhibit">
<figcaption><span>Centralized logging</span><small>Container logs collected in Loki</small></figcaption>
<a href="assets/images/portfolio-grafana-logs.png" target="_blank"> <a href="assets/images/portfolio-grafana-logs.png" target="_blank">
<img src="assets/images/portfolio-grafana-logs.png" alt="Centralized container logs in Grafana" /> <img src="assets/images/portfolio-grafana-logs.png" alt="Centralized logs in Grafana" />
</a> </a>
</figure> </figure>
</div>
</article>
<article class="system-story reverse"> <div class="coverage-matrix">
<div class="story-copy"> <div class="panel-title"><span>MONITORING COVERAGE</span><small>6 / 6</small></div>
<p class="story-number">02</p>
<p class="story-label">Identity & security</p>
<h3>One identity layer, fewer exposed edges.</h3>
<p>
Authentik provides SSO, OIDC, forward auth, and 2FA across eleven applications.
CrowdSec watches hostile traffic and creates firewall decisions alongside key-only SSH,
UFW, and TLS.
</p>
<ul> <ul>
<li>Authentik</li><li>CrowdSec</li><li>UFW</li><li>Let's Encrypt</li> <li><span>Host metrics</span><i>ACTIVE</i></li>
<li><span>Container metrics</span><i>ACTIVE</i></li>
<li><span>Application logs</span><i>ACTIVE</i></li>
<li><span>Endpoint uptime</span><i>ACTIVE</i></li>
<li><span>Resource alerts</span><i>ACTIVE</i></li>
<li><span>Security events</span><i>ACTIVE</i></li>
</ul> </ul>
<div class="stack-list">
<span>Grafana</span><span>Prometheus</span><span>Loki</span>
<span>Alloy</span><span>Node Exporter</span><span>cAdvisor</span>
</div> </div>
<div class="story-images">
<figure class="screenshot-exhibit">
<figcaption><span>Identity portal</span><small>Applications protected by Authentik</small></figcaption>
<a href="assets/images/portfolio-authentik-applications.png" target="_blank">
<img src="assets/images/portfolio-authentik-applications.png" alt="Applications protected by Authentik" />
</a>
</figure>
<figure class="screenshot-exhibit">
<figcaption><span>Active firewall decisions</span><small>CrowdSec command-line output</small></figcaption>
<a href="assets/images/portfolio-crowdsec-decisions.png" target="_blank">
<img class="crowdsec-capture" src="assets/images/portfolio-crowdsec-decisions.png" alt="CrowdSec security decisions" />
</a>
</figure>
</div> </div>
</article> </div>
</section>
<article class="system-story"> <section id="services" class="module services-module">
<div class="story-copy"> <header class="module-header">
<p class="story-number">03</p> <div>
<p class="story-label">Development</p> <span class="module-id">APP.03</span>
<h3>Code and delivery stay in-house.</h3> <h2>Service inventory</h2>
<p>
Gitea stores infrastructure code and documentation. Woodpecker runs repository-linked
pipelines, including the build and deployment path for this site.
</p>
<ul>
<li>Gitea</li><li>Woodpecker CI</li><li>Docker</li><li>Git</li>
</ul>
</div> </div>
<div class="story-images"> <p>Core applications currently exposed through the internal service portal.</p>
<figure class="screenshot-exhibit"> <div class="module-state"><i></i> 20 ONLINE</div>
<figcaption><span>Source control</span><small>Self-hosted repositories in Gitea</small></figcaption> </header>
<div class="service-layout">
<div class="service-table" role="table" aria-label="Service inventory">
<div class="service-row service-head" role="row">
<span>Service</span><span>Role</span><span>Access</span><span>State</span>
</div>
<div class="service-row"><strong>Gitea</strong><span>Source control</span><span>SSO</span><i>ONLINE</i></div>
<div class="service-row"><strong>Wiki.js</strong><span>Documentation</span><span>SSO</span><i>ONLINE</i></div>
<div class="service-row"><strong>Dashy</strong><span>Service portal</span><span>SSO</span><i>ONLINE</i></div>
<div class="service-row"><strong>Vaultwarden</strong><span>Secrets</span><span>DIRECT</span><i>ONLINE</i></div>
<div class="service-row"><strong>Stirling PDF</strong><span>Document tools</span><span>SSO</span><i>ONLINE</i></div>
<div class="service-row"><strong>IT Tools</strong><span>Utilities</span><span>SSO</span><i>ONLINE</i></div>
<div class="service-row"><strong>Uptime Kuma</strong><span>Availability</span><span>SSO</span><i>ONLINE</i></div>
</div>
<figure class="portal-shot">
<figcaption>
<span>SERVICE PORTAL</span>
<small>Dashy / internal access layer</small>
</figcaption>
<a href="assets/images/portfolio-cloud-dashboard-overview.png" target="_blank">
<img src="assets/images/portfolio-cloud-dashboard-overview.png" alt="ByteGeist Dashy service portal" />
</a>
</figure>
</div>
</section>
<section class="module devops-module">
<header class="module-header">
<div>
<span class="module-id">DEV.04</span>
<h2>Delivery pipeline</h2>
</div>
<p>Git-based workflows from repository change to automated deployment.</p>
<div class="module-state"><i></i> LAST RUN PASSED</div>
</header>
<div class="pipeline">
<div class="pipeline-flow" aria-label="Deployment pipeline">
<div><span>01</span><strong>COMMIT</strong><small>Gitea repository</small></div>
<b>→</b>
<div><span>02</span><strong>BUILD</strong><small>Woodpecker runner</small></div>
<b>→</b>
<div><span>03</span><strong>VERIFY</strong><small>Static checks</small></div>
<b>→</b>
<div><span>04</span><strong>DEPLOY</strong><small>Docker host</small></div>
</div>
<div class="pipeline-shots">
<figure>
<figcaption>GITEA / REPOSITORIES</figcaption>
<a href="assets/images/portfolio-gitea-dashboard.png" target="_blank"> <a href="assets/images/portfolio-gitea-dashboard.png" target="_blank">
<img src="assets/images/portfolio-gitea-dashboard.png" alt="Self-hosted Gitea dashboard" /> <img src="assets/images/portfolio-gitea-dashboard.png" alt="Gitea repositories dashboard" />
</a> </a>
</figure> </figure>
<figure class="screenshot-exhibit"> <figure>
<figcaption><span>Continuous delivery</span><small>Successful Woodpecker pipeline</small></figcaption> <figcaption>WOODPECKER / PIPELINE</figcaption>
<a href="assets/images/portfolio-woodpecker-pipeline.png" target="_blank"> <a href="assets/images/portfolio-woodpecker-pipeline.png" target="_blank">
<img src="assets/images/portfolio-woodpecker-pipeline.png" alt="Successful Woodpecker CI pipeline" /> <img src="assets/images/portfolio-woodpecker-pipeline.png" alt="Successful Woodpecker CI pipeline" />
</a> </a>
</figure> </figure>
</div> </div>
</article>
<article class="system-story reverse">
<div class="story-copy">
<p class="story-number">04</p>
<p class="story-label">Platform & recovery</p>
<h3>The boring work is part of the system.</h3>
<p>
Dashy keeps service access legible. The wiki records inventory, DNS, restore order,
backup validation, and the steps required to rebuild the environment from a clean host.
</p>
<ul>
<li>Dashy</li><li>Wiki.js</li><li>Restic</li><li>Runbooks</li>
</ul>
</div> </div>
<div class="story-images"> </section>
<figure class="screenshot-exhibit">
<figcaption><span>Service portal</span><small>Internal tools organized in Dashy</small></figcaption> <section id="security" class="module security-module">
<a href="assets/images/portfolio-cloud-dashboard-overview.png" target="_blank"> <header class="module-header">
<img src="assets/images/portfolio-cloud-dashboard-overview.png" alt="ByteGeist internal service portal" /> <div>
<span class="module-id">SEC.05</span>
<h2>Identity & perimeter</h2>
</div>
<p>Central authentication, protected services, and automated hostile-traffic decisions.</p>
<div class="module-state"><i></i> ENFORCING</div>
</header>
<div class="security-layout">
<figure class="auth-shot">
<figcaption>
<span>AUTHENTIK</span>
<small>11 protected applications / 2FA enforced</small>
</figcaption>
<a href="assets/images/portfolio-authentik-applications.png" target="_blank">
<img src="assets/images/portfolio-authentik-applications.png" alt="Applications protected by Authentik" />
</a> </a>
</figure> </figure>
<figure class="screenshot-exhibit">
<figcaption><span>Recovery runbook</span><small>Documented rebuild and restore process</small></figcaption> <div class="security-data">
<div class="security-score">
<span>ACCESS POSTURE</span>
<strong>HARDENED</strong>
<small>KEY-ONLY SSH / SSO / TLS</small>
</div>
<ul>
<li><span>OIDC providers</span><strong>04</strong></li>
<li><span>Forward-auth apps</span><strong>07</strong></li>
<li><span>2FA policy</span><strong>ENFORCED</strong></li>
<li><span>Public SSH password</span><strong>DISABLED</strong></li>
</ul>
</div>
<figure class="crowdsec-shot">
<figcaption>
<span>CROWDSEC / ACTIVE DECISIONS</span>
<small>Automated hostile source blocking</small>
</figcaption>
<a href="assets/images/portfolio-crowdsec-decisions.png" target="_blank">
<img src="assets/images/portfolio-crowdsec-decisions.png" alt="CrowdSec active firewall decisions" />
</a>
</figure>
</div>
</section>
<section class="module recovery-module">
<header class="module-header">
<div>
<span class="module-id">DR.06</span>
<h2>Recovery state</h2>
</div>
<p>Configuration in Git, persistent data backups, and an explicit rebuild sequence.</p>
<div class="module-state"><i></i> RUNBOOK CURRENT</div>
</header>
<div class="recovery-layout">
<div class="recovery-sequence">
<div><span>01</span><strong>Provision host</strong><small>Ubuntu / firewall / SSH</small></div>
<div><span>02</span><strong>Restore config</strong><small>Git / Compose / secrets</small></div>
<div><span>03</span><strong>Restore data</strong><small>Volumes / databases</small></div>
<div><span>04</span><strong>Validate edge</strong><small>DNS / TLS / identity</small></div>
</div>
<figure>
<a href="assets/images/portfolio-wiki-disaster-recovery.png" target="_blank"> <a href="assets/images/portfolio-wiki-disaster-recovery.png" target="_blank">
<img src="assets/images/portfolio-wiki-disaster-recovery.png" alt="ByteGeist disaster recovery runbook" /> <img src="assets/images/portfolio-wiki-disaster-recovery.png" alt="ByteGeist disaster recovery runbook" />
</a> </a>
</figure> </figure>
</div> </div>
</article>
</section>
<section id="lessons" class="lessons section-rule">
<div class="section-heading compact">
<p class="section-index">03 / Lessons</p>
<div>
<h2>What actually took work.</h2>
</div>
</div>
<div class="lesson-list">
<article>
<span>01</span>
<h3>OIDC is mostly careful bookkeeping.</h3>
<p>Redirect URIs, claims, group mapping, and user provisioning all need to agree across both ends.</p>
</article>
<article>
<span>02</span>
<h3>Backups are unfinished until restore is documented.</h3>
<p>I track data location, dependencies, restore order, DNS, certificates, and validation—not just archive jobs.</p>
</article>
<article>
<span>03</span>
<h3>Observability has to support a decision.</h3>
<p>A smaller dashboard that explains an incident is more useful than a wall of metrics nobody checks.</p>
</article>
<article>
<span>04</span>
<h3>Convenience can weaken the boundary.</h3>
<p>Every exposed service has a cost. SSO, least privilege, and fewer public endpoints keep that cost explicit.</p>
</article>
</div>
</section>
<section class="closing">
<p class="section-index">Project notes</p>
<h2>This environment is still changing.</h2>
<p>
I use ByteGeist to practice the parts of infrastructure work that tutorials tend to skip:
upgrades, failure modes, access policy, noisy logs, and recovery.
</p>
<a class="button primary" href="https://wiki.casko.dev">Open the documentation <span>↗</span></a>
</section> </section>
</main> </main>
<footer> <footer>
<a class="brand" href="#top"> <div class="footer-mark">
<img src="assets/images/bytegeist-logo.png" alt="" /> <img src="assets/images/bytegeist-logo.png" alt="" />
<span>ByteGeist</span> <div><strong>BYTEGEIST</strong><span>KEITH CASKO / INFRASTRUCTURE LAB</span></div>
</a> </div>
<p>An infrastructure lab by Keith Casko, running on Hetzner.</p> <p>PLAIN HTML / CSS / JAVASCRIPT</p>
<p>Built with plain HTML and CSS.</p> <p>STATUS: <span class="status-text">OPERATIONAL</span></p>
</footer> </footer>
</body> </body>
<!-- Pipeline Success Test -->
</html> </html>
+42
View File
@@ -0,0 +1,42 @@
const timeElement = document.querySelector("#local-time");
const refreshElement = document.querySelector("#refresh-indicator");
const counters = document.querySelectorAll("[data-count]");
function updateClock() {
const now = new Date();
timeElement.textContent = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(now);
}
let secondsSinceRefresh = 0;
function updateRefreshState() {
refreshElement.textContent = `UPDATED ${String(secondsSinceRefresh).padStart(2, "0")}s AGO`;
secondsSinceRefresh = (secondsSinceRefresh + 1) % 60;
}
function animateCounters() {
counters.forEach((counter) => {
const target = Number(counter.dataset.count);
let current = 0;
const increment = Math.max(1, Math.ceil(target / 18));
const timer = window.setInterval(() => {
current = Math.min(target, current + increment);
counter.textContent = current;
if (current === target) window.clearInterval(timer);
}, 45);
});
}
updateClock();
updateRefreshState();
animateCounters();
window.setInterval(updateClock, 1000);
window.setInterval(updateRefreshState, 1000);
+1103 -722
View File
File diff suppressed because it is too large Load Diff