Repository navigation
docs(architecture): the board is the default face - the switch, the cross-links, and the release gate - #603
Conversation
…hips in the other repo
Operator decision 2026-09-26: the TollGate board becomes the DEFAULT face - the
hostname/entry point opens the board, not LuCI - and LuCI stays available but
secondary, via a cross-link BUTTON in the board UI and a config SWITCH for what
the hostname/ports serve. This PR records the decision, the mechanism that makes
it a two-repo change, and the three defects it has to answer. It ships no
behaviour: docs-only.
The record: docs/architecture/default-ui-and-entry-port-decision.md
* The switch is a flat config.json field, entry_ui in {board, luci}, default
board (D1): declared in the schema so the board's settings page renders it
with no SPA code, read by the uci-defaults writers with jq, and a missing
file / missing key / unparseable value all resolve to the SAME mapping as
board - never to "no admin listener" (invariant 6).
* The mapping moves PORTS, not webroots (D2): in board mode the entry pair
(:8080 HTTP + :443 HTTPS + the derived redirect) is answered by uhttpd.admin
(the board) and the secondary pair (:8090/:8443) by uhttpd.main (LuCI); luci
mode is today's mapping exactly. Because the two port SETS do not change,
neither guard fragment, nor the br-mgmt scope, nor the pre-auth list, nor
any guest-path byte moves - and the whole change reverts with one value.
* Why the entry point is a port question at all: uhttpd resolves ONE docroot
per instance and resolves CGI against it, so LuCI's webroot must be /www and
the board's must not be. The 2026-09-20 MT3000 incident (portal
92-tollgate-admin-setup header, OpenTollGate#566) is what that costs when it is wrong.
Path prefix, a shipped /www/index.html (image-owned, restored by apk) and a
hostname alias are each rejected with the reason.
* One writer per section, and the identity follows the listener (D3): the
portal's 92 owns uhttpd.admin (the only writer that knows the build-time
brand webroot), the module's 99 owns uhttpd.main, and both compute the
mapping from the same switch value. uhttpdCertPath()/applyRedirectHTTPS()
(src/cmd/tollgate-cli/ssl.go:858-880) read uhttpd.main.cert BY SECTION NAME
today and must read the certificate of the section that owns :443, or in
board mode the module would keep provisioning the secondary listener while
the entry served the image placeholder.
* A half-converged pair degrades to today's mapping, never to a bind fight
(D4). One port, one binder: a new 99 beside a pre-switch 92 would have both
sections claiming :8090 and an admin UI would not start. The mode-aware 92
announces the protocol; 99 honours entry_ui=board only when it does.
Consequence, stated as a release gate: the board default may ship only in a
release whose feed pin carries the mode-aware 92 - the feed is the
atomicity boundary, as in the br-mgmt record's promotion order.
* A cross-link is a router answer or nothing (D5): tollgate ui links --json
returns an https:// URL on the same host with a port the router supplies,
only while that UI's TLS listener exists with an identity that covers this
router and that UI's port pair is inside the admin-access scope; otherwise
url is null and NO button is rendered, with the reason surfaced instead of a
dead link.
Three defects read from source, all in the Context section:
1. The release guide called LuCI's URL "the admin UI" (docs/rc-tester-guide.md
:419 vs :442): both statements true, the pair ambiguous, and :443 answers
LuCI - the inconsistency OpenTollGate#593 left, and what the operator read as "the
board's URL is https://<hostname>.lan/".
2. The board SPA's own cross-link is
href={`http://${window.location.hostname}:8080/`}
(portal origin/main admin/src/routes/login.tsx:336): cleartext, a
hardcoded port, and self-referential once :8080 answers the board.
3. The board's opt-in :8443 still serves the image's placeholder identity:
portal 92 keys it off /etc/uhttpd.crt while the module's generator writes
/etc/tollgate/ssl/server.{crt,key} (ssl.go:23-31; uhttpdCertDefault is a
fallback for uhttpd.main, ssl.go:1143-1155). OpenTollGate#593 fixed the entry listener
and left this one.
Also in this PR, the guide half of (1): docs/rc-tester-guide.md section 7 now
names BOTH admin UIs and their listeners in one table, says plainly that the
entry point answers LuCI on this build (a report saying so is describing the
shipped mapping, not a defect), states the HTTPS-only rule for admin
cross-links, and separates the board's :8443 certificate question from LuCI's
:443 one.
17 assertions that must hold before this ships, split offline/bench, each one
something a person or a script can run, plus the implementation slices per repo
(module, portal, feed, bench card). Two things this record deliberately does not
decide are listed as such: the admin-access network field (card t_868d0fa7,
folding into the br-mgmt record) and the paywalled wired network (t_ba0558f2).
…t opened as The entry was written before the PR existed and guessed OpenTollGate#602; OpenTollGate#602 was taken.
|
Technical review (evidence pass) — PR head Docs-only: a decision record, the guide half of defect 1, and a CHANGELOG entry. Compact pass per the triage rule — but the record's citations were still walked against source, because a document that will be cited as the source of the mapping is only as good as its line numbers. The claims check out. Every in-tree citation verifies exactly: One factual slip worth fixing. The SOP-surface cost bullet cites "the operator guide's 'where do I log in' paragraph" (record, L290-L295) and assertion 10 folds The §7 rewrite itself is the honest correction of defect 1 — one table naming both UIs and their listeners, a plain statement that the entry point answers LuCI on this build, and the board's Hygiene: two clean commits with the "#602 was taken" repair as its own commit, no assistant-attribution footers; CHANGELOG entry under Changed / Internal in the house wrapped format with the correct PR link; no code, dependencies, or packaging surface touched, so the Go battery and contract lint are unaffected; the pre-commit claim matches the configured hook set and the new file is clean on trailing whitespace and EOF newline. Disposition: land-with-followups —
|
main moved four commits ahead of this branch's base (37c5141) while the PR was in review. One conflict, and it is the CHANGELOG [Unreleased] / Changed / Internal section only: both sides prepend an entry at the same anchor. Kept both (OpenTollGate#603's board-default-face record first, then main's OpenTollGate#606 src/cli module-file tidy). Nothing else in this branch conflicts; the branch is docs-only (docs/architecture/default-ui-and-entry-port-decision.md, docs/rc-tester-guide.md, CHANGELOG.md) and the merge adds nothing to that set. Checked rather than assumed: `git diff upstream/main -- CHANGELOG.md` has zero removed lines, i.e. no side of the conflict was dropped.
Head branch was pushed to by a user without write access
OpenTollGate#603) Second pass: main moved again while this branch was in review (0a7c3bb -> b6ecb16, the squash-merge of OpenTollGate#608). Same single conflict, same anchor: CHANGELOG [Unreleased] / Changed / Internal, where both sides prepend an entry. Kept both (this branch's board-default-face record, then OpenTollGate#608's per-merchant pre-flight seam); the previously merged OpenTollGate#606 entry is untouched below them. Zero removed lines against main.
|
Conflicts resolved — the PR is What conflicted Only The conflict had to be resolved twice because
Both passes were unions — this branch's entry keeps its place first, and main's entries ( Verification of the resolved head
Note for the record: this repo's live CI lane is ngit CI — a pushed GitHub PR branch is not mirrored, so no workflow fires on this head unless it is pushed to the mirror and triggered manually. Nothing in this PR is coverable by the Go lanes anyway (docs-only), so I did not spend a CI run on it. If you want a green check recorded for the release gate, say so and I will push the head to |
What
Operator decision 2026-09-26: the TollGate board becomes the default face —
the hostname/entry point opens the board, not LuCI — and LuCI stays
available but secondary, via (a) a cross-link button in the board UI and
(b) a config switch for what the hostname/ports serve. This PR is docs
only: it records the decision, the mechanism, and the three defects the
decision has to answer; it ships no behaviour.
Files:
docs/architecture/default-ui-and-entry-port-decision.md(D1-D9, 17offline+bench assertions, 6 rejected alternatives, the implementation slices
per repo);
docs/rc-tester-guide.md§7 — the ambiguity fix(packaging): provision the router's TLS identity on setup, and require a covering certificate for redirect_https #593 left, corrected;CHANGELOG.md— one entry under Changed / Internal.The decision in one screen
entry_ui:8080+:443+ derived redirect):8090+:8443)board(default)uhttpd.admin,home=$ADMIN_HOME,error_page=/index.html,ubus_prefix=/ubus, no cgi/lua prefixesuhttpd.main,home=/wwwluci(legacy)uhttpd.mainuhttpd.admin, i.e. todayPorts move; webroots do not. The two port sets are unchanged, so
31-*.nft:52-53,32-*.nft:54-55, thebr-mgmtscope (PR #601), the pre-authlist and the guest path are all untouched — what changes is which UI answers on
each pair, and the whole change reverts with one value (
entry_ui=luci).Why the entry point is a port question: uhttpd resolves one docroot per
instance and resolves CGI against it, so LuCI's webroot must be
/wwwandthe board's must not be — which is exactly the 2026-09-20 MT3000 incident the
portal's
92-tollgate-admin-setupheader records (home=/wwwon the board'sport served the real LuCI CGI, #566). A path prefix on one instance, a shipped
/www/index.html(image-owned; the nextapktransaction restores or deletesit) and a hostname alias (DNS selects an address, not a port) are each rejected
with the reason in the record.
Two things the record decides against shipping blind
These are the parts a reviewer should look at hardest, because both are how this
change would take an admin UI away rather than add one:
A half-converged pair degrades to today's mapping, never to a bind fight
(D4). One port, one binder. The two halves live in two repos and arrive by
different installers (module postinst runs
90/99; the feed's recipe brings92; at boot the numeric order is90, 92, 99), so a router carrying a new99beside a pre-switch92would have both sections claiming:8090and one instance would not start. The mode-aware
92therefore announces themapping protocol (a marker) and
99honoursentry_ui=boardonly when itis present; otherwise it repairs to the
lucimapping, logs one WARNING, andleaves the operator's value recorded for the release that carries both halves.
Stated as a release gate: the
boarddefault may ship only in a release whosefeed pin (
vendor.lock.json/PKG_SOURCE_VERSION) carries the mode-aware92. The feed is the atomicity boundary — the same shape thebr-mgmtrecord's promotion order uses.
The cross-link is a router answer or it is not rendered (D5). A
per-client answer is not available to the board: its backend is rpcd over
the uhttpd instance's
ubus_prefixand the plugin sees the request payloadonly (
openwrt/rpcd/tollgatepipes it throughcat), while the one placethat can answer per socket is the module's identity resolver on
:2121—which the admin path deliberately cannot reach (
30-backend-firewall.nft:21-22drops
:2121foriifname != { "br-lan", "lo" }). So the record keepsliveness a router-wide fact (
tollgate ui links --json→https://<same-host>:<router-supplied-port>/, only while that listener existswith an identity that covers this router and that UI's port pair is inside
the admin-access scope; otherwise
url: nulland no button) and makes "the twoUIs' network scope is identical" an invariant with a drift guard. That is the
honest shape of the card's second constraint, and A5 in the record says what a
genuine per-client rule would require.
The three defects read from source
docs/rc-tester-guide.md:419sends the tester to
https://<hostname>.lan/;:442then says the board haslisteners of its own. Both true, the pair ambiguous, and
:443answersLuCI — which is what an operator read as "the board is at
https://<hostname>.lan/". fix(packaging): provision the router's TLS identity on setup, and require a covering certificate for redirect_https #593's subject was TLS identity, not which UI aURL answers.
origin/mainadmin/src/routes/login.tsx:336ishref={`http://${window.location.hostname}:8080/`}— cleartext, ahardcoded port, an admin login on the far side, and self-referential once
:8080answers the board.:8443still serves the image's placeholder identity. Portal92keys the opt-in listener off/etc/uhttpd.crt, which is the OpenWrtimage's placeholder (
CN=OpenWrt, no covering SAN); the module's generatorwrites
/etc/tollgate/ssl/server.{crt,key}(src/cmd/tollgate-cli/ssl.go:23-31,/etc/uhttpd.crtsurvives only as a fallback foruhttpd.main,ssl.go:1143-1155). fix(packaging): provision the router's TLS identity on setup, and require a covering certificate for redirect_https #593 fixed the entry listener and left this one, so anHTTPS-only cross-link cannot point at
:8443as shipped.Also in this PR: the guide half of defect 1
docs/rc-tester-guide.md§7 is now "The two admin UIs, and what acertificate warning means": one table naming both UIs and their listeners, a
plain statement that the entry point answers LuCI on this build (so a report
saying so describes the shipped mapping, not a defect), the HTTPS-only rule for
admin cross-links, and the board's
:8443certificate question separated fromLuCI's
:443one.What is not delivered here
The mapping itself. It is split across two repositories and one release
boundary by design, and the record names the slices:
tollgate-module-basic-go— theentry_uifield/schema/defaults/migration,99's mapping writer and the D4 marker gate, the identity-follows-listenerchange (
setup_uhttpd_tls_identity+ssl.go), the mode-aware rewrites ofdrop_admin_listeners/setup_uhttpd_configui/sanitize_uhttpd_main_configui_port,tollgate ui links, offline suite 1-10;tollgate-captive-portal-site—92becomes mode-aware (entry vs secondarypair, marker, the provisioned identity, the pre-auth
add_listblock removed— it contradicts the module's own
del_listand, in one mode or the other,names the LuCI admin login;
:8443no longer keyed off/etc/uhttpd.crt),the login page's anchor replaced by the
ui linksrender, the portal-sideguards;
FreedomTechFeed/packages— re-vendor + re-pin in the same release that turnsentry_ui=boardon by default;Nothing here has been measured on hardware. The assertions are the only
honest statement of what "done" means, and the record says explicitly which of
them need the router.
Gates
.nft, no packaging file is touched, somake go-battery, the contract lint and the offline uci-defaults tier areunaffected.
pre-commit(trailing-whitespace, end-of-file, merge-conflict,detect-secrets) passed on the commit.
configured (
ngit account list→ no accounts available), same as fix(packaging): provision the router's TLS identity on setup, and require a covering certificate for redirect_https #593.merge_claim: this PR is the single merge for this change — branch
pr/board-default-ui, filesdocs/architecture/default-ui-and-entry-port-decision.md(new),
docs/rc-tester-guide.md,CHANGELOG.md. No other open PR carries it.docs/rc-tester-guide.mdis a hotspot (#601 also edits it) andCHANGELOG.mdisthe usual prepend conflict; both are docs-only and resolve by keeping both sides.