Files
roro9stack/site/content/dev/milestones/w1.md
T
twislaandClaude Sonnet 5.5 3b4100dc6a
CI / build (pull_request) Successful in 8m38s
Site / build (pull_request) Successful in 9s
Site: the developer docs (phase 4), with the Debug Builds and the Debug Console first
/dev/ has Debug Builds and the Debug Console (builds and the token, the
console and its protocol, files and screenshots, driving the UI, crashes and
Safe Mode, the command reference), Build, test and release (including how an
update works), the architecture decisions and the milestone plans.

Generated from the repository by site/tools/gen_dev_docs.py: the ADRs, the
milestones, the README's sections, and the command reference, read from the
firmware's own `help` text. The pages are committed (Zola cannot read outside
its folder); the Site workflow checks they are current, and now also runs
when src/main.cpp changes. M0, M1 and CONTEXT.md are not published.
README: the gnss commands that the table lacked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 21:25:33 +02:00

17 KiB

+++ title = "Website" description = "A public home for the project at roro9stack.net, separate from the blog (stories) and from Gitea (developers): what it is, how to install it, how to use each App, and the docs." weight = 80

[extra] docs = true source = "docs/milestones/W1.md" tag = "W1" +++ Status: phases 1 to 3 (home, Install and Downloads; the user guide; how-tos and the FAQ) and the devlog are live at roro9stack.net; phase 4 (the developer docs) is in a pull request. Issue #12.

Goal: a public home for the project at roro9stack.net, separate from the blog (stories) and from Gitea (developers): what it is, how to install it, how to use each App, and the docs.

The home page was designed on a canvas in a Claude chat (a dark and a light theme, built on the device's own 256-colour palette, pixel-notched corners, DM Mono and Hanken Grotesk). It is the starting point, not the final copy: it has to say only what the firmware does today.

What was found while planning (2026-10-06)

  • roro9stack.net already points at the server that hosts Gitea. Plain HTTP redirects to HTTPS; HTTPS has no certificate yet, which is the server's side to set up.
  • Release downloads from Gitea carry no CORS header, so a browser can't fetch the factory image from another origin as things are. Gitea is behind Caddy, which can add the header (below).
  • Zola can read JSON from a URL at build time (load_data), so the home page's "latest version" can come from the Gitea API.
  • There is no Gitea wiki: the design's "Wiki" links would 404.
  • The blog is published by pulling its repository on the web server and running zola build. The site does the same.

Decisions (design round 2026-10-06)

# Decision
Q175 The site lives in this repository, in site/, so the documentation is built from docs/, CONTEXT.md and the README instead of being copied.
Q176 Zola, like the blog. The design becomes a template, its tokens CSS custom properties. Dark and light follow the visitor's setting, with a visible switch. No JavaScript except the flasher's.
Q177 Domain: roro9stack.net. The blog stays at experiments.twis.la.
Q178 Publishing is the blog's way: the web server pulls main and runs zola build; that part is the maintainer's. Changes reach main through pull requests as everywhere. CI is split: a dedicated site job builds the site (zola build) when site/, docs/, README.md or CONTEXT.md change, and the firmware tests and builds skip a change that touches nothing else. A change that touches both runs both.
Q179 Phases, each its own pull request: 1. the CI split, the home page, an Install page with the browser flasher, downloads and the changelog. 2. a user guide page per App. 3. how-tos and the FAQ. 4. developer docs generated from the repository.
Q180 A browser flasher (ESP Web Tools), without copying the firmware. Caddy, in front of Gitea, adds Access-Control-Allow-Origin: https://roro9stack.net (and Vary: Origin) to GET and HEAD on /twisla/roro9stack/releases/download/* and /api/v1/repos/twisla/roro9stack/releases*: both are public already. The Install page asks the API for the latest release in the browser, finds the asset ending -factory.bin, and gives ESP Web Tools a manifest built on the spot, so it offers a new release as soon as it exists, with no rebuild. The library is vendored into site/static/ (Apache-2.0), not loaded from a CDN. The file's SHA-256 is shown on the page. Chrome or Edge on a desktop only; other browsers, and visitors without JavaScript, get the esptool steps on the same page.
Q181 Docs for the latest version only. The changelog is the Gitea releases, read at build time.
Q182 English only.
Q183 The FAQ starts from real questions: the README, and issues labelled kind/docs.
Q184 Fonts are self-hosted (no request to a third party). The hero keeps the design's illustrations, labelled as illustrations, and a section of real device screenshots is added.
Q185 The site says only what the firmware does today. Planned features are marked as planned, with their milestone. The mesh messenger is planned: the LoRa Scanner listens, nothing is sent.
Q186 Left out, each with its issue: a Gemini capsule mirror (#57), French (#58), docs per version (#59), search (#60).
Q187 The site has no version of its own. Contact is contact@roro9stack.net, and the issue tracker.

The design, reviewed

Kept as designed: the layout, the tokens, the nine App cards (their facts check out against the code: Probation 3 minutes, Safe Mode after 3 crashes, 60 seconds of typing before an update restarts the device).

Changed before it ships:

  • Install, not Download, is the first action. Downloads are for developers; a visitor wants to try it.
  • A "what you need" strip: Cardputer ADV, the Cap LoRa-1262 (only the radio needs it), a microSD card, Wi-Fi. And a plain status line: the version, and what isn't there yet.
  • The mesh card and the hero no longer promise sending and reading mesh messages.
  • The latest version is read from the API, not typed.
  • "Wiki" is replaced by Docs. The updates section gains what v0.11.0 added: the device installs releases from the project's server itself.
  • An independence line: not affiliated with or endorsed by M5Stack or Meshtastic.
  • No cookies, no analytics, no third-party requests, said on the page (fonts self-hosted).
  • The keyboard focus ring is invisible on the notched buttons: clip-path clips an outline. Another way to show focus is needed.
  • The wordmark SVGs carry an embedded C2PA content-credentials block: stripped from the site's copies.

Done when (phase 1)

  • Pushing a change under site/ runs the site job and not the firmware tests; a firmware change runs the firmware jobs and not the site's.
  • The home page renders in both themes, at phone width, with the keyboard, and says nothing the firmware doesn't do.
  • The latest version on the page is the latest release.
  • The browser flasher installs the latest release on a Cardputer ADV from Chrome (tried by hand), the page shows the file's SHA-256, and the esptool steps are on the same page.
  • A release published after the site was built is the one the Install page offers.
  • The home and Downloads pages make no request to another origin, and the Install page only asks git.twis.la.

Work breakdown

  1. CI split: a site workflow, path filters on the firmware workflow.
  2. The skeleton: site/ with the tokens, fonts, base template and the theme switch.
  3. The home page, from the design, with the changes above.
  4. Install and downloads: the flasher with its manifest built in the page, the esptool steps, the changelog. Needs the Caddy headers on the Gitea host (the maintainer's side); the page is tested against them once they're in.
  5. Checks, recorded here.

As built (phase 1)

  • CI is split. ci.yml (the firmware) has paths-ignore: site/**, docs/**, README.md, CONTEXT.md on pushes to main and on pull requests. site.yml runs zola check and zola build with a Zola pinned by its checksum, then site/tools/check_site.py, when those files change. Gitea's own source (v1.24, read, not run against the 1.27 server) shows that path filters count as matched for tag pushes, so a tag still releases. A change that touches both runs both.
  • The build output goes to public/ at the root of the repository, not into site/: output_dir = "../public" in site/config.toml, so zola build in site/ and zola --root site build from the root agree, and git ignores /public/.
  • The site is in site/: config.toml, templates (base, home, install, downloads, 404), data/ for the App cards and the screenshots' captions, static/ (stylesheet, theme switch, fonts, wordmark, icons, real screenshots, the vendored flasher). site/README.md says how to build it and what the server needs.
  • The home page follows the design. Changed from it: the hero and the mesh card promise nothing that isn't built (the mesh messenger is a "planned" card), an Install button first, a status box, a "what you need" row, a section of real screenshots, the updates section says the device installs releases itself, an independence line and a statement about cookies and third-party requests in the footer, the latest version read from the Gitea API at build time, and the nav's Wiki replaced. The two hero drawings are generated by site/tools/make_illustrations.py (a port of the design's scripted shapes) as inline SVG.
  • The focus ring. clip-path clips outlines, so a focused notched control drops its notches and shows square corners and its ring.
  • The Install page asks the API for the latest release in the browser, builds the ESP Web Tools manifest as a blob, shows the version, size and SHA-256, and only ever hands the flasher a download from the project's own server for this repository. The flasher library (ESP Web Tools 10.4.0, Apache-2.0) is vendored, trimmed to the ESP32-S3. Fonts (DM Mono, Hanken Grotesk, SIL OFL) are self-hosted.
  • The Downloads page lists the last 30 releases with their files, read at build time.
  • The wordmark SVGs from the design carried an embedded C2PA content-credentials block; it is removed from the site's copies.

As built (phase 2, the user guide)

  • /guide/ is a section of 11 pages, site/content/guide/, each with template from the section's page_template and an order from weight: the basics (keys, Launcher, Status Bar, first start, the card), then one page per App (LoRa Scanner, GNSS, Gemini, IRC, Wi-Fi tools, Notes, Storage, System), Settings and Updates. Pages with real screenshots list them in extra.screens, looked up in data/screens.toml.
  • Facts come from the README, the milestone documents and the Apps' own source (key handlers, labels, the Status Bar's drawing code), not from memory. Some wording was corrected against the source while writing: the Track folder is /gnss/tracks, the reasons a Track won't start, what the Status Bar shows.
  • Not covered: the mesh messenger (planned), the debug console and Debug Builds beyond a pointer to the README. How-tos and the FAQ are phase 3.

As built (the devlog)

Not one of the planned phases: the blog's seven roro9stack posts, imported into site/content/devlog/ and shown in the site's own style, with the Blog link in the navigation and the footer replaced by Devlog. The posts' text, tone and structure are unchanged; what changed:

  • Links: the posts' links to each other point to /devlog/<same name>/, and one link to an unpublished work-in-progress post became plain text. Each post keeps its old directory name, so the old URL /<name>/ maps to /devlog/<name>/.
  • The posts' parts (the sign, the cast, the steps, asides, folded sections, diagrams, captions) are shortcodes in site/templates/shortcodes/, restyled in static/css/devlog.css: the site's palette, notched boxes, DM Mono and Hanken Grotesk. The two older posts about other subjects (a vinyl remote, a ZFS rescue) stay on the blog.
  • The 17 diagrams are inline SVG, and carried <style> blocks and style attributes that the site's Content-Security-Policy refuses. Their rules moved to static/css/devlog-diagrams.css (one block per diagram, plus colour classes for what the attributes did), and a diagram's minimum width is a class, not a style attribute. The Caddy policy needs no change.
  • The diagrams' four colours (red, green, yellow, accent) are defined for .devlog on the site's RGB332 grid, one value for each theme.
  • Links between posts: every post's reference to another ("the last post", "the first post", the milestone lists) is a link to it. check_site.py now also checks every link inside the site, and its #fragment: a broken one fails the Site job. External links are checked by zola check run by hand (without --skip-external-links, which CI uses); its only complaints today are line-range and heading anchors on Gitea, which Gitea resolves in the browser.
  • An Atom feed at /devlog/atom.xml, linked from every devlog page.
  • Checked in Chromium with the production CSP applied to every response: the index and the seven posts, at 1100 and 390 px, no policy violation, no broken image, no sideways scroll; check_site.py 24 pages, 0 problems.

Checks (2026-10-06)

Check Result
The Site workflow's own commands, in a clean container with the pinned Zola zola check clean, build and page checks pass
tools/check_site.py 4 pages, 0 problems: titles, descriptions, a language, every image with alt text, every local file referenced exists, nothing loaded from another origin
Browser tests (Chromium, 16 checks) All pass: no request to another origin from the home, Downloads and 404 pages; no horizontal scroll at 1280 and 390 px; a visible focus ring on a notched button; the Install page reads the latest release, shows its version, size and SHA-256, builds a blob manifest naming an ESP32-S3 factory image at offset 0, and loads only git.twis.la; a download on another host is refused; the real server (no CORS header today) makes the page fall back to the esptool steps
The pages looked at Home in dark and light, at desktop and phone width; Install; Downloads
esptool against the real v0.11.0 factory image An ESP32-S3 image, bootloader at 0x0, partition table at 0x8000: flashing at offset 0 is right. The command's syntax was checked, not a flash

Not checked:

  • Flashing a real Cardputer from Chrome. It needs the device on a machine with a browser; the page's flasher logic is tested, the flashing itself isn't.
  • Caddy's headers on the real server (not applied yet), and HTTPS on roro9stack.net (the name resolves, the certificate isn't there).
  • The CI split on a change that touches only site/ or docs/. This pull request touches both the workflows and the site, so it runs both; the first docs-only pull request will show it.
  • Firefox and Safari rendering, screen readers, and a printed page.
  • The unverified wording in the Install text is kept to what's known: nothing about how long flashing takes, or what the screen shows in download mode.

As built (phase 3, how-tos and the FAQ)

  • /howto/ has eight short recipes: when flashing fails, find your files on the SD card, install an update from the card, use a network without DHCP, record a Track, capture LoRa packets for Wireshark, read Gemini pages offline, and what to do when a connection says "not enough memory". /faq/ is one page of questions with a list at the top. Both use the guide's templates (guide-index.html, guide-page.html, now generic: the page's parent section gives the eyebrow, the title and the pager).
  • The FAQ starts from the README and from the problems the project met (Q183): the flash troubles and the memory limit are the two that were hit most. The issues labelled kind/docs turned out to be design rounds for the mesh, not user questions, so they gave nothing to answer.
  • Every step comes from the README, the milestone documents or the Apps' source. The privacy answer says plainly that the device contacts the project's server once a day for the update check (on by default, one switch to turn it off).
  • Linked from the guide's index, not the navigation, which stays short.

As built (phase 4, the developer docs)

  • /dev/ has four sections: Debug Builds and the Debug Console (first, and the longest: Debug Builds, the Console and its protocol, files and screenshots, driving the UI, crashes and Safe Mode, and the command reference), Build, test and release (the README's build, CI and flash sections, and how an update works, with the update file, the four ways in and Probation drawn), Decisions (the ADRs) and Milestones (the plans).
  • Generated from the repository, not copied by hand: site/tools/gen_dev_docs.py writes the ADR pages, the milestone pages, the README's sections, and the command reference, which is read from the firmware's own help text in src/main.cpp and then the README's table of what each command does. Zola can't read outside its own folder (not even through a symlink), so the generated pages are committed, and the Site workflow runs gen_dev_docs.py --check and fails when one is out of date; it now also runs when src/main.cpp changes, because the command list lives there. The server's pull; zola build is unchanged.
  • Left out on purpose: the M0 and M1 milestone documents and CONTEXT.md (the glossary) describe Wi-Fi monitoring, which the site does not publish. They stay in the repository.
  • The Debug Console pages were written against the source and the live console: the protocol (the token line, the banner, the 4 KB backlog, one client, 8 queued commands, 240-byte lines, denied after a second) and the replies shown were checked on a Debug Build, v0.11.0-3, over Wi-Fi. Not run: crash abort, crash wdt and Safe Mode, which are described from ADR 0005 and the code.
  • Found while writing it: the README's table lacked the gnss commands (rows added); piping commands into rdbg.py returns before the replies unless the input stays open (documented, not changed); update install on a Debug Build needs force (documented).