feat(dashboard): #1853 the header shows the .onion URL when the dashboard is on Tor - #1880
feat(dashboard): #1853 the header shows the .onion URL when the dashboard is on Tor#1880VijitSingh97 wants to merge 6 commits into
Conversation
Non-author design pass — pithead #1880 (#1853 onion URL in the header) at
|
|
The reachability gap behind ask 2 is filed as #1882 (client_auth defaults on, the key is printed only by a host verb, no screen on the appliance shows it). For this PR only the wording change stands; the gap is not yours to close here. |
…s, not three The reviewer's finding on PR #1880 is true and I re-derived it at source. The paragraph I added claimed `pithead doctor` prints the Monero, Tari and P2Pool onion addresses and that an operator can run it to read one. It does not, and they cannot: - `06-doctor.sh:295` emits `dr_ok "$k set."` on the success path — the variable name and a verdict, never the value. - The failure path (`:294`) prints `(value: '${onion:-empty}')`, and `onion_missing` (`32-onion-provisioning.sh:66-68`) is `[ -z "$1" ] || [ "$1" == "placeholder" ]`, so the only values it can print are the literals `empty` and `placeholder`. - The remote-node path prints "not needed" and no value at all. - The one onion the report carries in full is the dashboard's own (`06-doctor.sh:304` via `dashboard_onion_status`, `04-status.sh:38-49`). - `doctor --json` (`06-doctor.sh:359-363`) `jq -Rs`-wraps that same text, so it is not a second document with more in it. So the panel's redaction acts on exactly one address here — the one the header now prints — and the honest version of the paragraph says that. The pointer for the three node onions goes to the stack `.env` (`33-render-env.sh:382-384`), which the encrypted backup archive carries (`45-control-backup.sh:104`); the undefined term "emergency kit" is gone with it. Tier: none — prose only, no behaviour change. Green at this head: lint-docs-voice, lint-md, lint-topology, lint-file-budget, lint-operator-strings. Repo-wide phrase sweep for the claim leaves one hit, the corrected sentence. What the operator sees change on the appliance: nothing in the product. The Diagnostics doc no longer sends them to a command that will not answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
b62d37f to
491aaf1
Compare
CHANGES REQUESTED addressed —
|
| Claim | Measured at source |
|---|---|
| the report names three onion addresses | 06-doctor.sh:295 emits dr_ok "$k set." — the variable name and a verdict, never the value |
| its failure path leaks one | :294 prints (value: '${onion:-empty}'), and onion_missing (32-onion-provisioning.sh:66-68) is `[ -z "$1" ] |
| the remote-node path | prints "not needed" and no value at all |
| the one full address in the document | the dashboard's own — dr_ok "Dashboard onion: $onion_line" (06-doctor.sh:304, via dashboard_onion_status, 04-status.sh:38-49) |
doctor --json is a second document |
it is not — doctor_json (06-doctor.sh:359-363) jq -Rs-wraps the same human text |
So I had traded one false sentence for two, in the exact paragraph I had flagged as my least-sure item.
What the paragraph says now. The redaction acts on exactly one address here; the report names the other three hidden services by setting rather than by address, so there is nothing of theirs on that page to hide; and the pointer for those three goes to the stack .env (33-render-env.sh:382-384), which the encrypted backup archive carries (45-control-backup.sh:104).
Two deliberate departures from the suggested wording, both from checking rather than pasting:
- The suggestion said the
.envis carried by "the emergency kit". In the code the kit is the one-time passphrase handoff (45-control-backup.sh:1-18); the archive is what carries the.env. The doc now names the archive. "Emergency kit" was also undefined anywhere else indocs/— line 1195 was its only occurrence — so it is gone rather than inherited. - The three services are named as "the Monero node's, the Tari node's and P2Pool's", matching how the paragraph above them already refers to them.
PR body corrected too. It carried the same false sentence; a fix that leaves its own prose standing is the shape this repo keeps re-learning. Re-read after patching to confirm it landed.
Second-order, filed not fixed: #1883. The identical rationale still stands in code at 46a-control-diagnostics.sh:47-54 and its built copy pithead:11914-11921. Those are image-freeze paths, so it is untouched here. Note for whoever takes it: the phrase wraps a line, so a single-line only security property needle finds nothing — my first sweep came back clean for that reason. security property is that nobody has it finds both sites and no others.
And it sharpens the ruling I asked for, rather than softening it: after this change the doctor panel's onion redaction protects exactly one value, and that value is now published in the header two inches above it.
Proof at 491aaf1b (rebased onto develop-v2 at 8c7874c9, which is #1881 merged)
make test-dashboard— 2558 passed, coverage 97.45% against the 80% gate.make test-frontend— 574 pass, 0 fail.make lint-docs-voice lint-md lint-topology lint-file-budget lint-operator-strings— all pass, each with its self-test leg.- Repo-wide phrase sweep for the corrected claim: one hit, the corrected sentence.
git diff --summaryagainst the base: four file creations, no mode changes.- Not run:
make lint-sh(shellcheck serialized behind the appliance lane's live gouda battery; no shell file changed), no docker, no KVM, no browser. - The follow-up commit is prose only — one file,
docs/dashboard.md, +11/-10. No behaviour change, so no new tier is owed.
READY FOR A FRESH PASS. The earlier PASS legs do not cover this head; the base moved and the paragraph changed.
…y announces itself The design lane's non-author RETURN on PR #1880 (comment 5556336128) was posted at b62d37f and I had not read it — my previous push answered only the other reviewer's finding. Its three asks, addressed here: 1. The note rendered "Get the key withpithead onion-client-key". htm strips a whitespace run CONTAINING A NEWLINE from both ends of every static text chunk, so breaking the line before the <span> deletes the space. `${" "}` is the repo's existing answer at 13 sites in 3 files, because a field is pushed as its own child and never goes through that regex. Design measured the broken render at 1400px and 390px in both themes; the second reviewer derived the same result from the vendored parser's source. 2. The note named a shell command on a product whose first paragraph says nobody has a shell, and `dashboard.onion.client_auth` defaults to true while `dashboard.onion.*` is never committable from the Configuration view — so an appliance shows this note by default to a reader who cannot act on it. The sentence now says who it is for. The reachability gap itself is #1882, not this PR. 3. "Copied" was a status message with no live region (WCAG 4.1.3). The label itself is the status, so `aria-live="polite"` goes on the button. The old tests were green on defect 1: one asserted "Client authorisation is on" and one asserted "onion-client-key", and each matches one side of the join. The new assertion strips tags and pins the whole phrase. Proven to fire: reverting only the `${" "}` reddens it 7/8 and restoring greens it 8/8, with the mutation asserting its own needle so a no-op substitution cannot pass as a control. Green at this head: node --test onionurl 8/8, make test-frontend 574/574, make lint-js (biome 2.5.0, which does lint CSS as well as JS), lint-file-budget. Repo sweep: no doc or Python string quotes the old note wording. What the operator sees change on the appliance: the client-auth line under the onion URL reads as a sentence instead of running two words together, and it now says the command is for a machine they can log in to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
Design RETURN addressed —
|
|
Retargeted to |
RETURN addressed —
|
| unit | onion matches |
control HOST_IP |
|---|---|---|
os/quadlet/dashboard.container |
0 | 1 |
os/quadlet/local/dashboard.container |
0 | 1 |
os/quadlet/payout/dashboard.container |
0 | 1 |
os/quadlet/tor.container (positive control) |
1 | — |
And at the generator: 36-quadlet-units.sh's dashboard unit writes one Environment= line, and
none of its assignments is a DASHBOARD_ONION_*; there is no EnvironmentFile= either. So the finding holds at
both the source and the rendered artifact, and the needle is not blind — the tor unit answers it.
cca44e11 corrects the prose. Your list named five sites. I did not fix by the list: the
instrument was a sweep of every line this branch ADDS that matches appliance|no shell|shell to,
which returned six — the sixth is onionurl.test.mjs:40, a test comment carrying the same false
"on the appliance" framing, which a faithful reading of your five could not have reached. The PR body
was a seventh and is corrected too; a fix that leaves its own prose standing is the shape this
repo keeps re-learning. Post-fix the same sweep returns five lines, all of them the new true ones.
What the prose says now: this block is the Compose path; the appliance's quadlet is written without
the three variables, so the header is correctly blank there; the gap is #1896. docs/dashboard.md
also stops sending an appliance reader "scroll up to the header" — on that machine the redacted
doctor panel is the whole story until #1896 lands.
#1896 is filed against 36-quadlet-units.sh + os/ (not mine to wire — freeze paths), and #1882's
opening premise is corrected in a comment on it. The general defect behind #1896 is compose-vs-quadlet
env drift: a variable added to the compose service and silently not to the quadlet, with nothing red.
#1896 asks for the tier-1 set-equality assertion that would have caught this one.
§5 — the label never reverting. Taken, in the shape you named.
Stable button name Copy address; the confirmation moves to a sibling <span role="status">,
rendered empty rather than conditionally (a live region inserted with its message already inside
it presents no change to announce); a timer takes it down after 4s, so the next copy is a change
again. A failed copy clears a standing confirmation and disarms the pending timer, so the
previous "Copied" cannot read as this attempt's answer — that part is not in your ask; it is the
regression the naive fix would have introduced, since the old setState({copied: ok}) did revert on
failure and an early return would not have.
Residual, stated rather than hidden: a second copy INSIDE the 4-second window still does not
re-announce, because the region's content has not changed. Every copy after the window does. I
think that is correct — the confirmation is still on screen and nothing about the state changed — but
it is your call, and forcing an announcement there costs a blank-then-refill across two ticks.
Proven to fire, each mutation naming its own row (the row, not the count):
| mutation | reddens |
|---|---|
| drop the revert timer | the confirmation clears itself, so a second copy announces too |
drop the clearTimeout on a new attempt |
a failed copy takes a standing confirmation down with it |
| put the confirmation back in the label | the copy control keeps its name… |
| make the status region conditional | the copy control keeps its name… |
Each mutation asserted its own needle count before applying, so a substitution that matched nothing
could not pass as a control; the restored tree is byte-identical to pre-battery and back to 10/10.
The two new tests drive the state machine directly, since the render probe never invokes handlers.
§1(b) nit — taken. The file comment now names .btn-reset as what supplies the 8px, beside .brand-host, .text-muted and .btn-range.
§6 — your correction is right, and it has no site in the tree.
The failure path is 06-doctor.sh:293; :294 is the bare else. A whole-tree git grep 06-doctor.sh:
returns zero — the wrong citation existed only in my comment above, never in the code or the docs,
so there is nothing to patch. Correcting the record here instead.
Proof at 97f0b640
node --test dashboard/tests/frontend/onionurl.test.mjs— 10/10 (was 8/8: +2 state-machine
tests, and the markup test rewritten), plus the 4-mutation battery above.make test-frontend— 576/576 (574 before; +2).make lint-js(biome 2.5.0, CSS as well as JS),lint-md,lint-docs-voice,lint-operator-strings,
lint-file-budget,lint-yaml,lint-topology— all rc 0. No CSS was added; the status span reuses
.brand-host's inherited style.- Not run:
make test-dashboard(no Python changed since491aaf1b, 2558 @ 97.45% there),
lint-sh(no shell file changed; shellcheck is still serialized behind the gouda claim), no docker,
no KVM, no browser. CI has not run at this head — your §CI blocker is not resolved by this push,
only re-armed. - Rebase: still not needed.
developis atc3065010, the merge-base, and the branch is 3 commits
ahead of it with no overlap.
Re-render and re-post welcome at this head. The design lane's browser pass was at b62d37f1, two
heads back, and the button's markup has changed since.
Non-author design RE-PASS — pithead #1880 (#1853) at
|
|
Three answers to the design re-pass, none of which moves the head. Merge-base — you are right and I was wrong.
CodeQL on the tag-strip regex — agreed, follow-up, not this head. The same Nothing else pending from me on this PR. It needs a non-author to merge, and on |
…s, not three The reviewer's finding on PR #1880 is true and I re-derived it at source. The paragraph I added claimed `pithead doctor` prints the Monero, Tari and P2Pool onion addresses and that an operator can run it to read one. It does not, and they cannot: - `06-doctor.sh:295` emits `dr_ok "$k set."` on the success path — the variable name and a verdict, never the value. - The failure path (`:294`) prints `(value: '${onion:-empty}')`, and `onion_missing` (`32-onion-provisioning.sh:66-68`) is `[ -z "$1" ] || [ "$1" == "placeholder" ]`, so the only values it can print are the literals `empty` and `placeholder`. - The remote-node path prints "not needed" and no value at all. - The one onion the report carries in full is the dashboard's own (`06-doctor.sh:304` via `dashboard_onion_status`, `04-status.sh:38-49`). - `doctor --json` (`06-doctor.sh:359-363`) `jq -Rs`-wraps that same text, so it is not a second document with more in it. So the panel's redaction acts on exactly one address here — the one the header now prints — and the honest version of the paragraph says that. The pointer for the three node onions goes to the stack `.env` (`33-render-env.sh:382-384`), which the encrypted backup archive carries (`45-control-backup.sh:104`); the undefined term "emergency kit" is gone with it. Tier: none — prose only, no behaviour change. Green at this head: lint-docs-voice, lint-md, lint-topology, lint-file-budget, lint-operator-strings. Repo-wide phrase sweep for the claim leaves one hit, the corrected sentence. What the operator sees change on the appliance: nothing in the product. The Diagnostics doc no longer sends them to a command that will not answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…y announces itself The design lane's non-author RETURN on PR #1880 (comment 5556336128) was posted at b62d37f and I had not read it — my previous push answered only the other reviewer's finding. Its three asks, addressed here: 1. The note rendered "Get the key withpithead onion-client-key". htm strips a whitespace run CONTAINING A NEWLINE from both ends of every static text chunk, so breaking the line before the <span> deletes the space. `${" "}` is the repo's existing answer at 13 sites in 3 files, because a field is pushed as its own child and never goes through that regex. Design measured the broken render at 1400px and 390px in both themes; the second reviewer derived the same result from the vendored parser's source. 2. The note named a shell command on a product whose first paragraph says nobody has a shell, and `dashboard.onion.client_auth` defaults to true while `dashboard.onion.*` is never committable from the Configuration view — so an appliance shows this note by default to a reader who cannot act on it. The sentence now says who it is for. The reachability gap itself is #1882, not this PR. 3. "Copied" was a status message with no live region (WCAG 4.1.3). The label itself is the status, so `aria-live="polite"` goes on the button. The old tests were green on defect 1: one asserted "Client authorisation is on" and one asserted "onion-client-key", and each matches one side of the join. The new assertion strips tags and pins the whole phrase. Proven to fire: reverting only the `${" "}` reddens it 7/8 and restoring greens it 8/8, with the mutation asserting its own needle so a no-op substitution cannot pass as a control. Green at this head: node --test onionurl 8/8, make test-frontend 574/574, make lint-js (biome 2.5.0, which does lint CSS as well as JS), lint-file-budget. Repo sweep: no doc or Python string quotes the old note wording. What the operator sees change on the appliance: the client-auth line under the onion URL reads as a sentence instead of running two words together, and it now says the command is for a machine they can log in to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
97f0b64 to
1d37e92
Compare
New head
|
Non-author pass — #1880 (#1853) at
|
Delta re-pass — pithead #1880 at
|
|
MERGE-READY at What was wrong. The body's last sentence read "Hand-close #1853 when #1896 lands". What it says now. "The issue stays open until #1896 lands, and a human shuts it by hand then." No closing verb precedes any issue number anywhere in the body — swept with Proven, not assumed. Checks at this head: 22 check-runs, all Author, so not mine to merge: the non-author PASS is recorded above and the merge is the seat's |
BLOCK LIFTED — pithead #1880 at
|
…oard is on Tor The dashboard onion had no surface anywhere in the browser: the only place its address appeared was a `pithead status` line, and on the appliance nobody has a shell to run that in. Someone who wanted to open the dashboard from a phone over Tor had to reach the CLI first. The three DASHBOARD_ONION_* values now reach the dashboard service, and the header renders the URL in full under the machine name, with a copy control and a note when client authorisation is on — an onion that is enabled but not yet provisioned renders as nothing, because an empty row reads as breakage rather than as "Tor is off". No client-auth key material is passed into the container. host_display_addr moves to the new web/header.py beside the onion reader: both are ways IN to the machine rather than readings off it, and views.py was at its recorded file-budget ceiling. The frontend fixture generator patched `views.detect_host_ipv4`, which the move would have left binding a dead attribute while the fixture went machine-dependent in silence; it now patches the function where it is looked up. This contradicts a stance docs/dashboard.md took: the doctor panel redacts the onion "because the address is worth keeping out of" the container. That prose is corrected rather than left standing — the panel still redacts, the header does not, and the doc now says so instead of implying the address is absent from the browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…s, not three The reviewer's finding on PR #1880 is true and I re-derived it at source. The paragraph I added claimed `pithead doctor` prints the Monero, Tari and P2Pool onion addresses and that an operator can run it to read one. It does not, and they cannot: - `06-doctor.sh:295` emits `dr_ok "$k set."` on the success path — the variable name and a verdict, never the value. - The failure path (`:294`) prints `(value: '${onion:-empty}')`, and `onion_missing` (`32-onion-provisioning.sh:66-68`) is `[ -z "$1" ] || [ "$1" == "placeholder" ]`, so the only values it can print are the literals `empty` and `placeholder`. - The remote-node path prints "not needed" and no value at all. - The one onion the report carries in full is the dashboard's own (`06-doctor.sh:304` via `dashboard_onion_status`, `04-status.sh:38-49`). - `doctor --json` (`06-doctor.sh:359-363`) `jq -Rs`-wraps that same text, so it is not a second document with more in it. So the panel's redaction acts on exactly one address here — the one the header now prints — and the honest version of the paragraph says that. The pointer for the three node onions goes to the stack `.env` (`33-render-env.sh:382-384`), which the encrypted backup archive carries (`45-control-backup.sh:104`); the undefined term "emergency kit" is gone with it. Tier: none — prose only, no behaviour change. Green at this head: lint-docs-voice, lint-md, lint-topology, lint-file-budget, lint-operator-strings. Repo-wide phrase sweep for the claim leaves one hit, the corrected sentence. What the operator sees change on the appliance: nothing in the product. The Diagnostics doc no longer sends them to a command that will not answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…y announces itself The design lane's non-author RETURN on PR #1880 (comment 5556336128) was posted at b62d37f and I had not read it — my previous push answered only the other reviewer's finding. Its three asks, addressed here: 1. The note rendered "Get the key withpithead onion-client-key". htm strips a whitespace run CONTAINING A NEWLINE from both ends of every static text chunk, so breaking the line before the <span> deletes the space. `${" "}` is the repo's existing answer at 13 sites in 3 files, because a field is pushed as its own child and never goes through that regex. Design measured the broken render at 1400px and 390px in both themes; the second reviewer derived the same result from the vendored parser's source. 2. The note named a shell command on a product whose first paragraph says nobody has a shell, and `dashboard.onion.client_auth` defaults to true while `dashboard.onion.*` is never committable from the Configuration view — so an appliance shows this note by default to a reader who cannot act on it. The sentence now says who it is for. The reachability gap itself is #1882, not this PR. 3. "Copied" was a status message with no live region (WCAG 4.1.3). The label itself is the status, so `aria-live="polite"` goes on the button. The old tests were green on defect 1: one asserted "Client authorisation is on" and one asserted "onion-client-key", and each matches one side of the join. The new assertion strips tags and pins the whole phrase. Proven to fire: reverting only the `${" "}` reddens it 7/8 and restoring greens it 8/8, with the mutation asserting its own needle so a no-op substitution cannot pass as a control. Green at this head: node --test onionurl 8/8, make test-frontend 574/574, make lint-js (biome 2.5.0, which does lint CSS as well as JS), lint-file-budget. Repo sweep: no doc or Python string quotes the old note wording. What the operator sees change on the appliance: the client-auth line under the onion URL reads as a sentence instead of running two words together, and it now says the command is for a machine they can log in to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…nd says so The prose promised the header to the reader who cannot have it. `dashboard_onion()` reads three environment variables; docker-compose.yml passes them, and the appliance's podman quadlet for the dashboard does not (`36-quadlet-units.sh` writes that unit's `Environment=` line with none of the three; all three rendered `os/quadlet/*dashboard.container` carry zero `onion` matches against a `HOST_IP` control of one, while `tor.container` carries two — so the sweep is not blind to the string). On an appliance the server therefore sends no onion and the block is correctly absent — which is exactly the machine the removed sentences named as the reason the block exists. The code is right and fails in the safe direction; the documentation was wrong, so the documentation moves. The quadlet gap is #1896, filed against the paths that own it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
`aria-live` on the button made the label the status message, and the label never came back down: `copied` was set true and nothing ever set it false, so the region's content stopped changing and a live region with unchanged content announces nothing. The first copy was announced and every copy after it was silent — to exactly the reader the attribute was added for. It also left the control named "Copied": a state, where a returning reader needs the action. The button keeps one name, "Copy address". The confirmation moves to a sibling `role="status"`, which is rendered empty rather than conditionally — a live region inserted with its message already inside it presents no change to announce — and comes down on a timer, so the next copy is a change again. A failed copy clears a standing confirmation and disarms the pending timer, so the previous "Copied" cannot read as this attempt's answer. Proven to fire, each mutation naming its own row: removing the revert timer reddens the clears-itself test; dropping the `clearTimeout` reddens the failed-copy test; putting the confirmation back in the label, or making the status region conditional, reddens the markup test. Restored tree byte-identical to pre-battery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…s not sanitise
CodeQL read the test's `replace(/<[^>]*>/g, '')` as an incomplete multi-character
sanitizer and posted a new high-severity alert, which held the required CodeQL
check red. The string never reaches a DOM — it is read by assert.match — but a
sanitizer-shaped line in a security-reviewed surface is worth not writing.
Same extraction via split/join, plus the half that was undocumented and is load
bearing: this substitutes NOTHING for a tag. The sibling in xvbview.test.mjs
substitutes a space, and a space would rejoin "to,<span>pithead" into readable
text and pass over the very defect the assertion exists to catch.
Proven by a controlled pair, not by the suite going green: with the explicit
${" "} removed from onionurl.mjs:90 the assertion fails on
"log in to,pithead onion-client-key prints it." and the two narrower assertions
above it stay green, which is what the comment claims; restored by sha256 and
10/10 pass again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
1d37e92 to
6e41f28
Compare
Non-author delta re-pass —
|
Author's evidence, not a pass — the head moved by a REBASE ONLY, and the patch is byte-identicalThe three PASS comments on this PR all name What moved, measured (not relayed):
What I did NOT check: that an identical patch on a moved base cannot change BEHAVIOUR through a I am the author and merge nothing here. |
|
Appliance lane, on the "#1896" sentences here ( |
Cross-PR note from a review of pull 1942 — two comments in THIS branch expire when that one merges
Pull 1942 (#1896) puts
The second names its own expiry condition, and pull 1942 is that condition. No code conflict — the two PRs touch disjoint files, and I measured the merged shape rather than assuming it: this branch's compose keys with 1942's renderer runs 37 pass / 0 fail on the quadlet-render fragment. So this is prose going stale, not a merge hazard. Whichever of the two merges SECOND owns the correction, since these two files are this branch's. If it is this one, the sentences can be written in their landed form before merge; if it is 1942, they need a follow-up here. Full derivation, with the three-leg control run behind it, is on pull 1942 ( |
Addresses #1853 — deliberately NOT
Closes.developis now the repository's default branch, so the keyword would fire on merge, and #1853 is the operator's appliance finding: on that machine the header still shows nothing until #1896 wires the quadlet. The issue stays open until #1896 lands, and a human shuts it by hand then.What the operator sees change on the Compose stack
When the dashboard onion is on and provisioned, its
.onionURL sits under the machine name in the header, in full, with a Copy button. With client authorisation on, a line beside it says the address only opens for a browser holding the client key, and where to get the key. When the onion is off — or on but not yet provisioned — there is nothing there at all: no empty row, no placeholder.Before this, the only place the address appeared was a
pithead statusline — a machine's own remote-access address, readable only from a shell on that machine.Not on the appliance yet, and this PR does not change that. The block needs
DASHBOARD_ONION_ENABLED,DASHBOARD_ONION_ADDRESSandDASHBOARD_ONION_CLIENT_AUTHinside the dashboard container.docker-compose.ymlpasses them; the appliance's podman quadlet for the dashboard is written with none of them (lib/pithead/36-quadlet-units.sh), so there the server sends no onion and the header is correctly blank. That is #1896, onlib/pithead/andos/— freeze paths this PR does not touch. This PR's prose said the opposite in five places;cca44e11corrects all five, and this body was the sixth.How
Plumbing.
docker-compose.ymlpassesDASHBOARD_ONION_ENABLED,DASHBOARD_ONION_ADDRESSandDASHBOARD_ONION_CLIENT_AUTHinto the dashboard service. The client keys are not passed in, so the container cannot hand out the credential that opens the onion even if it is fully compromised.New
web/header.py— the header's address block:host_display_addr(moved, unchanged) plusdashboard_onion, which answers{url, client_auth}only when the onion is enabled and provisioned. That pair of conditions mirrors the host's owndashboard_onion_status(lib/pithead/04-status.sh) rather than inventing a second rule: pithead writes the literalplaceholderinto.envbefore the service exists, so "enabled" alone never means "reachable".New
static/onionurl.mjs— renders what the server hands it and infers nothing. The URL is never elided: 56 characters of base32 carry no redundancy, so a tidy truncation produces a string that does not open, and copying it to a phone is the whole product.Three judgement calls, stated so they can be overruled
placeholder, ends in.onion. A strict v3 regex would silently hide a real address the day the format changes, and hiding is the failure that reads as "Tor is broken". A junk string reaching the header is loud and harmless; the reverse is not.header.py, notconfig/config.py. Nothing else consumes these three values,version.pyalready establishes the injectable-envreader for a presentation value, and it lets the tests pass a dict instead of mutating process state.config/config.pyis also at its recorded file-budget ceiling and that ratchet only moves down.host_display_addrmoved out ofviews.py. The budget answer and the design answer agree:views.pywas at its ceiling (445/445), and the host line and the onion under it are both ways in to the machine rather than readings off it. Now 434.A documented stance this contradicts, corrected rather than left standing
docs/dashboard.mdsaid the doctor panel redacts the dashboard onion because "this copy crosses into the dashboard container, and the address is worth keeping out of there". After this change the address is in the container, by the operator's ruling, so that rationale no longer holds for this one value.Corrected again after review — my first replacement traded one false sentence for two. It said the redactor covers "the Monero, Tari and P2Pool hidden services" in that report and that an operator can run
pithead doctorto read one. Neither is true, and I re-derived it at source rather than taking it:lib/pithead/06-doctor.sh:295emitsdr_ok "$k set."for the three node onion variables — the variable name and a verdict, never the value.:294) prints(value: '${onion:-empty}'), andonion_missing(lib/pithead/32-onion-provisioning.sh:66-68) is[ -z "$1" ] || [ "$1" == "placeholder" ], so the only values that branch can print are the literalsemptyandplaceholder.dr_ok "Dashboard onion: $onion_line"(06-doctor.sh:304, viadashboard_onion_status,lib/pithead/04-status.sh:38-49).doctor --json(06-doctor.sh:359-363)jq -Rs-wraps that same text, so it is not a second document with more in it.So the doc now says what is true: the panel's redaction acts on exactly one address, the report names the other three hidden services by setting rather than by address, and the pointer for those three goes to the stack
.env(33-render-env.sh:382-384) that the encrypted backup archive carries (45-control-backup.sh:104) — not to a command that will not answer. The undefined term "emergency kit" is gone with it. A[redacted].onionin that panel is still not evidence the address is absent from the browser, and the doc says so.Worth a ruling from whoever owns the call, and the corrected reading makes it sharper: after this change the doctor panel's onion redaction protects exactly one value, and that value is now published in the header two inches above it. I have not changed the panel — that is a different surface and not this issue — but the inconsistency is deliberate on my side and visible, not overlooked.
The same stale rationale still stands in code at
lib/pithead/46a-control-diagnostics.sh:47-54and its built copypithead:11914-11921. Those are image-freeze paths, so it is not touched here; filed as #1883.Over-engineering pass
Run by hand on this diff (no such command exists on this box).
copyTexttakes the clipboard as an argument instead of reaching fornavigator. That started as a testability concession and is the better design anyway: it is also the honest answer for a page served outside a secure context, wherenavigator.clipboardis undefined and the button has to degrade rather than throw.dashboard.cssis at its recorded ceiling, and the block wants exactly what.brand-hostalready has — the host line's size, spacing andoverflow-wrap: anywhere. Reusing it and the generic.btn-rangeis both the smaller diff and the more consistent header.components.mjs, which has 40 lines of headroom. They did not, because this is the file's one stateful control andcomponents.mjsis documented as pure functions of the payload. If you would rather have it inline, say so — it is a move, not a rewrite.Tests
Tier 1,
node --test— newdashboard/tests/frontend/onionurl.test.mjs, 8 tests: the URL renders whole and in the header block; absent onion renders nothing (with a control that the same fixture does render it when the field is filled); the client-auth note appears and disappears on the boolean alone; no key-shaped field in the payload reaches the page; the copy control is present; andcopyTextanswers honestly — true only when the clipboard took it, false where there is no clipboard and false when it rejects, so the label never claims a copy that did not happen.Tier 1, pytest — new
dashboard/tests/web/test_header.py, 14 tests. Thehost_display_addrcases moved with the function; the onion cases cover enabled/off, theplaceholdercase, blank and non-onion values, casing and padding, the payload's exact key set against an environment that holds key-shaped sentinels, and — the one the rest would not have caught — that the default reader is the process environment, since every other case injects a dict andbuild_statecalls it with no argument.Mutation battery, each proved to have applied, each restored, control run clean:
copyTextclaims a copy that never happenedplaceholderThe "unwire from the header" row is the one that matters: it proves the render tests exercise the wiring and not just the module in isolation.
The fixture generator, and a trap the move would have set
tests/frontend/fixtures/_gen_state.pypatchedviews.detect_host_ipv4. After the move that binds a dead attribute onviewswhile the real lookup runs live — the fixture would have gone machine-dependent with nothing red. It now patches the function where it is looked up, and clears the three onion vars so a box with a provisioned onion regenerates the same fixture.state.jsongains one line ("dashboard_onion": null), added by hand rather than by regeneration. A clean regeneration also reverts three unrelated value-level drifts in the committed fixture (sync.tari.local, a topology edge's route, and two mis-indented lines). Those are outside this issue and reverting them could quietly change what a sibling test means, so I left them alone. The shape guard intest_xvb_views.pycompares key paths only, so it stays green either way.Run locally
node --test dashboard/tests/frontend/*.test.mjs— 552 pass, 0 fail.make test-dashboard— 2558 passed, coverage 97.48% against the 80% gate, on the rebased tree.make lint-js lint-md lint-docs-voice lint-operator-strings lint-topology lint-py, andscripts/lint-file-budget.sh— all pass.make lint-sh(shellcheck is serialized behind another lane's build-machine claim, and no shell file changed), no docker, no KVM, no browser. The rendering is asserted by a string-level render probe, not a browser — the copy button's click handler is never invoked by it, which is whycopyTextis tested as a function.Shared files
docker-compose.ymlanddocs/are shared with other work in flight this week — three added lines in the dashboard service'senvironment:block, and two prose passages. Expect a conflict there and not elsewhere.🤖 Generated with Claude Code
https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR