aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
Commit message (Collapse)AuthorAgeFilesLines
* chore: bump version to 0.20.0Christophe Besson21 hours1-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(packaging): setup ends a command-line client run from its installChristophe Besson21 hours2-4/+29
| | | | | | | | | | | | | | | A `meshbay-cli chat --follow` left open holds node-runtime's DLLs, so the previous version's uninstaller failed ("Failed to uninstall old application files"), a silent update included, and left the node it had stopped not running. stop-node.ps1 now ends a meshbay-cli.exe run from this install (-InstallDir, setup's $INSTDIR), and no other copy. Found through CIM, not Get-Process: setup runs the 32-bit PowerShell, which reads no path for a 64-bit process, and the first version of the stop matched nothing. The test runs the script in that PowerShell. Checked live: an update over a running follower now ends it, replaces every file and restarts the node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(auth): the recovery key never reaches the hubChristophe Besson25 hours1-1/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Ticking "also e-mail it to me" at registration sent the recovery key R in the register body, and the hub appended it to the verification e-mail: the hub saw R in clear on its way out, and the resend door — no captcha — let anyone who knew a pending username and address have the hub mail a "recovery key" of their choosing. The box is gone. The recovery screen offers "E-mail it to myself", a mailto: link that opens the person's own mail application with the key filled in; the desktop shell hands mailto: to the system (http, https and mailto only) and the Android shell to a mail application (ACTION_SENDTO). The CLI drops --email-recovery-key and prints the key, as before. The hub mails the code alone, and refuses a register that still carries a recovery_key rather than dropping it, so an older client offering the option cannot tell someone their key was sent when it was not. A mailbox holding R remains, with the reset code that reaches the same mailbox, enough to recover the account's identities; the link says so, and it is the person's choice. Checked in Chrome and Firefox (headless, the real page against a stubbed hub) and by the CLI's end-to-end registration against a local hub. The Android change was not built here, and the desktop link was not clicked in the application. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): one account, several linked nodesChristophe Besson29 hours1-22/+11
| | | | | | | | | | | | | | | | | | | | | | | | | An account links up to MAX_NODES_PER_ACCOUNT node keys (node_keys table) instead of the one users.pk_node_ed25519; the migration carries each account's key over and the downgrade keeps the last one linked. /v1/nodes/auth accepts any linked key, and its token names the key that won it (nk). PUT /me/node_key adds a key, idempotently, 409 past the limit; DELETE /me/node_key names the key: a browser session any of its own, a node only its own. Unlinking a key closes that node's socket and refuses its tokens at once, on REST and on the socket; the account's other nodes are untouched. /pubkeys no longer gives a node key to other accounts, and gives the caller its last linked one, which installed desktop apps read. The node reports waiting_for_node_key while its key is unlinked and signs in again as soon as the hub says so, so a key linked again is back within seconds; unlinking itself signs in first to hold a token naming its key. Profile -> Linked nodes lists each node (connected, last seen, IP, linked since, This machine in the desktop app) with Copy and Unlink, and adds a key by hand. The Node page banner, Create group and the desktop app add this machine's key instead of replacing another's. Messages that pointed at Settings -> Link Node point at Profile -> Linked nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): meshbay-cli, a command-line client for membersChristophe Besson41 hours2-4/+17
| | | | | | | | | | | | | | | | Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): replace reCAPTCHA with a self-hosted proof of workChristophe Besson2 days1-18/+5
| | | | | | | | | | | | | | | | | Sign-up and passphrase reset sent every visitor's address and browser details to Google, which needed a consent the sign-up form could not ask for freely. The hub now issues its own challenge (ALTCHA's scheme, signed with a key derived from the hub's private key, single-use, 30 minutes) and the page solves it with SubtleCrypto while the form is filled in. - GET /v1/users/captcha issues a challenge; 404 captcha_disabled when off. - [captcha] enabled / max_number replace site_key / secret_key, which are now ignored with a warning; MESHBAY_CAPTCHA_ENABLED replaces the old variables. - No Google origin left in the CSP of the hub, the desktop shell or the Android shell; captcha_site_key leaves /v1/hub/info. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: let the operator pause and resume a root's indexing (MNP 6.2)Christophe Besson5 days2-1/+3
| | | | | | | | | | | | | | | A whole-root walk now checks its root before every file: removed or ejected, it stops instead of reading the drive to the end; paused, it finishes from the hash cache alone. A paused root is not watched, not swept by reconcile and survives a restart (roster), its indexed files stay served. Resume runs the reconcile sweep over the root, so entries and their enrichment are kept. Signed root_pause / root_resume over MNP, loopback routes, CLI `root pause|resume`, a pause button on the indexing dock and pause/resume in the roots table of Group settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): say when the account's node is elsewhere, and let go of itChristophe Besson6 days1-1/+4
| | | | | | | | | | | | | | | | | | | | | | Signing in on a desktop links this machine's node to the account, but never over a key already linked (that would cut off the user's other machine) nor a node set up for another account. On a real install both left the node reading "Running" while the hub refused it in a loop, and nothing said why. And the only way to unlink was the linked machine's own Node page, of no use once that machine is gone. - The Node page works out, from the node and the hub each time it looks, whether this node can serve the signed-in account (nodeLinkProblem), and says why not: "Link this node instead" (asked first) puts this node's key on the account; "Use this node for my account" switches a node set up for another account through node:start, which takeOver now lets past a node that answers "running". The first version went through node:start for both, and clicking it on a real install did nothing: a node signed in before the account was linked elsewhere answers "running". - The Profile page unlinks the account's node (DELETE /v1/users/me/node_key), from any machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(packaging): the Store package declares what the NSIS scripts doChristophe Besson6 days3-12/+157
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A test-signed install of the MSIX build, in the WindowsApps folder a Store install uses, showed that every script-made piece of the NSIS model breaks there, because each names the install folder and every update deletes it: the firewall rules went stale, the PATH entries piled up pointing at deleted folders, and the Startup-folder .vbs was refused ("Permission denied") right after sign-in. The network capabilities the manifest declared covered nothing: they make rules for sandboxed apps only, and a listener in the package still got the Windows firewall prompt. The package's own startup task was on by default, started the node whatever mode the Node page said, and ran the console executable, whose window stopped the node when closed. The package now declares what Windows then creates at install, carries across updates and removes with the app, all without an administrator prompt (each measured on the real install, through an update and a reboot): - firewall rules for the node, in a custom manifest template, since only a package-level element can hold them; - the startup task, off by default, running meshbay-nodew.exe, a new build of the daemon without a console; - an execution alias for meshbay-node.exe, so the app adds no PATH entry. The node's CLI switches the startup task (platform.startup_task, ctypes over the WinRT ABI): Windows gives the package's identity to the executables in it, not to a powershell.exe the app starts, which got "Element not found". `meshbay-node autostart install | remove | status` therefore works in the Store package from the app and a terminal alike; the app caches the answer, since the Node page polls. Starting at boot stays the .exe installer's: the Store package offers no service mode, and the CLI refuses `service install` there. Process listings count both image names. The Node page's status poll cleared the message of a refused action within five seconds; the two errors are kept apart now (all Windows builds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: let the operator purge a group's chat (MNP 6.1)Christophe Besson7 days1-1/+1
| | | | | | | | Signed chat_purge from the Chat settings deletes every stored message; epoch keys and attachments stay. The ack is broadcast so open chat panels empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.19.0Christophe Besson9 days1-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): stop the node at Quit in "only while open", whoever started itChristophe Besson9 days1-6/+25
| | | | | | | | | | | | | | | | | | | Switching from "at sign-in" to "only while MeshBay is open" left the node the sign-in launcher had started running after Quit: only a node this process had started was stopped. In that mode the app owns the node, so Quit stops the one that is there. The start with the app and the sign-in's own start (ensureNode) also both ran `autostart start` at launch -- three meshbay-node.exe were seen racing for the port. The sign-in's start and node:start now wait for the launch's. The end-to-end test covers the mode: Quit leaves no node, opening the app starts one. It launches the app with the environment it was imported with: the suite's conftest points HOME, USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory per test, and the app started under that crashed at once (0x80000003), which first looked like a crash of the app itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: set the Windows node up at sign-in, and stop it for realChristophe Besson9 days2-11/+115
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found by the first Windows beta tester, then reproduced on a clean install. After a service-mode install nothing set the node up for the account that signed in: the boot task started a node that quit ("hub.username not set"), and the sidebar showed Node / Create group only once the hub held a node key. The only way to the wizard that provisions was the home page's welcome card, which an account already in a group never sees. The way out was `meshbay-node init` and the key pasted on the profile page -- which is also what PACKAGING-GUIDE.md told people to do. - main.js `node:ensure`, called by app.js at sign-in: provisions, starts and links the node this build ships (Windows, bundled node only). A node set up for another account, or an account linked to another node, is left alone. node:start waits for it, so the two never race. - The sidebar shows the Node section when a node exists on this machine. - The Node page's status is the node's: its control API and the process list, not the service task's state (a node started from a terminal ran while the page said Stopped). Stop says Stopped only once no meshbay-node.exe is left, and stays offered for a process that answers nothing. - CLI stop kills the pid that answered when a graceful stop does not finish, and fails with the reason when a node process is still there. - The daemon ends its process 3s after _shutdown(): Python's exit waited for a busy indexer thread, with the control API already closed. Armed by main() only, never by a daemon run inside a test. - node.toml is read as utf-8-sig (PowerShell 5.1 writes a BOM), and a config that cannot be read is logged instead of dying silently in service mode. - "Pair this browser" queues the code for the next group of this node to open instead of saying "Paired successfully"; no banner before a group. - test_e2e_windows_app.py (opt-in, MESHBAY_WIN_E2E=1) drives the installed app against a throwaway hub: fresh account to linked node, Stop, Start, Restart, checked against the real processes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): remove firewall rules and boot task on uninstall, unaskedChristophe Besson9 days2-19/+24
| | | | | | | The Yes/No before it defaulted to No, so they stayed behind. Read both unelevated and raise the UAC prompt only when one is left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: read the node token per call and detect before polling the index dockChristophe Besson11 days1-11/+14
| | | | | | | | | | | | | node:op only knew the daemon's token once a page had called detect(), and kept it after the daemon replaced it on restart: the index dock stayed empty on a node machine until the Node page was opened, every operation answered 401 after a node restart, and on a machine without a node each 30 s poll was a rejected IPC call Electron printed to the terminal ("Node not detected"). node:op now reads the config and token from disk on every call, and the dock asks detect() before its first operation and after any failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: license MeshBay — LGPL protocol layer, AGPL for the restChristophe Besson11 days4-0/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | The protocol layer is LGPL-3.0-or-later in every language it exists in, so any client may use it whatever its own licence: meshbay-common, and the files marked with an SPDX line — keyderive.js, crypto.js, playlist-crypto.js, transport*.js; keyring.js, transcripts.js and argon2-wasm.js on the desktop; Kdf.kt, Keyring.kt and Transcripts.kt on Android. Everything else is AGPL-3.0-or-later, which the RPM specs and package.json already declared without a licence file to back them. Two AGPL section 7 permissions: - group applications may be under any licence when they use the interface only through a named surface (static/licenses/APPLICATION-EXCEPTION.txt); the reference application is 0BSD so that copying it brings no AGPL code; - the Android application may be conveyed linked with Google Play services. Third-party code is accounted for: THIRD-PARTY-NOTICES.txt is generated from what a build ships (packaging/third_party_notices.py) for the deb/rpm venv and the frozen Windows node — PyAV's wheel grafts in libx264 and libx265, which its BSD licence does not mention — and the vendored browser libraries get their licence texts and htm-preact.js its provenance. Wheels carry SPDX metadata, RPMs %license, debs a DEP-5 copyright file, every Windows target LICENSE.txt. test_licensing.py holds the line: the LGPL layer imports nothing under the AGPL, the reference application nothing outside the application interface, and every SPDX line is one of the known ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cast): music on the TV, the music bar as its remoteChristophe Besson12 days4-55/+191
| | | | | | | | | | | | | | | | | A cast button in Music's toolbar and in the music bar. With a television chosen, each decrypted track goes to the relay with its cover — found as the album card finds it — and plays there as music with its title, artist and album; the bar's play, pause, seek, previous and next drive the receiver, its clock is the receiver's, and the end of a track there moves the queue on. A film or a photo taking the television pauses the bar; stopping the cast carries the track on locally. Photos and tracks now share one path: a whole file sent to the relay in pieces (binary frames on Android, written to disk there), served at /file with byte ranges and its cover at /cover, and loaded as what the relay says it is. cast:image is gone; cast:chromecast:seek is new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cast): photos on the TV, and a television chosen once for the sessionChristophe Besson12 days4-10/+85
| | | | | | | | | | | | | | A cast button in Videos' toolbar, at the top of Photos, in an album's bar and in the lightbox, in a group and in Search alike. A television chosen there is kept for the session: a film opened plays on it with the player as its remote from the start, and a photo opened in the lightbox is shown on it, scaled to 1920x1080, upright, as JPEG. The lightbox gains a slideshow. The relay serves one photo at /image behind the stream's token, on the desktop and on Android; the shell, not the page, decides that the receiver loads it as a picture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): music keeps playing with the screen offChristophe Besson12 days2-2/+20
| | | | | | | | While a track plays, the page asks the shell to stay awake (playback:keep-alive): on Android the cast's foreground service and visible WebView, with a notification; on desktop a power save blocker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.18.0Christophe Besson13 days1-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cast: receiver position and pause/play on both clientsChristophe Besson13 days3-0/+55
| | | | | | | The player's remote mode reads where the television is instead of the local playhead, which drifts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(mnp): remove ten operator messages no client sent0.17Christophe Besson2026-10-021-6/+5
| | | | | | | | | node_status, node_settings_set, roster_read, denylist_read, denylist_clear, node_reload and the signed gek_rotate, member_unpin, transfer_limits, group_detach leave MNP 6.0; the Node page and the CLI do this work over loopback. Their ops keep their tests, moved to the ops level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): no confirmation dialog for adding a folder or its flagsChristophe Besson2026-10-021-30/+4
| | | | | | | Removes confirmFolder (addRoot, attachGroup) and the writable confirmation added in e4f6177, with their two catalogue keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(mnp): sharing a folder is decided on the node's machine only (MNP 6.0)Christophe Besson2026-10-022-2/+26
| | | | | | | | | | root_add, root_update and group_attach leave MNP: adding a directory and switching writable/removable go through the loopback API (native dialog in the desktop app) or the CLI. The operator's Settings tab still lists the roots from any browser, read-only. The desktop app refuses to sign those ops; a loopback flag change now reaches open pages (publish_roots). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(client): list cast receivers as they answerChristophe Besson2026-10-023-31/+50
| | | | | | | | The scan still runs six seconds, but the picker polls what it has found and shows each receiver immediately. A rescan no longer has its timer cut short by the scan it replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): rotating a group key asks nothingChristophe Besson2026-10-011-10/+1
| | | | | | | Rotation narrows rather than widens: members still connected receive the new key, and nothing already shared changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): hosting a group asks nothing when its folder came from the pickerChristophe Besson2026-10-011-2/+1
| | | | | | | The folder chosen in the native picker is the consent; the dialog that followed on every group creation asked the same thing twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: remove a spike page served in production and an unused derivationChristophe Besson2026-10-011-1/+1
| | | | | | | | static/webrtc-test.html posted a raw password to /login from the hub's own origin; meshbay_common/keyderive.py derived keys from a password and nothing called it (F-32). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: an MBK2 bundle is opened once and stored again as MBK3Christophe Besson2026-10-011-2/+32
| | | | | | | | | | | | Transitional. The Argon2 run that makes M makes A, the key MBK2 bundles were sealed under; a session keeps it as a decrypt-only key (IndexedDB in a browser, the key storage in the desktop app). A client meeting an MBK2 bundle opens it — or its recovery copy — and stores the same identity as MBK3 once connected; the desktop app reseals or withdraws it as browser access says. A session without A asks for the passphrase once. Older formats stay refused by name. Replaces the unpin-and-reinvite step the 0.17 flag day required on every node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: downloads are marked and keep their extension; Explorer files are refusedChristophe Besson2026-10-011-1/+18
| | | | | | | | | | | The desktop app writes the Mark-of-the-Web on each file it saves on Windows, as a browser does. Bidirectional controls are reserved characters in a saved name (portable-name.js and paths.sanitize_for_download, and again in the main process), so a name cannot display one extension and carry another. The node refuses uploads of files Windows Explorer acts on by itself: desktop.ini, .lnk, .url, .scf, .library-ms, .searchConnector-ms (F-19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): how a hosted group admits people is the operator's, not the hub'sChristophe Besson2026-10-011-1/+4
| | | | | | | | | | | attach_group no longer copies join_policy and visibility from the hub's answer: they come with the operator's request (the desktop creation form, `group add --open`) and default to invite/private; the CLI says when the hub lists the group otherwise. Every string written into node.toml is escaped (toml_string) and read back through tomllib, so a group or folder name cannot write lines of its own (F-17). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: an identity signs a named kind, and a device approval answers a requestChristophe Besson2026-09-304-5/+182
| | | | | | | | | | | The desktop main process builds every transcript itself from fields (transcripts.js) and signs no raw bytes; the page's identity has the same contract (crypto.js transcriptFor). The keyring seals no bundle while browser access is off. On the node, device_add must redeem a pending request filed by the same keys, and device_revoke is signed under its own prefix (meshbay:device_revoke:v1), so a retirement signature admits nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(client): the desktop application keeps M and every node identity in its ↵Christophe Besson2026-09-304-0/+407
| | | | | | | | | | | | main process keyring.js derives, opens, mints, seals, signs and agrees there; the page gets public keys and a handle. Argon2 comes from the page's own WebAssembly build (Electron's crypto has none). Without OS key storage the page keeps its keys as a browser does. A node's bundle is settled after connecting, re-sealed when the key changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.17.0, and the client minimum with itChristophe Besson2026-09-301-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: the page connects to its own origin and reCAPTCHA, nowhere elseChristophe Besson2026-09-301-1/+3
| | | | | | | | connect-src drops https: and wss: in both policies. Checked against Google's reCAPTCHA test keys: in Chrome widget, token and registration unchanged; in Firefox the widget loads; no violation reported in either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): the page names node operations, and the app confirms what ↵Christophe Besson2026-09-302-78/+303
| | | | | | | | | | | widens the node node:call is replaced by named operations with checked arguments; hosting a group, sharing an unpicked folder, key rotation, denylist clearing and a change of node account are confirmed by a native dialog. Every channel checks its sender, secrets:get/set/clear are gone, node:start writes the app's own hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: remove dead code across packagesChristophe Besson2026-09-281-10/+0
| | | | | | | | | Unused modules, functions, constants and client helpers with no caller, the unreachable hub:probe IPC handler, and the CSAM hash matching. Behaviour unchanged; the dispatch golden loses only the two removed message types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: Windows installer and desktop app start and stop the node one wayChristophe Besson2026-09-273-160/+347
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A 0.16 upgrade in service mode left the previous node running: setup's unelevated taskkill cannot reach session 0, and it ran in customInstall, which electron-builder inserts after the files are copied. The locked exe was not replaced, and the new app talked to the old node ("started but could not link", "No operator paired"). Installer (build/installer.nsh, build/stop-node.ps1): - customCheckAppRunning, which runs before uninstallOldVersion and extraction, stops the node with an embedded stop-node.ps1: control API, then schtasks /end, then Stop-Process, and refuses to half-upgrade if one survives. - An upgrade keeps the mode it finds (task, launcher, previous install), restores the sign-in launcher the old uninstaller deletes, and restarts the node the way that mode runs it. A silent upgrade of an "at sign-in" install used to end with no autostart and no node. - The uninstaller removes the task and firewall rules only on a real uninstall, not on an update. Desktop app (src/main.js): - Start, Stop, Restart and node:start go through the CLI's lifecycle verbs instead of a second implementation; a child spawned by Electron also held Electron's sockets after the app quit. - "Only while MeshBay is open" is a real mode: the app starts a provisioned node at launch and stops the one it started when it quits. - Switching modes stops the node first -- deleting a task does not end its instance, and a new service found the port taken -- keeps the firewall rules every mode needs, and starts the node again. A declined or unanswered UAC prompt restores the node instead of leaving it stopped, and says that nothing changed. - waiting_for_hub counts as a node that is up; linking waits for a node that answers, with a longer deadline, and reports a version mismatch. Packaging (packaging/win): - The service task gets no 72-hour limit, runs on battery and ignores a second start; service.ps1 status reports a stale registration so setup re-registers it; remove ends the running instance before deleting the task. - build-node-runtime.ps1 starts the frozen daemon in a throwaway profile (smoke-node-runtime.ps1) instead of only asking for --help. The mode that was "Off (start manually)" is labelled "Only while MeshBay is open" in all ten catalogues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.16.0Christophe Besson2026-09-251-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): split transport.js into classic scriptsChristophe Besson2026-09-251-0/+9
| | | | | | | | | | | | transport.js keeps the core (connection, reconnect, leases, dispatch). Chat, media, admin, upload and device methods move, cut as text, into transport-*.js scripts that hand a class of their own to extendTransport, which copies each method onto MeshBayTransport.prototype; the codec, roster checks, node pins and the rewrap fan-out move as they were. Both shells load them after transport.js. Every prototype member, class property and top-level function has the same source text as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): let Copy buttons write the clipboard in the desktop appChristophe Besson2026-09-231-3/+7
| | | | | | | | Only fullscreen was granted, so navigator.clipboard.writeText was refused and every Copy button did nothing. Grant clipboard-sanitized-write; reading stays refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.15.0Christophe Besson2026-09-201-1/+1
| | | | Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: cite MESHBAY_DESIGN.md and a section instead of the merged notesChristophe Besson2026-09-191-2/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The per-feature design notes were merged into docs/MESHBAY_DESIGN.md and deleted from the tree on 2026-09-11, but ~230 comments across the three packages still named them — usually written `docs/musicbay.md §3.2`, as though the file were still in docs/. A reader had to know §16 existed to resolve any of them. They now name the section directly. Every mapping comes from §16, the concordance, which already records where each old section landed: musicbay -> §9.8, mediacenter -> §9.7 for the Videos app and §6.5 where the subject is derived data, photos -> §9.9, auth-confirm -> §3.6, refactoring-search -> §9.11, invite-pairing-v1 -> §3.4, per-node-identity-v1 -> §3.2, captcha -> §7.7, chat-sender-keys -> §4.5, apps/refactor-groups -> §9.1–§9.4, desktop-client-v1 per section. Bare citations of the same documents (`draft-v6 §2.11`, `§4.8`, `§3.4`) are retargeted too: those collide with real section numbers in the design document, so leaving them would have been worse than the named form. Four cases the concordance does not cover, each decided rather than guessed: Sub-item references into documents that no longer exist — mediacenter's `§3.3 row 4`, `§3.4b/c`, `apps.md §3b` — name rows and sub-items §9.7 and §9.2 do not reproduce. The module-level citation stays; the sub-item pointer is dropped. The V-findings keep their labels but lose the dead `§10.1/` prefix. §13.8 lists V1–V13 as per-application open items, which is not what the labels mean in these comments, so pointing them at §13.8 would have been a false citation. `apps.md §5`'s virtualization requirement has no counterpart anywhere in the design document. The requirement is stated in the comment itself, so the citation is dropped rather than aimed at a section that does not say it. Comments that attributed a *sentence* to an old note — musicbay's "several thousand files" example, its "what got measured" note, its measured ~11%/~26% cover-art figures, the "original no root, whole shared tree" call — state the fact without attribution now. §9.8 does not contain those sentences and citing it for them would have been wrong. CLAUDE.md's "a reference to a document that no longer exists" row now says the concordance is for git history and out-of-tree material; the code cites sections directly. Verified: 2851 passed, 4 skipped. The 12 errors in the run are the Firefox leg of test_sticky_header.py's browser harness, which is broken at the browser level on this machine — headless Firefox (snap) dies with `[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer`, renders nothing, and the probe exits `{"error": "no measurement"}` after its full 90s wait. Chrome runs the same 12 assertions in 3.2s and passes. Nothing here can affect it: every changed line in style.css is inside a comment. Also checked: ast.parse on every changed .py, `node --check` on every changed .js, the /* */ balance in style.css, and that no changed line exceeds the width its file already used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: drop the references to design notes that live outside the repoChristophe Besson2026-09-191-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Seventeen comments across the Windows packaging targets pointed at C:\Users\admin\devel\light-client.md and msix-installer.md -- absolute paths on one developer's machine, unreadable to anyone else who clones this repository and unverifiable by any test here. The surrounding prose already carried the substance in every case, so these are removals, not rewrites, with two exceptions where the pointer was doing real work: electron-builder.msix.yml's header told the reader to go read §4 and §8 first. It now states the fact directly (an AppX/MSIX install never elevates, by design, so the package carries none of installer.nsh's elevation logic) and says the open items are called out at each declaration below -- which they already were, at `capabilities` and `customExtensionsPath`. The two "msix-installer.md §8" citations become "an open item" / "the other open item", beside the description of the item that was already there. Paragraphs the removals left ragged are re-wrapped. Verified: node suite 1403 pass / 4 skip (71 of them test_packaging_win.py), both electron-builder configs still parse as YAML, node --check on main.js. The .ps1 edits are inside <# #> comment headers plus one deleted Write-Host in a block that keeps two others; no pwsh on this machine to parse them. Not touched, and much larger: ~230 comments elsewhere in the codebase cite per-feature design notes (musicbay.md, mediacenter.md, auth-confirm.md and twenty more) that were merged into docs/MESHBAY_DESIGN.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: decode and re-encode video on the GPU where there is oneChristophe Besson2026-09-181-1/+169
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A 1080p film is decoded by whoever watches it and re-encoded by the node when no browser can decode the source. Both were on the CPU, and on an Atom or Celeron mini-PC neither reaches real time — which is what `transcode_incompatible_video` exists to refuse. This adds the mechanism that makes refusing it unnecessary. Node — `hwaccel.py`: VA-API on Linux, Quick Sync or NVENC on Windows, established by encoding 1080p and reading the file back with ffprobe. Nothing is accepted that does not produce the exact profile and level `stream_init` announces, since the client checks that string before it trusts a byte: an encoder that wrote another level would make the node's own codec string a lie, and ffmpeg takes `-level 4.1` and `-level 41` from h264_qsv without saying which it understood. Three modes per stream — hardware decode and encode, hardware encode alone, libx264 — demoted per source codec, because a GPU that decodes HEVC may have no decoder for MPEG-4 Part 2 and only asking it finds out. A mode that fails is detected on an empty stdout before `stream_init` goes out, so the viewer sees one working stream and never an error. Client — Chromium ships VA-API off on Linux. It is enabled where a render node and a driver are present, then verified through `navigator.mediaCapabilities`: a no moves to the next GL backend on the next launch and an exhausted list drops the switches, so a renamed feature cannot pass for a feature that is on and `--ignore-gpu-blocklist` cannot survive on a machine it did not help. Feature lists now merge rather than overwrite — `appendSwitch` replaces the value, and a second caller would have silently cancelled the mDNS switch aiortc depends on. Packaging — the drivers are weak dependencies on all four formats, so a machine without a GPU installs exactly as before. `dpkg -i` and `rpm -ivh` ignore weak deps; `packaging/README.md` now says so. Windows needs no driver: the bundled ffmpeg already carries h264_qsv and h264_nvenc, and a re-pin that dropped them would cost every low-power Windows node its hardware encoding silently. AMD on Windows (AMF) and macOS (VideoToolbox) are named gaps, not oversights: neither could be tried anywhere in this project, and both re-encode in software as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cast): put the relay's progress log behind the same switchChristophe Besson2026-09-181-15/+38
| | | | | | | | | | | | | | | | | | | | | | | Most of what the relay says repeats without bound — a line every fiftieth fragment for the length of a film, one per dropped fragment whenever a client falls behind — and it lands in the terminal the app was started from. Those go through `debuglog` now, restored with: NODE_DEBUG=cast-relay npm start The loss of fMP4 framing stays on console.warn. It recovers by rescanning, so nothing throws and no other part of the system hears about it; that line is the only trace that the picture on the television is missing a piece. Every other failure here throws, and the renderer already reports the rejection. The logging guard now covers both cast modules, each with the one line it is meant to keep audible. Also corrects the header comment, which still claimed CORS was granted on the subtitle path only. It has been on both since the stream needed it too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
* fix(cast): keep the Chromecast progress log out of the terminalChristophe Besson2026-09-181-10/+30
| | | | | | | | | | | | | | | | | | The receiver reports its state on a timer, so `player status: PLAYING` repeats for as long as a film runs and buries everything else in the terminal the app was started from. The progress lines now go through Node's own `debuglog`, which costs nothing when disabled — the message is never formatted — and is restored with: NODE_DEBUG=cast-chromecast npm start The socket error stays on console.error. It fires when the connection to the receiver dies and is the only trace of why a cast stopped; routing it through the same switch would make the one failure worth seeing the one that disappears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
* feat(cast): carry subtitles to a Chromecast, on the relay's clockChristophe Besson2026-09-184-29/+194
| | | | | | | | | | | | | | | | | | | | | | | | 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
* fix(win): a service-mode daemon can be replaced, and the Node page can link oneChristophe Besson2026-09-141-3/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | Two live-reproduced bugs in Windows node start/stop, found sideloading the 0.14.0 build: - node:start's crash-recovery step killed a service-mode daemon with taskkill/CTRL_BREAK, both of which fail with "Access is denied" against a process running under the Scheduled Task's own S4U logon session (a different session from the Electron app's). The daemon it was meant to replace just kept running, unreplaced, and schtasks /run on a task Windows still considered Running was then a silent no-op too. Route through winServiceTaskEnd() (schtasks /end) first, the way nodeServiceStop/ nodeServiceRestart already correctly do. service-mode.ps1 also now starts the task right after registering it -- Register-ScheduledTask's own AtStartup trigger does not run it immediately, so nothing was listening until the next reboot. - The Node page's Start button called node.start() with no arguments, so an unlinked node (a fresh install, or one whose hub-side link was lost) could never link on Start alone -- only create-group-page.js's own call passed {hubUrl, username, token}. Reproduced on a fresh non-service install signed in to the real hub: Start hung for ~105s and failed with "could not link", pointing at a "Link Node" control that lives on Settings, not the Node page (that message is fixed too). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: bump version to 0.14.0Christophe Besson2026-09-141-1/+1
| | | | | Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6jPTeocXA1BePekdsgPya