A nightly status page can turn into an application to maintain: a frontend build, an API, packages to update, and services that must stay available just to show yesterday's totals.
For small read-only reports, internal status pages, and AI-generated dashboards that can use a snapshot, start with one self-contained static HTML file. A scheduled job regenerates it; readers open the result without installing packages or running an application server.
Embed data, styles, and necessary scripts in the HTML. Use system fonts; avoid external CDN assets, analytics, and API calls. Prefer HTML tables when they communicate the result clearly.
Copy the file to a static host, attach it, or open it offline. Keep the generator and input schema in source control. Stable ordering and formatting simplify reviews and diffs; dated copies preserve what readers saw. One bounded file also gives an AI an inspectable artifact to update. Review its output as code before sharing it.
For normal websites, shared styles belong in a stylesheet, and separate JavaScript files support caching and debugging. This narrow exception trades those benefits for portability. Larger sites need a suitable rendering strategy and static-site stack.
Northwind needs yesterday's order count and total, refreshed once each night. Everyone receives the same non-sensitive aggregate; nobody edits orders through this page.
❌ Figure: Bad example - The SPA needs a build, API and sign-in service to display a nightly snapshot; offline readers see no totals
✅ Figure: Good example - One HTML file keeps the same nightly totals and their timestamp available offline
The export job still needs an owner, credentials, monitoring, and a schedule.
Save this fictional whole-dollar export as northwind-orders.json. as_of is the source's data cutoff.
{"as_of": "2026-11-06T00:00:00Z","order_date": "2026-11-05","orders": 37,"total_usd": 5550}
Save this as generate.py beside it; run python generate.py with Python 3. It uses the standard library, renders HTML without JavaScript, and replaces the output after writing the complete temporary file.
import base64import hashlibimport jsonfrom html import escapefrom pathlib import Pathdata = json.loads(Path("northwind-orders.json").read_text(encoding="utf-8"))css = "body{font:1.2rem system-ui;max-width:48rem;margin:3rem auto;padding:1rem}"digest = base64.b64encode(hashlib.sha256(css.encode()).digest()).decode()policy = f"default-src 'none'; style-src 'sha256-{digest}'; base-uri 'none'; form-action 'none'"as_of = escape(str(data["as_of"]))order_date = escape(str(data["order_date"]))orders = int(data["orders"])total = int(data["total_usd"])if orders < 0 or total < 0:raise ValueError("Expected non-negative order totals")page = f'''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="{policy}"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Northwind nightly orders</title><style>{css}</style></head><body><main><h1>Northwind nightly orders</h1><p>Example data · Order date: {order_date}</p><p>Data as of: {as_of}</p><table><caption>Daily order totals (USD)</caption><tr><th scope="row">Orders</th><td>{orders:,}</td></tr><tr><th scope="row">Order value</th><td>USD {total:,}</td></tr></table></main></body></html>'''target = Path("northwind-orders.html")temporary = target.with_suffix(".html.tmp")temporary.write_text(page, encoding="utf-8")temporary.replace(target)
Figure: A minimal generator turns the Northwind export into an offline HTML snapshot
Schedule the export then the generator, using the same working directory and one run at a time. Stop on export failure. Validate the source schema, cutoff and output before publishing; retain the previous successful file on failure. Alert the owner on failure or an overdue source cutoff. Scheduled snapshots trade freshness for availability: show the reporting period and a visible Data as of timestamp.
html.escape for text fields. Do not splice untrusted strings into scripts or innerHTML. Review AI-generated HTML for network requests and executable content.unsafe-inline. See CSP guidance and inline style hashes. A hosted response can add headers; a meta policy supports fewer features.For approved public data, GitHub Pages serves static files with optional build processing. Azure Static Web Apps can skip its app build for ready-to-serve files. Hosting may use managed servers or a CDN; the report needs no application server or external CDN assets.
Move to an application or managed service for authentication, per-user authorization, shared state across users, live writes, or data too large to load comfortably. Enforce access and writes on a trusted service; a login screen in HTML does not protect its contents. The frontend can remain static. Local sorting and filtering alone need no backend.
Before handing over, open a copied file offline, check totals and cutoff against the export, and inspect browser network activity for unexpected requests. Readers should see a complete, dated report without starting a service.