Back to blog
FILE 0x9D·A COUNTER CLOCK AS THE SIMPLEST THING THAT COULD POSSIBLY WO

A counter clock as the simplest thing that could possibly work

Back to blog
FILE 0x9D·A COUNTER CLOCK AS THE SIMPLEST THING THAT COULD POSSIBLY WO
Back to blog
FILE 0x9D·A COUNTER CLOCK AS THE SIMPLEST THING THAT COULD POSSIBLY WO
April 27, 2026 · homelab, nginx, static-site

I needed a public webpage that counts time since a fixed instant. Two days, four hours, big number, mobile-friendly. The interesting part wasn't the page — it was how much infrastructure it didn't need.

What was happening

I keep falling into the trap of reaching for a framework whenever I want a "real" page. React for one number. A static site generator for one HTML file. A backend to serve a string that never changes.

The page I needed has exactly one moving part: now() - start. Everything else is layout. So I wrote it as a single HTML file with an inline script and put it behind nginx.

What I found

The architecture is:

  • /var/www/<host>/index.html on an LXC, owned www-data
  • One nginx server block with a Let's Encrypt cert (certbot, auto-renew)
  • Route 53 CNAME to the public-facing reverse proxy
  • DNS-01 wildcard cert covers it for free

The HTML itself is one file, no build step, no dependencies:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Counter</title>
  <style>/* dark glass UI, ~80 lines of CSS */</style>
</head>
<body>
  <main>
    <div class="days" id="days">—</div>
    <div class="breakdown" id="breakdown"></div>
    <div class="ticker" id="ticker"></div>
  </main>
  <script>
    const START_UTC = Date.UTC(2025, 11, 14, 6, 31, 0); // months are 0-indexed
    function tick() {
      const now = Date.now();
      const ms = now - START_UTC;
      const totalDays = Math.floor(ms / 86_400_000);
      document.getElementById('days').textContent = totalDays;
      // calendar-aware month walk for the breakdown:
      let y = 2025, m = 11, d = 14;
      let cur = new Date(Date.UTC(y, m, d, 6, 31, 0));
      let months = 0;
      while (true) {
        const next = new Date(cur); next.setUTCMonth(next.getUTCMonth() + 1);
        if (next.getTime() > now) break;
        months += 1; cur = next;
      }
      const remMs = now - cur.getTime();
      const remDays  = Math.floor(remMs / 86_400_000);
      const remHrs   = Math.floor((remMs % 86_400_000) / 3_600_000);
      const remMins  = Math.floor((remMs % 3_600_000) / 60_000);
      const remSecs  = Math.floor((remMs % 60_000) / 1000);
      document.getElementById('breakdown').textContent =
        `${months}mo ${remDays}d`;
      document.getElementById('ticker').textContent =
        `${String(remHrs).padStart(2,'0')}:${String(remMins).padStart(2,'0')}:${String(remSecs).padStart(2,'0')}`;
      requestAnimationFrame(tick);
    }
    tick();
  </script>
</body>
</html>

That's the whole site. No build, no bundle, no API. View-source renders the same as the running app. A second machine on the LAN consumes a JSON variant via a tiny http.server Python service on port 8090, which nginx proxies under /api/:

# sober-api.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime, timezone
import json

START_UTC = datetime(2025, 12, 14, 6, 31, 0, tzinfo=timezone.utc)

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        now = datetime.now(timezone.utc)
        delta = now - START_UTC
        body = {
            "start": START_UTC.isoformat(),
            "now": now.isoformat(),
            "total": {
                "days": delta.days,
                "seconds": int(delta.total_seconds()),
            },
        }
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(json.dumps(body).encode())

HTTPServer(("127.0.0.1", 8090), H).serve_forever()

systemd unit, 14 lines. Restart on failure. Done.

What I'd do differently

I would not. The temptation when I look at this is to "modernize" it — add a framework, add a CI pipeline, add a CDN. None of that would make the page better. The page works on first paint, weighs less than a single React icon-font request, and survives any nginx upgrade I throw at it.

The lesson I keep relearning: when you have exactly one moving part, you also have exactly one thing that can break. Resist the urge to surround it with things that can also break.