A counter clock as the simplest thing that could possibly work
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.htmlon an LXC, ownedwww-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.