aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_DESIGN.md
Commit message (Collapse)AuthorAgeFilesLines
* feat(cli): chat --followChristophe Besson36 hours1-1/+4
| | | | | | | | | | | | chat GROUP --follow prints the last messages, then each one the node pushes, until Ctrl+C (exit 130). The connection is pinged every half minute; one that closes, fails or stops answering is opened again, and what was said meanwhile is read from the history. Each message is printed once, by its (device, nonce) pair, which the node stores once. With --json, one object per line. The man page lists it, with an example, and its FILES now names the device keys kept for the Tier 2 check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): chat — Tier 2 check on the signing device, interactive roomChristophe Besson36 hours1-3/+7
| | | | | | | | | | | | | | | | Each message's signing device is checked against the group's roster as the page checks it (a port of transport-roster.js, held to it by the parity test): chains of countersignatures walked from each account's roots, the keys accepted per account and node kept in the store, the roster read again once before a key is called changed. Only "changed" is said, under the message; --json carries every verdict. chat with no group opens the interactive chat: the groups, then one group's conversation IRC-style with pushed messages arriving live and a line to write in; Esc or /part goes back to the groups, /quit or Ctrl+Q quits. Like the bare command, it opens on the sign-in screen when nothing is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): chat — read a group's last messages, post oneChristophe Besson36 hours1-7/+16
| | | | | | | | | | | | | | chat GROUP prints the last ten messages, oldest first (-N, as head -N, or -n N for another count, paging back past the node's 200), and -m posts one (- reads it from stdin). The application's construction: every epoch key from chat_keys_req, a post sealed under the current epoch through the device subkey and signed with this machine's key on the node, the sender's name inside the envelope; nothing sent past the node's ciphertext bound. A stored message is read signature first, then key, then content, and one that fails is shown as unreadable, never dropped. Not done here: the Tier 2 roster check on the signing device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): search across groupsChristophe Besson37 hours1-3/+12
| | | | | | | | | | | | | search dials every group through the pool and matches each index here, on the Search page's fields (name, folder, title, artist, album), every word required. A group left out of Search by its operator (search_listed in the sealed ack) is skipped before its index is asked for. A file several groups hold is one result, keyed by its content hash, printed from the reader's own node first, as a path get takes. A group that cannot be searched is named and the others go on; one waiting for this machine's approval names its code instead of holding everything up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): rm, and Delete in the interactive modeChristophe Besson37 hours1-2/+8
| | | | | | | | | | | | | rm deletes files, resolved as get resolves paths and patterns, with the node's rule (design §5.4): the uploading account's devices, or the node's operator account. The whole run is checked against the index first (the entry's uploader_id, the role the join result gives this key): a file outside the rule, or a folder, refuses it before anything is asked or sent. Then one confirmation, or --yes. The node's challenge is checked against what was asked before it is signed. In the interactive mode, Delete on the right pane does the same, asked once per batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): invitations, join, decline, leave, devices, browser accessChristophe Besson37 hours1-16/+36
| | | | | | | | | | | | | | join accepts an invitation or redeems a link pasted as sent, then has the node admit a key pair made here, with the invitation's code (a link's code goes only to the node it names; one for a node that is away is kept for the next connection). decline, leave and devices revoke ask first, and refuse without --yes where there is no terminal. devices approve countersigns the one waiting request a code matches, none when several do. browser-access is the application's setting, kept per machine and never enabled from the hub's mirror: on leaves a copy sealed under M on each node, off withdraws it, unset touches nothing. A key pair made for a first contact is now kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: a browser is not linked as a deviceChristophe Besson39 hours1-4/+5
| | | | | | | | | | A browser keeps no identity between connections, so a key approved for it is gone by the next one: the guide, the design and the interface no longer offer it. With browser access off, the application and the command-line client open the account's groups; the linking form, shown in the application only, now says "this computer". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): link a device from the application; approve one match onlyChristophe Besson39 hours1-1/+5
| | | | | | | | | | | | | | | "Get a linking code" called the connection the page had just closed on the node's unknown_device refusal, so it never filed anything. It now retries with the request filed on that connection, before the proof (MNP 6.3), and the page tries the group again after each refusal until a linked device approves the code. Offered only in the desktop application: a browser's minted identity is gone by its next connection. Approving refuses a code that matches more than one pending request: filing needs only the account's token, so somebody holding the passphrase who saw a code could file their own keys under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): meshbay-cli, a command-line client for membersChristophe Besson40 hours1-21/+93
| | | | | | | | | | | | | | | | 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-2/+18
| | | | | | | | | | | | | | | | | 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>
* fix(node): index, serve and write files past Windows' MAX_PATHChristophe Besson2 days1-2/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | With the system's default setting (LongPathsEnabled off) every call given a plain path of 260 characters or more refuses it, so a deep library was partly invisible: the walk could not list such a directory and its files never reached the members. The node now hands each call that touches a group's disk the extended form (paths.long_path), and keeps the plain path in the index, the hash cache and the log. A directory is listed under its long form and its entries named from the plain one (indexer.walk_files, wire._walk_dirs). Covered: the walk, stats and hashing, reconcile and the watcher, the folder list members see, serving over WebRTC and QUIC, deletion, uploads and their recovery, folder creation and removal, audio tags and the cover beside a track, photos, and a show's episode numbering. ffmpeg extends a long path itself and keeps the plain one. long_path now normalises before prefixing (the extended form skips the system's normalisation) and starts at 248 characters, where creating a directory is already refused. test_long_paths.py runs with the setting off: 15 of its 17 tests fail without the change. Checked by hand in the installed app with files at 312 to 380 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): skip a file the disk stops answering for, then pause its rootChristophe Besson2 days1-0/+50
| | | | | | | | | | | | | | | | | | | | | | | | | | A Windows tester's scan stuck for good on one directory of a large disk: one read that never returned held the indexer's only thread, and with it every root behind that one. - A disk call is timed by its silence, not its length: each directory entry listed and each megabyte read counts as an answer. After 60 s of silence the call is abandoned on its daemon thread, the file is noted unread and the scan goes on. - Three silent files in a row pause the root (not persisted). Resume skips the files already known to give no answer; a restart, a replug or a change to the file retries them. - The walk is per directory with os.scandir: a subdirectory that cannot be listed costs itself, and a listing that stops answering pauses the root at once. A scan no longer stats anything from the event loop. - The log names the file after 10 s of silence, the indexing band shows it, and Group settings shows the paused root, where it stuck and the unread files with their reason, on the node's machine only. - generate_http_api.py writes LF on Windows too. Verified with real kernel-blocked opens (an unacknowledged oplock) in the installed frozen app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): run the connection processes on Windows, source and frozenChristophe Besson4 days1-2/+4
| | | | | | | | | | | | | | | | | | | | | | | | | Connection processes and the larger UDP receive buffer were written and measured on Linux; nothing had run on Windows, and `meshbay-node.exe webrtc-worker` had never run from a frozen build on any system. - test_connection_processes.py failed on Windows (8 tests): the selector loop the suite uses there cannot start a process. A test marked `daemon_loop` now runs on the daemon's own loop, chosen through pytest-asyncio's loop factories. - The build starts both frozen executables as connection processes and carries a connection through each (smoke-connection-process.py). The daemon's own smoke test never signs in to a hub, which is when the processes start. - The warning for a receive buffer below what was asked named net.core.rmem_max on every system; it names it on Linux only. Windows grants the 4 MB. Measured on an installed build in service mode, docs/WINDOWS-PORT.md §5.12: two processes at startup and on a live change, a browser's handshake, chat, a 200 MB download over 8 connections across both processes, an upload, a video stream, and stop, restart and start leaving no process behind. 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 days1-5/+30
| | | | | | | | | | | | | | | 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(node): a receive buffer that holds an upload's windowChristophe Besson5 days1-0/+13
| | | | | | | Uploads ran at 6.5 MB/s because the kernel dropped datagrams at the node's 256 KB socket. Asking for 4 MB gave 19.5 MB/s, no drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): carry connections on several coresChristophe Besson5 days1-1/+38
| | | | | | | | connection_processes (default 1) starts that many processes for the WebRTC stack; sessions and keys stay in the node. Set from node.toml or the Node page, applied to new connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: spread a large download over eight connectionsChristophe Besson5 days1-2/+43
| | | | | | | | | One WebRTC connection carries 5 to 6 MB/s whatever the line can do. A leased download of 64 MB or more now opens seven more to its node, and a lease serves the other connections of its device in its group. Measured through the whole stack: 5 MB/s before, 24 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): log what a transfer moved when it endsChristophe Besson5 days1-0/+17
| | | | | | | One INFO line per lease: bytes, duration, rate, and the SCTP association's window, round trip, losses and T3 expirations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(android): what the first backup on a phone showedChristophe Besson6 days1-3/+4
| | | | | | | | | Slices are asked for in the address, not a Range header the WebView drops; a settings change runs at once; the notification is updated once a second; the SMS section says how to lift Android's restricted setting; the photo section says whether videos are included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: record that a folder renamed on disk empties until reconciliationChristophe Besson6 days1-0/+1
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back up WhatsApp's own chat backups, and its media if askedChristophe Besson6 days1-1/+20
| | | | | | | | A WhatsApp section takes WhatsApp's folder through the picker, sends its encrypted Databases and Backups (not the dated copies of earlier weeks) into <folder>/<account>-whatsapp, and names the restore set in the manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): send a manifest of what each backup run sentChristophe Besson6 days1-3/+18
| | | | | | | | Photos, videos and files record, per file, their path on the node, their path and album on the phone, dates, size and SHA-256, uploaded as meshbay-manifest/manifest-<date>.jsonl once the run is done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back up the files of folders the person choosesChristophe Besson6 days1-1/+17
| | | | | | | | A Files section takes folders through the system picker (no storage permission), refuses DCIM, Pictures and Movies, skips what the photo backup sends, and runs on the photo backup's own runner into <folder>/<account>-drive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(photos): show a phone's clips in their albumsChristophe Besson6 days1-1/+11
| | | | | | | | Videos in a photo folder are thumbnailed by the node, counted and marked in the album, and played in the group's video player from the lightbox; the slideshow passes them by. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back the videos of the chosen albums up beside the photosChristophe Besson6 days1-3/+16
| | | | | | | | A 'Videos too' option, off by default, sends them into the same YYYY/YYYY-MM folders. Files are read from the phone a ranged chunk at a time, so a large video is never whole in the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back the calendars up as an iCalendar fileChristophe Besson6 days1-6/+18
| | | | | | | A Calendar section sends every calendar the person can edit, as a dated .ics, into <folder>/<account>-calendar once a day when it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back up the personal profile onlyChristophe Besson6 days1-0/+9
| | | | | | | A copy of the application inside a work profile offers no backup, and no source reads another profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back text messages up, in a build Play does not getChristophe Besson6 days1-11/+31
| | | | | | | | A Messages section sends the SMS added since the last copy, as restorable <smses> XML, into <folder>/<account>-messages/YYYY. A play flavor has neither READ_SMS nor the code that reads messages; full is the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): one backup destination, a group the account owns aloneChristophe Besson6 days1-21/+25
| | | | | | | | Chosen once at the top of Android Sync for every kind; photos and contacts go into <folder>/<account>-photos and -contacts. Owner and sole member are checked at set-up and before every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): delete a photo from the Photos appChristophe Besson6 days1-0/+1
| | | | | | | On a right-clicked tile and in the lightbox bar, after a confirmation, for the node's operator or the photo's uploader, as in Files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): back the phone's contacts up to a group of one's ownChristophe Besson6 days1-0/+31
| | | | | | | | A Contacts backup section on the Android Sync page sends a dated .vcf into <folder>/<account>-contacts once a day when the address book changed, only to a group the account is alone in, checked again at every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): photo backup moves to its own Android Sync pageChristophe Besson6 days1-2/+8
| | | | | | | A Phone section of the side menu leads to it, on the Android application only; Settings no longer holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(android): back photos up under YYYY/YYYY-MM, not YYYY/MMChristophe Besson6 days1-1/+1
| | | | | | A month folder named 08 alone read like an album number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: back up the phone's photos to a group, once a day on Wi-FiChristophe Besson7 days1-1/+87
| | | | | | | | | | | | | | | | | | | | | | | | | The Android application sends the photos taken on the phone to one folder of one group chosen by the member (docs/MESHBAY_DESIGN.md §9.12). The phone lists MediaStore, keeps a ledger of what was sent and hands each photo's bytes to the page by an opaque token on the packaged origin; the page decides when a run is due and uploads through the existing path, one photo at a time under a slot. - Once a day from the last finished run, on an unmetered network only; "Back up now" asks first on mobile data. Leaving Wi-Fi stops after the file in flight. - Photos already on the phone are sent by default, newest first, under <folder>/YYYY/MM; edits are sent beside the original as -edited-<date>. - Additive by construction: nothing is ever deleted, renamed or replaced on the node, and a photo deleted on the node is not sent again. - A confirmation names the group, owner, members, folder and size when the destination or starting point changes; a lasting refusal (disk full, folder read-only or gone, no longer a member) is said once and retried a day later. - No ACCESS_MEDIA_LOCATION, so the platform redacts photo locations. - A dataSync foreground service keeps a run going with the screen off. HEIC/HEIF photos are sent but not shown in Photos yet (§15.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): refuse an upload on a full disk with a stated reasonChristophe Besson7 days1-0/+11
| | | | | | | | | | | | Nothing on the upload path knew about ENOSPC: a write that found no room raised out of the handler, the catch-all answered "Request failed", and the .part stayed behind holding the space that had run out. The node now refuses with `disk_full` at chunk 0 when the announced size would leave less than 1 GiB free, and at any write that fails with ENOSPC/EDQUOT, dropping the partial. The client carries the code on the error and the transfers panel says "The node's disk is full" in every catalogue. 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-2/+5
| | | | | | | | 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>
* feat: notifications on Android while closed, with nothing to installChristophe Besson7 days1-4/+40
| | | | | | | | | | | | | | | | | | | | The phone fetches what is new every fifteen minutes with a poll secret (POST /v1/push/poll) that reads notification lines and nothing else. When a UnifiedPush distributor is already installed, the hub also pushes at once, encrypted to the phone (RFC 8291); losing the distributor falls back to fetching. The hub now honours "disable all notifications" itself: create_notification creates nothing for that account, as it already did for a muted group, so neither switch lets anything reach a phone. The interface used to be the only reader of the account-wide switch. Push endpoints are member-supplied URLs: a send refuses non-public addresses, connects to the address it checked, and follows no redirect. Android build untested here (no SDK on this machine). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: sign Android releases with the release keyChristophe Besson8 days1-4/+6
| | | | | | | assembleRelease reads the key from ~/.gradle/gradle.properties and fails without it instead of falling back to the debug key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(node): sample 9 MB with the size above 9 MB, keep known ids0.19Christophe Besson9 days1-13/+20
| | | | | | | | hash_version 3: size + first 4 MB + last 4 MB + 1 MB at the middle, 5.5x faster cold on a USB disk than the 45 MB sample. The cache now serves a hit under whatever version it holds, so no existing id moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): name a root after its drive when its basename is takenChristophe Besson9 days1-3/+6
| | | | | | | | | Two drives with a folder of the same name made the second add fail, and no screen could supply another name. add_root now names it "Name (H)" or "Name (parent)"; a name the operator typed is still refused on a clash. 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(node): name members admitted without an invitation nameChristophe Besson9 days1-1/+1
| | | | | | | | | | | A member who joined by link, by a new device or into an open group was pinned in the roster with no name, so the audit log showed only the first characters of their id. The hub's MNP token now carries the account's username, and after the handshake the node writes it into the roster for an account whose name is empty. An invitation's name is never overwritten; the name is a label, authority stays on `sub`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(node): list a group's directories off the event loopChristophe Besson9 days1-1/+0
| | | | | | | | Every full index walked all roots on the loop, and a node with several large roots stopped answering for seconds. Walk directories only, on the roots' disk thread; index_sync is spawned and still answers on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): drop a directory moved out of the root from the indexChristophe Besson9 days1-0/+1
| | | | | | | | Watchdog reports such a move as one "directory deleted" event and nothing for the files, which the indexer ignored until the next reconcile. The freeze rules still apply: root live, directory gone, parent present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): let a download wait out a reconnect instead of failingChristophe Besson9 days1-0/+4
| | | | | | | | Chunks sent while the reconnect's connect() runs throw at once, and six retries 1.5 s apart ran out before the reconnect landed. Wait for it, up to two minutes, without spending retries. Follow-ups parked in §15.3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): play video on iPhone through ManagedMediaSourceChristophe Besson10 days1-0/+1
| | | | | | | An iPhone has no MediaSource; every film was refused as an unsupported codec. A browser with neither now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: copy a file's or folder's #/name@owner link from Files, Music, Photos ↵Christophe Besson11 days1-0/+1
| | | | | | | | | | | | | | | | | | | | | | | and Search "Copy link" puts the address group-link.js resolves on the clipboard, on the hub's origin rather than the page's, so a link copied in the desktop application is not app://meshbay. Files offers it for one row, from the right-click menu or the toolbar with one row ticked (a phone's way in); Music on one track's menu, whose dots a phone has; Photos on a right-clicked tile and in the lightbox's bar. The video player and the file preview carry a link button next to Download. Applications get a `linkFor(entry | folderPath)` prop (MESHBAY_DESIGN.md §9.2) and offer the action only when it names a link. The group page builds it from the hub's row; Search from each result's own group and its path before the merged views prefixed it, and names no link for a folder of the merged tree, which a group name alone does not identify. harness/copy_link_probe.py mounts the three applications in Chrome and reads what reached the clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: open a group, a folder or a file from a #/name@owner linkChristophe Besson11 days1-1/+28
| | | | | | | | | | | | | | | | | | | A group can now be reached by the handle shown under its name, and a path after it points inside the group: #/name@owner/root/dir/file downloads the file and opens Files on its folder; a folder opens Files there. The handle is resolved in the client against the account's own /v1/groups/mine, so no hub route answers for a name and nobody can probe for one. While a group is open the address shows the handle (replace, no history entry); a linked path is taken out of the address once acted on, so a reload does not download twice. Signing in no longer sends everyone home: the form stood in for the page the address named, and that is where a link opened signed out was going. group-link.js holds the parsing and lookups, executed whole by test_group_link.py; harness/group_link_probe.py drives the router in Chrome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: list as members only the accounts the node has admittedChristophe Besson11 days1-1/+5
| | | | | | | | | | | | | | | | The Members list showed the hub's membership, which an account gains when it accepts the invitation or redeems a link, before it has presented its code to the node. The node's roster is the authority (MESHBAY_DESIGN.md §3.4), so the list now crosses the hub's members with the sealed group roster the node already sends every connected member. An account the node has not admitted yet is shown to the owner alone, as waiting for its code, with the Remove button; other members do not see it. When the roster cannot be read, the hub's list is shown as before. groupRoster() takes { fresh: true } so the page sees who joined since the connection opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): plug an auto-ejected removable root back once its files returnChristophe Besson11 days1-3/+13
| | | | | | | | | | | | | | | | | A node started with the desktop session runs before the session has mounted its USB drives. The safety net then auto-ejected every removable root and persisted it exactly like an operator's eject, so after each reboot those roots stayed ejected until someone plugged them by hand (seen on a node whose /media drives were mounted a minute after it started). An auto-eject is now stored as such ("auto" in roster.db). At startup and at every reconcile, an auto-ejected root whose path is readable again is checked against a few files the hash cache knows under it, at the same path with the same size and mtime; one found and the root is plugged back and rescanned. An empty mount point or another drive in its place is not recognised and stays ejected. An operator's eject is never undone automatically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>