| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The relay forwards the node's fragments untouched, and those begin at zero
at the seek point. The player never notices because its SourceBuffer is given
`timestampOffset = start`; a receiver has no equivalent, so the cues are
shifted by `-start` before they leave, recomputed at every restart of the
relay. Sent as they are, a subtitle would be out by the whole seek.
The document is served from the relay's own port at /subs.vtt, behind the same
token as the stream and with CORS: a receiver fetches a side-loaded track with
XHR from its own origin, and without the headers it fails as a network error
with nothing on screen to say so. The URL carries a version because a track is
cached by address — changing the cues behind a fixed URL leaves the previous
language showing.
Cues that end before the stream begins are dropped rather than clamped, so a
line from before the seek cannot appear over the first frames after it.
The relay is plain Node, so the tests start it and fetch from it rather than
reading its source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
MeshBay Light has no bundled meshbay-node.exe, so the create-group wizard
(which assumes it can start a local node) needs its own signal, not just
platform.node.available. main.js exposes it over IPC (node:bundled) by
checking the packaged resources directory rather than trusting a build-time
constant; preload.js and platform.js carry it through the usual
contextBridge/wrapper path.
winCanElevateServiceMode() replaces the two prior 'app.isPackaged' checks
for whether the app can offer service-mode elevation -- Light is packaged
but has no service-mode.ps1 to elevate into, so packaged alone was already
the wrong test even before this target existed.
create-group-page.js gates the wizard step that starts a node on the new
capability instead of hiding the whole feature; node-page.js's comment fix
is unrelated cosmetic drift caught in the same pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
ensureTray() was reachable only from the window:minimize-to-tray handler, so
the indicator did not exist until you had already hidden the window into it.
That is backwards on both desktops — most of what a tray is for is finding an
application that is not in front of you — and on Windows it read as the app
having no tray presence at all.
Created during app.whenReady(), after registerBridge() and before
createWindow(). The order matters: buildTrayMenu reads the nodeService that
registerBridge assigns, so the other way round puts the Start/Stop entry on
the menu one five-second poll late.
The menu's labels were the one thing that came *from* the minimise call, since
the main process has no i18n. A new tray:labels IPC (platform.setTrayLabels)
carries them instead, sent from the renderer's boot once initLocale() has a
catalogue; a language change reloads the page, so the same call covers it. The
window between launch and that first message shows TRAY_FALLBACK, in English.
§5.10's two platform gates become one — trayOS() in main.js, which every tray
path calls. test_desktop_shell.py's existing test is rewritten against it and
two are added: the launch ordering, and that no tray path tests
process.platform inline instead of calling the gate.
Windows still files a new tray icon under hidden icons until the person drags
it onto the taskbar. No API promotes it; documented in WINDOWS-PORT.md §5.11
rather than worked around.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The tray's Start/Stop already drove nodeService.status/stop/restart, which
had full win32 branches for both startup modes from the Node page work --
so enabling it on Windows is widening two platform gates (the `tray`
capability in preload.js, the window:minimize-to-tray handler in main.js),
not new logic.
Replaced the tray icon: the previous white chevron-in-a-box read as an
envelope at tray size. New icon is a small "M" drawn as mesh nodes and
edges, echoing the app icon's own motif, in the brand blue instead of
plain white so it stays legible on both light and dark taskbars/panels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A dedicated monochrome button in the nav, immediately left of the notification
bell, hides the window to a tray indicator. Linux only for now; Windows is
being done on that OS, and the capability is declared per platform so the
button never appears where the desktop shows no indicator -- there it would
hide the window for good.
Hides, never closes: `window-all-closed` quits the app, so closing here would
make "minimise" mean "exit" and drop the session, the transfers and the node
connection. `second-instance` now calls the same restore path, since focusing
a hidden window does nothing visible.
The context menu is not decoration. Under libappindicator -- how GNOME shows a
tray at all, via the AppIndicator extension -- `tray.on('click')` never fires;
the indicator only opens its menu. A tray whose sole affordance was a click
would be inert on the one desktop this targets. The click handler is kept for
desktops that do send it.
Menu labels come from the renderer with the IPC call: the locale files are the
interface's, the main process has no i18n, and a second string table is how two
of them start disagreeing. English fallbacks if none arrive.
The icon lives in src/, not build/: package.json `files` packages only `src/**`
and `ui/**`, so an icon under build/ is present in a dev run and missing from
every installed one. Monochrome, stroked, matching the nav glyph.
Verified on this host: Ubuntu GNOME with ubuntu-appindicators@ubuntu.com and
libayatana-appindicator3 present, so the indicator has somewhere to appear.
Not yet run end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The installer's own mode question is effectively one-shot: customInstall
skips it entirely once the firewall rules already exist, for any reason --
and per-user mode sets those up on its own, with no Scheduled Task involved.
So declining once (or the rules existing from something unrelated, as
happened on a dev machine this session) was a dead end: no reinstall, repair,
or uninstall/reinstall cycle could ever bring the question back, since
uninstall defaults to leaving both alone.
Add the other door in (and out): a checkbox on the Node page, next to the
existing per-user autostart toggle, wired main.js -> preload.js -> platform.js
-> node-page.js. It runs packaging/win/service-mode.ps1 -- the exact script
installer.nsh already runs -- via one Start-Process -Verb RunAs elevation, so
the two paths can never disagree about what service mode means. The elevation
helper writes a tiny param()-based .ps1 to %TEMP% so the target script path
and its arguments bind through real PowerShell parameters instead of nested
string-quoting.
Also fixes a real pre-existing gap found while checking this: 8 of the 10
locale catalogues (all but en/fr) were missing the autostart/service-mode
keys added in an earlier commit this session (b782886) -- test_locales.py's
key-set-parity check uses a for-loop with an inline assert, so it stopped at
the first mismatch (fr) and never actually reached the other eight. Backfilled
all five keys (three pre-existing, two new) in de/es/it/ja/nl/pl/pt-BR/zh-CN.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Linux node runs under `systemctl --user`. Windows has no per-user
equivalent that works without elevation: `schtasks /create /sc ONLOGON`
(even `/rl LIMITED /it`) fails with "Access is denied" for a non-admin
user, because a logon trigger touches machine-wide scheduler state.
So autostart is a `.vbs` in the per-user Startup folder instead:
CreateObject("WScript.Shell").Run Chr(34) & "<exe>" & Chr(34), 0, False
wscript runs it at every sign-in, hidden (0) and non-blocking. No admin,
no console window, no new dependency. Verified end to end: the launcher
brings the daemon up with no window and it answers its loopback API.
node/platform.py
autostart_install/remove/status — write / delete / detect the launcher
autostart_run/end — start now (DETACHED|NO_WINDOW) / taskkill
_node_exe — PATH, then next to sys.executable, then argv[0]
node/daemon.py
new `autostart install|remove|start|stop|status` verb
reload (win32) -> POST /api/reload on the loopback API
restart-daemon (win32) -> autostart_end + autostart_run
reset (win32) -> also removes the launcher
client/main.js, preload.js
node:autostart handler + winAutostart* helpers (kept in step with platform.py)
node:service-status (win32) probes the daemon; stop/restart/start use
taskkill + a detached, windowless spawn
Tests: 8 autostart cases in test_platform.py (mocked sys.platform, APPDATA
pointed at tmp); `autostart status` added to the CLI dispatch sweep. Full
meshbay-node suite green on Windows (784 passed / 34 skipped).
Still open: no CTRL_CLOSE_EVENT handler, so a bare taskkill / window close
does not run _shutdown() (SetConsoleCtrlHandler, follow-up). Service mode
(pywin32/NSSM) stays Phase 2.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Chromium's P2P socket manager failed every STUN hostname with
ERR_NAME_NOT_RESOLVED in a restricted-resolver environment (a Windows KVM
guest), even though its own general network stack, the OS resolver and
Node's resolver all resolved the same names -- and mapping the names to IPs
with --host-resolver-rules changed nothing, so it is not ordinary
resolution. WebRTC was left with no server-reflexive candidate.
The desktop client now resolves the STUN hostnames in the main process
(`ice:resolve-stun`, Node's dns.resolve4) and hands `transport.js` the
IP form; a name that will not resolve (the decommissioned Mozilla host) is
dropped. In a browser there is no `meshbay` bridge and the hostnames are
used unchanged -- a browser resolves them fine, so that path is untouched.
Falls back to the hostname form if the bridge call throws.
Also, scoped to win32: disable WebRtcHideLocalIpsWithMdns, so the client
publishes its real local IP instead of a `.local` name the node's ICE stack
cannot resolve across the KVM bridge. Changes nothing on Linux/macOS.
Test-env workaround, revisit before release.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add a status panel at the top of the Node page — always visible, even
before an MNP connection exists — showing the meshbay-node systemd
unit's own state (via `systemctl --user show`, main process only) with
Start/Stop/Restart controls. This is the piece the rest of the page
cannot provide: it has to work while the daemon is stopped or crash-
looping, which the MNP-based sections require the daemon to already
answer.
While touching node lifecycle: `reload` and `restart-daemon` in the
CLI shelled out to pgrep + SIGTERM/SIGHUP and respawned the process by
hand, logging to a hardcoded /tmp path. That pattern already SIGHUPed
a developer's own running node by accident once (see the old
test_cli_dispatch.py comment). Both now delegate to
`systemctl --user reload|restart meshbay-node`, which the unit already
supports correctly (ExecReload=, Restart=on-failure).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
|
| |
|
|
|
|
|
|
|
|
|
|
| |
When the wizard calls node:start with {hubUrl, username}, the handler
writes a minimal node.toml and a random unlock.key if they don't exist.
The daemon then creates the keystore on first start using the unlock
file — fully non-interactive.
Existing configs are left untouched: if node.toml or unlock.key already
exist, provisioning is skipped and the node starts as before.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Desktop client guides new users through the full onboarding sequence
without a terminal: detect local node → start daemon (systemd with
direct-start fallback for dev) → link node key to hub → create group →
attach directories → gek-init → operator pair.
- node:detect returns configured field to distinguish missing vs stopped
- node:start tries systemctl first, checks is-active, falls back to
spawning the binary directly when the unit crashes (dev mode)
- probeNode() extracts the shared config→token→fetch pattern
- SetupWelcome on empty HomePage links to /create-group
- CreateGroupWizard step 0 handles node detection and start
- platform.js exposes node.installed() and node.start()
- 12 setup.* i18n keys added to all 10 locales
- Integration test for node:start fallback logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Re-serve decrypted fMP4 video over HTTP on the LAN so a Chromecast can
play the stream. The relay runs in the Electron main process — same trust
boundary as downloads and MSE playback.
- cast-relay.js: HTTP server with BoxAccumulator (reassembles WebRTC
chunks into moof+mdat pairs), ring buffer, backpressure, finish() for
clean end-of-stream, fixed port range 19550-19553
- cast-chromecast.js: mDNS discovery (bonjour-service) + CASTV2 protocol
(castv2-client), connect/reload/disconnect lifecycle
- Seek-aware: relay restarts on every seek, Chromecast reloads new URL;
generation counter prevents stale async errors from killing active
restarts; landingPlayheadRef suppresses programmatic seeking events
- Device picker in video top bar with scan, device selection, copy-URL
fallback, and cast status indicator
- IPC bridge (main/preload/platform) for start/push/stop/finish/status/
discover/chromecastConnect/chromecastReload/chromecastDisconnect
- Phase 3 design doc for DLNA/Smart TV in docs/cast-smart-tv.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Create Group wizard (Electron-only) consolidates 6 steps across 4 interfaces
into a single multi-step page: group creation on hub, node attachment, root
selection via folder picker, GEK initialization, and auto-pairing — all in one
flow. Browser SPA keeps its current behavior unchanged.
Public group support (Option A — GEK for all groups):
- All groups have GEK regardless of visibility; open-join groups auto-admit
via TOFU when join_policy is "open"
- Key rotation blocked for public groups (API guard + UI hidden)
- Hub signaling allows WebRTC offers for nodes hosting open-join groups even
when the caller isn't a member yet
- attach_group writes join_policy to node.toml
- Daemon loads GEK for all groups, not just private ones
- Known-device path in join_request now auto-admits to open-join groups
Node loopback API bridge (Electron IPC):
- node:detect, node:call, node:pairing-code IPC handlers in main process
- Renderer never sees tokens, paths, or keys (session token = physical access)
- platform.js node namespace for UI consumption
- Loopback endpoints: roots CRUD, member-upload toggle, reload
Bug fixes:
- Root change detection: removed premature ctx["roots"] updates from add_root
and remove_root that prevented indexer retarget on reload
- Duplicate offline message: global fallback now gated on !group
- Signaling membership check: fallback to open-join groups for non-members
Sidebar groups sorted by last_activity_at (most recent first):
- New Group.last_activity_at column with Alembic migration
- POST /v1/groups/{id}/activity endpoint, called on connect and chat send
- Client-side sort + throttled hub updates (1/min)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Node admin panel (NodePage) now covers every CLI operation over MNP:
group attach/detach, roster, member unpin, GEK rotate, denylist, reload.
Daemon hot-loads new groups and tears down removed ones on config reload
instead of requiring a full restart. Group attach/detach via MNP or
local API triggers an automatic reload so the group is live immediately.
Fixed GroupPage hang on first visit to a newly created group: the JWT
issued at login didn't include the new group, the node rejected with
not_a_member, and the token-refresh path returned without re-triggering
the connect effect (Boolean(token) didn't change). Now bumps retryKey
after a successful refresh so the effect re-runs with the fresh token.
NodePage marks groups hosted by the node but absent from the hub with a
"not on hub" badge so stale groups are visible and easy to remove.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Hub/UI:
- Icon-only group tabs (chat, files, settings) with per-group default tab
- Transfer widget: filename becomes a clickable link to open completed downloads
- Pulse animation on transfer icon (pale→dark green) while active
- Download button feedback in FilePreview (spinner, auto-reset)
- Group mute toggle persists across navigation
- Login page autofocus, chat refocus after send
- Theme toggle closes menu, status badge and duplicate connecting removed
- Create-folder restricted to operators, download-path note removed
- User preferences API (CRUD) with Alembic migration
- Profile: email display/edit via PATCH /v1/users/me
- Settings: "Defaults" section for default tab selector
- All 10 locale files updated
Node:
- upload_dir in node.toml: separate filesystem path for uploads
- Root.direct flag: uploads land at root path, no subdirectory
- CLI --upload-dir flag on `group add`
- Admin UI accepts upload_dir
Client (Electron):
- shell.openPath bridge for opening completed downloads
- platform.js passes open callback from native save
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
**Downloads were going through RAM.** `_openDownloadTarget` tries a granted
folder, then a service worker, then its floor: collect the whole file in the
page and hand the browser a blob. Both of the first two are absent in the
desktop application — `showDirectoryPicker` does not exist, and Chromium refuses
a service worker on a custom scheme — so every download under 512 MB took the
floor. A gigabyte of film meant a gigabyte of RAM, and the only visible symptom
was a Save As dialog at the *end* rather than the start, which is what the
operator noticed and asked about.
The main process now streams to disk: it honours "save automatically" with a
folder chosen once and no dialog, never overwrites (a colliding name gets a
suffix), awaits each write so the renderer cannot outrun the disk and queue the
file in memory anyway, and unlinks a cancelled download rather than leaving a
truncated file that looks complete to whoever opens it next. Settings now offers
the native folder picker instead of saying downloads are unsupported.
Measured in the running application: the file on disk grows 256 KB → 512 KB →
768 KB → 1 MB as the chunks arrive, and an aborted download leaves nothing
behind.
**A permanent scrollbar on sign-in.** `.layout` and `.page-center` each reserved
`100vh - 52px`, and `.page-center` sits inside `main`'s 24px vertical padding —
so the page overflowed by exactly 48px at every window size. Found by measuring
in the app rather than reading the stylesheet: `scrollHeight` 819 against a 771
viewport, then the bottom edge of every element. The centring page brings its
own padding, so main's is dropped for it and the duplicated arithmetic goes
rather than growing a third term. Now `scrollHeight == innerHeight`, no
overflowing elements.
**The first-run screen was unstyled.** It used a class name I invented
(`auth-page`) that appears nowhere in the stylesheet, so it had no card and the
button sat against the input. It now uses the same `page-center` + `login-card`
markup as sign-in, which is where the 12px gap comes from. The sign-in link in
the nav is hidden until a hub is chosen — it led to a page that could not work.
809 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
D4. The passphrase stays the account's credential and its only recovery path;
what changes is that it is not asked for on every launch.
**The renderer never holds the device key.** It is generated, stored and used
entirely in the main process, which signs `meshbay:user_auth:<username>:<ts>` on
request. Same rule as the save dialog, for the same reason: the renderer is the
part of this application that parses decrypted content from nodes, which is
attacker-controlled input. And this key is *not* a per-node identity key — those
are generated per node and never leave that relationship, so nothing here
correlates a person across operators.
First run asks which hub, with no default. A client that picks its own hub is a
client that can be pointed at one, and the address is the whole of what this
application trusts a hub for — the interface comes from the package.
Verified against a hub running this code, not against the deployed one:
register 201 → passphrase login 200 → device register 201 → **device sign-in 200
with a real session** → `/v1/users/me` 200 → a stranger's key 401. The
signature was also checked directly against the hub's own Python verifier before
any of that.
Inside the running application, over the debugging protocol: the bridge reaches
the main process, the renderer calls the hub **through it** (200 — the CORS fix
working end to end), and a call to a host that is not the configured hub is
refused.
**Not verified:** safeStorage persisting the key. This session has no secret
service, and standing one up in xvfb did not succeed. The application behaves
correctly there — it *refuses* rather than storing unprotected, and now says so
in Settings, which is a real case rather than a hypothetical one since it is
exactly what a headless or minimal desktop looks like.
Worth remembering for next time: meshbay.org runs whatever was last deployed. It
answered 405 on the Stage-C endpoints and reported MNP 0.2 while the tree had
0.3, so a local `uvicorn meshbay_hub.app:create_app --factory` on SQLite is what
tests hub changes. Nothing was deployed to production for this.
799 tests pass; e2e.py passes end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Electron 42 / Chromium 148, launched under xvfb. The packaged interface mounts
over `app://` with a secure context, `crypto.subtle` present, Argon2 WASM
loaded, and no console errors. Three statements in the design were wrong, and
only launching it found them.
**A CSP in a `<meta>` tag silently drops `frame-ancestors`.** Chromium says so
in the console. A policy carrying a directive that does nothing is worse than
one without it, so the policy is sent as a header by the protocol handler —
which is also the only thing serving the interface, so one source instead of
two.
**`secure: true` is not what makes the service worker register.** Chromium
refuses a worker on a custom scheme whatever its privileges: "The URL protocol
of the current origin ('app://meshbay') is not supported". The application has
no service worker and needs none — it saves through a native dialog, which is
the better of the two paths. `sw.js` stays in the package because the same files
serve the browser, where it is one of only three ways to write a large file.
What `secure: true` is actually for was measured at the same time: without it
**the whole of `crypto.subtle` is undefined**. The first probe loaded a `data:`
URL and every algorithm failed with TypeError, AES-GCM included — which is why
the probe was rewritten before believing its answer. X25519 and Ed25519 are both
present on Chromium 148, settling the version floor left open as O6.
**The renderer cannot call the hub.** Its origin is `app://meshbay` and CORS
refuses it. The hub has *no CORS middleware at all* — its API is reachable from
no web origin whatever — and that is worth keeping. Widening it for
`app://meshbay` would be worse than it looks: that origin is not a credential,
since any Electron application can claim the same scheme and host name.
So every hub call leaves from the main process, exactly as saving a file does,
and it refuses any origin that is not the hub the user signed in to.
`platform.apiFetch()` is `fetch` in a browser and the bridge in the application,
so no caller has to know which it got. `transport.js` reaches it through a
global because it is a classic script, not a module — the alternative was a
second fetch path, which is how two callers of one hub start disagreeing about
how to reach it.
Verified from inside Electron: the main process gets 200 from
/v1/hub/version, the renderer is refused by CORS, and **a script served by the
hub is refused by the policy** — T3's mitigation demonstrated rather than
asserted.
Build note, written into the README because it will bite the next person:
**Ubuntu 24.04's nodejs 18 cannot install Electron at all** — the download
script `require()`s an ESM module, which Node gained in 22. Node 24 LTS,
checksum-verified against nodejs.org, is what this was built with.
package-lock.json is committed; builds use `npm ci`, not `npm install`.
799 tests pass, e2e.py still passes end to end. The session harness needed a
platform stub: it lifts `hubFetch` out of app.js as text and runs it, so the
adapter is now part of the environment it models.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
Stage D, and the honest half of it.
D1 — the seam (done, and verified)
----------------------------------
`static/platform.js`. `HUB` becomes `platform.hubBase()` and the transport is
built with the same base, so one address has one source. In a browser it returns
'' and every path stays relative to the origin that served the page — the
acceptance criterion for this split was "the browser SPA behaves identically",
and it does. `platform.js` joins `_ASSETS`, or a change to it would not move the
content hash and a cached browser would never ask for it.
D2 — the shell (written, never launched)
-----------------------------------------
**There is no npm on this machine. Electron was never installed and
`packages/meshbay-client/` has not been run once.** That is stated here rather
than discovered later.
What is there: a main process serving the packaged interface over a privileged
`app://` scheme (`secure` and `standard` are not cosmetic — without them the
service worker refuses to register and streamed downloads break silently), a
preload exposing an enumerated bridge that never passes a filesystem path, a
window with `sandbox`, `contextIsolation` and no node integration, navigation
away from the package refused, and a CSP where the hub is reachable over
connect-src and is not a script source. The hub address arrives as a process
argument because `platform.hubBase()` runs before anything can await.
`test_desktop_shell.py` pins each of those by reading the source — the treatment
`test_downloads.py` already gives the three browser save paths. It catches a
property being removed and proves nothing about the application running. Two
were checked by breaking them.
The interface is *copied* into the package by `build/sync-ui.js` from the hub's
static directory, and `ui/` is gitignored: a silent fork is the only real way to
end up maintaining the interface twice.
D3 — partial
------------
The bridge, and the part worth having now: safeStorage's backend is reported
rather than assumed. On Linux it falls back to a fixed key when no keyring is
running, silently — someone who believes the OS is holding their keys is told
when it is not. The native key lifecycle belongs with D4 and needs a running
application to mean anything.
D8 — partial, and a real defect found
--------------------------------------
`meshbay-node.spec` installed the SYSTEM template — the one carrying `User=%i` —
into `%{_userunitdir}`. A user unit already runs as its owner and cannot carry
`User=`; systemd refuses the file, so the packaged unit could never have
started. Nothing noticed because nobody had built and installed the RPM.
Two units now: the template to `%{_unitdir}`, and a new `meshbay-node-user.service`
that a person enables themselves without a password — which is what lets the
desktop client install a node without asking for one. It carries ExecReload, so
`meshbay-node reload` does not have to stop a service somebody is streaming from,
and documents the drop-in for a drive outside the home, RequiresMountsFor
included.
798 tests pass; e2e.py still passes end to end. Nothing here was built or
launched: no npm, no rpmbuild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|