Public Access
Site: the posts link to each other wherever they refer to one; check_site.py checks every link of the site
Site / build (pull_request) Successful in 8s
Site / build (pull_request) Successful in 8s
"The last post" and "the first post" in the LoRa, Gemini and S1 posts are now links. check_site.py follows every link to another page of the site and the #fragment it names, so a broken one fails the Site job. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -127,7 +127,7 @@ The start page lists them by capsule, newest first. Inside a Saved Page, a link
|
||||
|
||||
## Where it hurt
|
||||
|
||||
- **The bug from the first post, again.** A status message ("Not Gemini: https://…") was cleared before it was ever drawn. It was timestamped with `millis()` after the main loop had read the clock, and in unsigned arithmetic, three milliseconds in the future is 49 days ago. That's exactly the bug that made toasts expire before they appeared in [the first post](/devlog/roro9stack/). Same fix: a signed comparison. Same feeling.
|
||||
- **The bug from [the first post](/devlog/roro9stack/), again.** A status message ("Not Gemini: https://…") was cleared before it was ever drawn. It was timestamped with `millis()` after the main loop had read the clock, and in unsigned arithmetic, three milliseconds in the future is 49 days ago. That's exactly the bug that made toasts expire before they appeared in [the first post](/devlog/roro9stack/). Same fix: a signed comparison. Same feeling.
|
||||
- **A space where none was.** Wrapped link and list lines are indented, which meant re-wrapping their continuation rows. I rebuilt that text by joining the rows with spaces, so a long URL the first wrap had cut mid-word came out as `faq.gm i`. Now it re-wraps the exact rest of the line.
|
||||
- **436 bytes.** The refresh job needed to know where a Saved Page came from, so it opened the page, all of it, next to the App's copy of the same page and a fresh TLS connection. The heap's lowest point since boot: 436 bytes. Nothing crashed, which is less reassuring than it sounds. Now it reads one line, and the 55 KB start floor guards every fetch, not just the ones the App asks for. Lowest afterwards: 53.8 KB.
|
||||
- **The same trap, twice in one milestone.** A forward declaration of `NetworkClientSecure` inside the project's namespace declares a different class that doesn't exist. I'd made that mistake in the Debug Console two milestones ago. I made it again, in the same way, and the compiler explained it again, at the same length.
|
||||
|
||||
@@ -53,7 +53,7 @@ text = "Broadband, flat across the band, with a comb of steady spikes on top. It
|
||||
|
||||
## Why it only listens
|
||||
|
||||
The plan from the first post put the mesh last: receiving in M4, transmitting in M5, and before that, a second Meshtastic device to test against. M3 is the radio coming up: drivers, the shared bus, a scanner. It needs nothing to talk to, which is lucky, because I have nothing to talk to.
|
||||
The plan from [the first post](/devlog/roro9stack/) put the mesh last: receiving in M4, transmitting in M5, and before that, a second Meshtastic device to test against. M3 is the radio coming up: drivers, the shared bus, a scanner. It needs nothing to talk to, which is lucky, because I have nothing to talk to.
|
||||
|
||||
I checked anyway. meshmap.net publishes the nodes that report to the internet. I downloaded the whole list and filtered it on my own machine, so my position stayed home. Within 10 km of my desk there are two nodes; within 30 km, six, all seen that day. The map blurs positions by a few kilometres, and it only shows nodes that report online, so there are probably more. Whether any of them reaches a small antenna in a city is another question. Spoiler: no.
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ So the backlog got sorted into milestones, and the first one is the boring kind:
|
||||
|
||||
## The card that never said no
|
||||
|
||||
The last post left it here: about once in five uploads, a write to the card failed, the retry code papered over the hole with zeros, and I'd fixed the papering but not the failing. The card "refuses a write now and then", I wrote, like it was weather.
|
||||
[The last post](/devlog/roro9stack-lora/) left it here: about once in five uploads, a write to the card failed, the retry code papered over the hole with zeros, and I'd fixed the papering but not the failing. The card "refuses a write now and then", I wrote, like it was weather.
|
||||
|
||||
### Four wrong guesses
|
||||
|
||||
**The radio.** It shares the card's SPI bus, so it was the obvious suspect. Cleared in the last post: the failures came with the radio asleep too.
|
||||
**The radio.** It shares the card's SPI bus, so it was the obvious suspect. Cleared in [the last post](/devlog/roro9stack-lora/): the failures came with the radio asleep too.
|
||||
|
||||
**A timeout.** The driver waits at most 500 ms for a busy card and then gives up, silently. Cards do pause for housekeeping. So I timed every write. Good ones took up to 107 ms. The failing one came back after **4 ms**. Nothing waits 500 ms in 4 ms.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ Usage: site/tools/check_site.py <built site folder>
|
||||
from elsewhere (links a visitor follows are fine).
|
||||
- Every page has a title, a description and a language, and every image has an alt attribute.
|
||||
- Every file a page references on this site exists.
|
||||
- Every link to another page of the site lands on a page that exists, and so does the #fragment it names.
|
||||
"""
|
||||
import html.parser
|
||||
import os
|
||||
@@ -29,9 +30,16 @@ class Page(html.parser.HTMLParser):
|
||||
self.lang = None
|
||||
self.loads = [] # (tag, url) of everything the page fetches by itself
|
||||
self.images_without_alt = 0
|
||||
self.links = [] # href of every <a>
|
||||
self.ids = set() # every id and name: what a #fragment can name
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
a = dict(attrs)
|
||||
for key in ("id", "name"):
|
||||
if a.get(key):
|
||||
self.ids.add(a[key])
|
||||
if tag == "a" and a.get("href"):
|
||||
self.links.append(a["href"])
|
||||
if tag == "html":
|
||||
self.lang = a.get("lang")
|
||||
if tag == "title":
|
||||
@@ -56,6 +64,7 @@ class Page(html.parser.HTMLParser):
|
||||
|
||||
problems = []
|
||||
pages = 0
|
||||
parsed = {} # path of a page, from the site's root -> its Page
|
||||
for folder, _, files in os.walk(ROOT):
|
||||
for name in files:
|
||||
if not name.endswith(".html"):
|
||||
@@ -65,6 +74,7 @@ for folder, _, files in os.walk(ROOT):
|
||||
p = Page()
|
||||
p.feed(open(path, encoding="utf-8").read())
|
||||
pages += 1
|
||||
parsed["/" + shown.replace(os.sep, "/")] = (p, folder)
|
||||
if not p.has_title: problems.append(f"{shown}: no title")
|
||||
if not p.has_description: problems.append(f"{shown}: no description")
|
||||
if not p.lang: problems.append(f"{shown}: no language")
|
||||
@@ -82,6 +92,31 @@ for folder, _, files in os.walk(ROOT):
|
||||
if not os.path.exists(target):
|
||||
problems.append(f"{shown}: {tag} {url} does not exist")
|
||||
|
||||
def page_of(url_path):
|
||||
"""The built page a path of the site names: /guide/ is /guide/index.html."""
|
||||
if url_path.endswith("/"):
|
||||
return url_path + "index.html"
|
||||
if os.path.isdir(os.path.join(ROOT, url_path.lstrip("/"))):
|
||||
return url_path + "/index.html"
|
||||
return url_path
|
||||
|
||||
|
||||
for shown_path, (p, folder) in parsed.items():
|
||||
for href in p.links:
|
||||
u = urllib.parse.urlparse(href)
|
||||
if u.scheme in ("mailto", "tel") or (u.netloc and u.netloc != HOST):
|
||||
continue
|
||||
if not u.path: # "#fragment": this page
|
||||
target = shown_path
|
||||
elif u.path.startswith("/"):
|
||||
target = page_of(urllib.parse.unquote(u.path))
|
||||
else:
|
||||
target = page_of(os.path.normpath(os.path.join(os.path.dirname(shown_path), urllib.parse.unquote(u.path))).replace(os.sep, "/"))
|
||||
if not os.path.exists(os.path.join(ROOT, target.lstrip("/"))):
|
||||
problems.append(f"{shown_path.lstrip('/')}: link {href} goes nowhere")
|
||||
elif u.fragment and target in parsed and u.fragment not in parsed[target][0].ids:
|
||||
problems.append(f"{shown_path.lstrip('/')}: link {href}: no #{u.fragment} on that page")
|
||||
|
||||
if not pages:
|
||||
problems.append(f"no pages found in {ROOT}")
|
||||
for p in problems:
|
||||
|
||||
Reference in New Issue
Block a user