fix(settings): sort What's New history newest-first
Demo images / Build & push demo images (push) Failing after 36s

This commit is contained in:
archipelago
2026-08-31 14:50:55 -04:00
parent e3275353b9
commit 7a39d8fbd1
2 changed files with 111 additions and 59 deletions
@@ -362,42 +362,6 @@ init()
</button> </button>
</div> </div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1"> <div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.107-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.107-alpha</span>
<span class="text-xs text-white/40">July 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Wi-Fi setup now heals itself on older nodes. Some nodes set up before a mid-year fix couldn't connect to a Wi-Fi network from the screen — it failed with a permissions error — because the piece that lets the node manage networking on your behalf was missing. Nodes now put that piece in place automatically on startup, so "scan, pick a network, type the password, connect" works without reinstalling.</p>
<p>Your node rejoins the mesh faster after an update. Applying this update briefly restarts the mesh service, and previously a node could sit disconnected from other nodes for up to five minutes before it retried. It now notices the restart and reconnects within seconds.</p>
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle — two separate faults that had been failing the build.</p>
</div>
</div>
<!-- v1.7.51-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.51-alpha</span>
<span class="text-xs text-white/40">April 30, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Stack installs now adopt containers that already exist instead of failing on them — a repair or reinstall over leftover containers completes, and the adopted container's readiness is waited on like any fresh start.</p>
<p>Failed installs come with evidence: the install path waits for its containers, and when one doesn't become healthy it captures that container's logs, so the error on screen names the real culprit instead of a bare timeout.</p>
<p>Bitcoin RPC bindings are ensured as part of install, and the startup self-heal path gained additional ground for already-deployed nodes.</p>
</div>
</div>
<!-- v1.7.50-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.50-alpha</span>
<span class="text-xs text-white/40">April 30, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>The OTA bridge older nodes needed: deployed binaries only knew how to apply two artifacts (the backend binary and the frontend archive), so the scripts, app specs and docker assets newer releases carry never reached them. This release packs those payloads inside the frontend tarball — the one channel old binaries do apply — and the new backend promotes them into /opt once it starts.</p>
<p>Runtime payloads are staged into timestamped directories and promoted atomically; a failed extraction cleans up its staging area instead of leaving half-written state for the next update to trip over.</p>
<p>This is the release that un-sticks the fleet's update pipeline: from here on, an OTA can carry more than the two artifacts, and app installs on updated nodes use the specs that match their backend.</p>
</div>
</div>
<!-- v1.8.5-alpha --> <!-- v1.8.5-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
@@ -550,7 +514,6 @@ init()
<p>Known gaps, unchanged from the last release: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.</p> <p>Known gaps, unchanged from the last release: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.</p>
</div> </div>
</div> </div>
<!-- v1.7.125-alpha --> <!-- v1.7.125-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
@@ -598,6 +561,7 @@ init()
<p>Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.</p> <p>Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.</p>
</div> </div>
</div> </div>
<!-- v1.7.122-alpha --> <!-- v1.7.122-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
@@ -862,6 +826,18 @@ init()
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle.</p> <p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle.</p>
</div> </div>
</div> </div>
<!-- v1.7.107-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.107-alpha</span>
<span class="text-xs text-white/40">July 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Wi-Fi setup now heals itself on older nodes. Some nodes set up before a mid-year fix couldn't connect to a Wi-Fi network from the screen — it failed with a permissions error — because the piece that lets the node manage networking on your behalf was missing. Nodes now put that piece in place automatically on startup, so "scan, pick a network, type the password, connect" works without reinstalling.</p>
<p>Your node rejoins the mesh faster after an update. Applying this update briefly restarts the mesh service, and previously a node could sit disconnected from other nodes for up to five minutes before it retried. It now notices the restart and reconnects within seconds.</p>
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle — two separate faults that had been failing the build.</p>
</div>
</div>
<!-- v1.7.106-alpha --> <!-- v1.7.106-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
@@ -1581,6 +1557,30 @@ init()
<p>Grafana lifecycle actions repair missing rootless host listeners on port 3000, and Debian 13 install paths pull security updates from trixie-security during image/install creation.</p> <p>Grafana lifecycle actions repair missing rootless host listeners on port 3000, and Debian 13 install paths pull security updates from trixie-security during image/install creation.</p>
</div> </div>
</div> </div>
<!-- v1.7.51-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.51-alpha</span>
<span class="text-xs text-white/40">April 30, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Stack installs now adopt containers that already exist instead of failing on them — a repair or reinstall over leftover containers completes, and the adopted container's readiness is waited on like any fresh start.</p>
<p>Failed installs come with evidence: the install path waits for its containers, and when one doesn't become healthy it captures that container's logs, so the error on screen names the real culprit instead of a bare timeout.</p>
<p>Bitcoin RPC bindings are ensured as part of install, and the startup self-heal path gained additional ground for already-deployed nodes.</p>
</div>
</div>
<!-- v1.7.50-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.50-alpha</span>
<span class="text-xs text-white/40">April 30, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>The OTA bridge older nodes needed: deployed binaries only knew how to apply two artifacts (the backend binary and the frontend archive), so the scripts, app specs and docker assets newer releases carry never reached them. This release packs those payloads inside the frontend tarball — the one channel old binaries do apply — and the new backend promotes them into /opt once it starts.</p>
<p>Runtime payloads are staged into timestamped directories and promoted atomically; a failed extraction cleans up its staging area instead of leaving half-written state for the next update to trip over.</p>
<p>This is the release that un-sticks the fleet's update pipeline: from here on, an OTA can carry more than the two artifacts, and app installs on updated nodes use the specs that match their backend.</p>
</div>
</div>
<!-- v1.7.49-alpha --> <!-- v1.7.49-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
+64 -12
View File
@@ -67,9 +67,53 @@ def undated_versions():
return found return found
def existing_versions(): def ordered_versions():
text = MODAL.read_text() """Return modal versions in display order (top to bottom)."""
return set(re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", text)) return re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", MODAL.read_text())
def version_key(version):
match = re.match(r"v(\d+)\.(\d+)\.(\d+)", version)
return tuple(map(int, match.groups()))
def sort_modal_blocks():
"""Sort complete release-note blocks newest-first, preserving gaps."""
lines = MODAL.read_text().splitlines(keepends=True)
marker = re.compile(r"^\s*<!-- (v\d+\.\d+\.\d+\S*) -->\s*$")
blocks = []
for start, line in enumerate(lines):
match = marker.match(line)
if not match:
continue
depth = 0
opened = False
for index in range(start + 1, len(lines)):
for tag in re.findall(r"</?div\b[^>]*>", lines[index]):
if tag.startswith("</"):
depth -= 1
else:
depth += 1
opened = True
if opened and depth == 0:
blocks.append((start, index + 1, match.group(1), lines[start:index + 1]))
break
else:
raise RuntimeError(f"unclosed What's New block for {match.group(1)}")
sorted_segments = [b[3] for b in sorted(blocks, key=lambda b: version_key(b[2]), reverse=True)]
output = []
cursor = 0
for (start, end, _version, _segment), replacement in zip(blocks, sorted_segments):
output.extend(lines[cursor:start])
output.extend(replacement)
cursor = end
output.extend(lines[cursor:])
changed = output != lines
if changed:
MODAL.write_text("".join(output))
return changed
def to_html(text): def to_html(text):
@@ -113,23 +157,31 @@ def main():
return 1 return 1
entries = parse_changelog() entries = parse_changelog()
have = existing_versions() displayed = ordered_versions()
have = set(displayed)
missing = [e for e in entries if e["ver"] not in have] missing = [e for e in entries if e["ver"] not in have]
expected_order = sorted(displayed, key=version_key, reverse=True)
out_of_order = displayed != expected_order
if not missing: if not missing and not out_of_order:
print("What's New modal is in sync with CHANGELOG.md " print("What's New modal is in sync with CHANGELOG.md "
f"({len(entries)} changelog versions, all present).") f"({len(entries)} changelog versions, all present and newest-first).")
return 0 return 0
names = ", ".join(e["ver"] for e in missing) names = ", ".join(e["ver"] for e in missing)
if check: if check:
if missing:
print("FAIL: these CHANGELOG versions have no block in the Settings " print("FAIL: these CHANGELOG versions have no block in the Settings "
f"What's New modal: {names}", file=sys.stderr) f"What's New modal: {names}", file=sys.stderr)
if out_of_order:
print("FAIL: What's New entries are not newest-first; the modal currently "
f"opens at {displayed[0]} instead of {expected_order[0]}", file=sys.stderr)
print("Run: python3 scripts/sync-whats-new.py", file=sys.stderr) print("Run: python3 scripts/sync-whats-new.py", file=sys.stderr)
return 1 return 1
# Insert missing blocks newest-first, immediately before the newest existing if missing:
# block marker (the first "<!-- v... -->" line in the file). # Insert before the first block; the full sort below makes this safe even
# when a historical hand-written block was accidentally left at the top.
lines = MODAL.read_text().splitlines(keepends=True) lines = MODAL.read_text().splitlines(keepends=True)
marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$") marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$")
idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None) idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None)
@@ -137,12 +189,12 @@ def main():
print("ERROR: could not find an existing version block marker in the modal.", print("ERROR: could not find an existing version block marker in the modal.",
file=sys.stderr) file=sys.stderr)
return 2 return 2
lines.insert(idx, "".join(render_block(e) for e in missing))
# newest-first: sort missing by their order in `entries` (already newest-first)
block_text = "".join(render_block(e) for e in missing)
lines.insert(idx, block_text)
MODAL.write_text("".join(lines)) MODAL.write_text("".join(lines))
print(f"Inserted {len(missing)} block(s): {names}") print(f"Inserted {len(missing)} block(s): {names}")
if sort_modal_blocks():
print("Sorted What's New blocks newest-first.")
return 0 return 0