Restores the last configuration on startup by asking the MODULE for it, not by
keeping a copy. The module already persisted the config it accepted and
reloaded it on load; the missing piece was reading it back, since getConfig()
masks provider URLs and a masked URL cannot repopulate a field. It uses
getConfigUnredacted(). One source of truth, and a config applied from the CLI
shows up in this form too.
The restore runs once, and only once BOTH the network table and the stored
config have arrived — the table is fetched asynchronously and the selector's
model is empty until it lands, so restoring earlier silently dropped the
network. It also sets the selector's appliedNetwork before the index, so the
change handler does not helpfully clear the very root being restored.
Fixes the dead end reported from live use: stop, switch network, and both
buttons are disabled with nothing explaining why. Start was correct to be
disabled — changing network clears the trusted root, because a root names a
block on one chain — but a disabled button explains none of its preconditions.
There is now a line next to it naming the one that is routinely missing.
Adds a marker for when the State panel is describing a different chain from the
one selected, which is the honest state between switching the selector and
pressing Start.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel hardcoded both the network list and a pair of sepolia URLs as field
defaults. Selecting a different chain changed only the name, leaving the
previous chain's endpoints and trusted root in place — a config that cannot
bootstrap, and whose failure arrives long after the change that caused it.
The selector model and the per-chain endpoint defaults now come from the
module's supportedNetworks(). Hardcoding the list here was a latent crash:
`network` reaches a quit() inside the Nim library when upstream does not
recognise it, so a UI list that drifted from the module's whitelist would take
the whole host process down.
Switching chains overwrites both endpoints and clears the trusted root, and
CLEARS the endpoints when a chain has no default rather than leaving the
previous chain's — a stale URL that looks filled in is the failure this is
here to prevent. Configure now also requires both endpoints, since they can
legitimately be empty.
Verified in Basecamp on all three: switching populates each pair, and a full
hoodi run — fetch root, configure, start, call — reports chain 560048,
head 0x3564da, and returns 3499225 against an independent 3499227.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two panel defects surfaced by a live stop / switch-network / start, which
crashed the module (fixed separately in proxy_runtime).
The trusted block root survived a network change. A root names a block on ONE
chain, so carrying it from sepolia to hoodi builds a config that cannot
bootstrap, and whose failure arrives long after the change that caused it. The
network dropdown now clears it and says so.
The panel also has no way to notice a module that stops answering. When the
module went away mid-start, NEITHER the 5s status timeout NOR the 150s start
timeout ever fired — the callbacks were simply never invoked — so `busy` stayed
true and disabled Configure, Start, Stop and Send at once, which is the "stuck"
that was reported. Liveness now comes from elapsed time since the last ANSWERED
status() rather than from any reply, and declaring the module unreachable
clears busy so the panel stays usable. It clears itself if the module answers
again, rather than latching an error it has recovered from.
Scope stated honestly in the code: this covers a module that goes SILENT while
this backend lives. It does NOT cover a module process crash — measured, killing
the module process makes Basecamp tear down ui-host with it, so nothing is left
running to notice, the view keeps its last property values, and only the host
can report it. The real defence there is the module not crashing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running the panel in Basecamp rather than by reading it.
"Fetch finalized" never worked. Basecamp sandboxes ui_qml plugins away from the
network, so the view's XMLHttpRequest was refused outright — "sandboxed ui_qml
modules may not use the network" — and the only sign of it was one line in the
host log plus a silent "root fetch failed: HTTP 0". It now calls the module,
which is the component allowed to make the request; the trusted root it returns
matches the beacon node's finalized root exactly.
Every lifecycle outcome was logged twice: the backend emits both logLine and
configured/startFinished/stopFinished, and the QML appended on both. The
backend owns the wording now and the QML handlers only react.
The "serve on 127.0.0.1" checkbox drew its label in the stock Basic style's
palette colour — near-black on a near-black panel, invisible. It has an
explicit contentItem now.
"degraded" no longer reads as not-running. It means up-but-heartbeat-failing,
so the process is still live and stoppable; the old mapping would have disabled
Stop and re-enabled Start on exactly the proxy an operator most needs to
restart. Health stays visible through the state badge and lastError.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A ui_qml module — QML view over a small C++ backend, with the QML-facing
surface declared as a QtRO .rep contract, matching storage-ui/wallet-ui/chat-ui.
(QML-only was the other option and is rejected: an empty manifest `main`
installs fine and then shows RETRY forever.)
Three things are deliberate rather than incidental:
* State is POLLED, not event-subscribed. A UI plugin's event subscription is
one-shot and is refused if armed before the registry handshake, with no
retry, so a panel built on subscriptions silently shows nothing when it loses
that race. status() is cheap and never touches the proxy thread, so a 2s poll
is both simpler and strictly more robust.
* Every module call goes through the generated *AsyncResult twins. start()
blocks in the module until the light client bootstraps — up to 120s — so a
synchronous call would freeze the UI for that long. Consumer-side timeouts are
set LONGER than the module's own (150s vs 120s for start, 45s vs 30s for
calls) so the module's real error wins rather than a bare transport timeout.
* The form constrains what can be typed. `network` is a dropdown of exactly the
three the library supports, because any other value reaches a quit() inside
Nim and takes the host process down — a text field here would be a footgun
wired to the host's life. keepAlive never preselects "off" and warns when it
is chosen, since without a heartbeat the verified head goes BACKWARDS.
The view avoids Logos.Theme and every other optional import: one unresolved
import takes down the WHOLE view, and the host routes that qWarning somewhere
easy to miss, so the failure looks like an empty pane.
Icon is generated at exactly 256x256 — the .lgx build enforces it — and the
package round-trips with assets/icon.png staged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>