Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions web-standalone/changelog.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
[
{
"id": "2026-09-07-fresh-visit",
"date": "2026-09-07",
"title": "Fresh visits start in the right place",
"body": "What's new showed old entries because the browser kept yesterday's file; the page now revalidates on every visit. A fresh visit puts the cursor in the first-hop field, also right after the intro. And a Step 2 label no longer shows before a cluster is locked."
},
{
"id": "2026-09-07-the-link-carries-the-scene",
"date": "2026-09-07",
Expand Down
21 changes: 20 additions & 1 deletion web-standalone/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
crossorigin=""
>
<style>
/* The hidden attribute wins over any display rule. Without this a
label or banner with its own display: block stayed on screen while
carrying hidden (#124); several places worked around it locally. */
[hidden] {
display: none !important;
}
:root {
--bg: #08141b;
--panel: rgba(7, 25, 35, 0.88);
Expand Down Expand Up @@ -5686,6 +5692,15 @@ <h3>Unknown Node Estimate</h3>
if (!introOverlay || introOverlay.hidden) return;
introOverlay.hidden = true;
markIntroSeen();
focusFirstInput();
}

// A fresh visit starts in the first-hop field (#124). preventScroll keeps
// a phone from jumping the page to the field on load; the flow's own
// scrolls (#118) take over from there.
function focusFirstInput() {
if (!prefixInput || prefixInput.value) return;
try { prefixInput.focus({ preventScroll: true }); } catch (error) { prefixInput.focus(); }
}
function introLoadingDone() {
if (!introOverlay || introOverlay.hidden || !introLoading || introLoading.hidden) return;
Expand Down Expand Up @@ -5717,6 +5732,8 @@ <h3>Unknown Node Estimate</h3>
discoverButton.click();
} else if (showIntro) {
openIntro(false);
} else if (!new URLSearchParams(window.location.search).get("incident")) {
focusFirstInput();
}

// "What's new" changelog (#22): loads web-standalone/changelog.json,
Expand All @@ -5728,7 +5745,9 @@ <h3>Unknown Node Estimate</h3>
const SEEN_KEY = "changelog:lastSeenId";
let entries = [];
try {
entries = await fetch("changelog.json").then((r) => r.json());
// Revalidate every time: with the default the browser kept yesterday's
// file for hours and What's new showed old entries (#124).
entries = await fetch("changelog.json", { cache: "no-cache" }).then((r) => r.json());
} catch (error) {
whatsNewButton.hidden = true;
return;
Expand Down
12 changes: 12 additions & 0 deletions web-standalone/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,23 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, directory=BASE_DIR, **kwargs)

def end_headers(self):
# Every response revalidates. Without a Cache-Control header browsers
# apply heuristic caching to index.html and changelog.json and served
# yesterday's page for hours; Cloudflare in front honours this too
# (#124). The proxy routes set their own no-store before this runs.
if not any(name.lower() == "cache-control" for name, _ in self._headers_buffer_names()):
self.send_header("Cache-Control", "no-cache")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
super().end_headers()

def _headers_buffer_names(self):
for line in getattr(self, "_headers_buffer", []):
if isinstance(line, bytes) and b":" in line:
name = line.split(b":", 1)[0].decode("latin-1", "replace").strip()
yield name, None

def do_OPTIONS(self):
self.send_response(204)
self.end_headers()
Expand Down
Loading