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

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:

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.