summaryrefslogtreecommitdiffstats
path: root/docs/apps.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-11 00:19:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-11 00:19:06 +0200
commitf059cb118c556d1f0279350507f74b8a47d5a98a (patch)
tree9a97762a844038a06134b4b7dcead1758477dfc1 /docs/apps.md
parentb045ba0010d69360b6a0265eb7c73a07900fe328 (diff)
downloadmeshbay-f059cb118c556d1f0279350507f74b8a47d5a98a.tar.gz
docs: remove the documents MESHBAY_DESIGN.md replaces
Twenty-four files, about 17 000 lines: the two architecture drafts, the three security reviews, eleven design notes, the roadmap, the decisions file, the v1–v4 archive, the deprecated user guide and the stale quickstart. Their content is in MESHBAY_DESIGN.md, and git history holds the originals. The reason to delete rather than keep bannered: a document that is superseded but present still gets read, and a reader cannot always tell which of two accounts of one mechanism is the live one. That was the argument for retiring the user guide rather than repairing it, and it applies to the whole set. What made this safe is the concordance. Roughly 290 comments and docstrings cite these files by section — `musicbay.md §6`, `mediacenter.md §5.5`, `draft-v6 §2.11` — and section 16 maps every one onto its replacement, so not a single comment needs editing to stay followable. It now says plainly that the files are gone and where to recover them, and it gained rows for the three reviews (their findings are section 13), and for the two guides. Four kept documents pointed into the set and were repointed first: `playlists.md` (nine references — it is a live proposal and must not dangle), `WINDOWS-PORT.md`, and CLAUDE.md's example. No dangling reference remains outside section 16. Two files were dropped from the list after checking what they hold. `HTTPS.md` is an operational runbook — Caddy, certificate renewal, DNS, troubleshooting — and MESHBAY_DESIGN.md deliberately covers no operations, so nothing would replace it; the versioned Caddyfile is the config, not the procedure. `cast-smart-tv.md` is the plan for the unbuilt DLNA phase of a feature whose first two phases ship, and section 11.4 summarises it in four lines rather than carrying the SSDP/UPnP work. There is no user guide now, and section 0.1 says so rather than leaving a reader to discover it. Suites green: 2258 passed, 4 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVoHVCcfBqud6ZjG4db3y7
Diffstat (limited to 'docs/apps.md')
-rw-r--r--docs/apps.md333
1 files changed, 0 insertions, 333 deletions
diff --git a/docs/apps.md b/docs/apps.md
deleted file mode 100644
index 991af1b..0000000
--- a/docs/apps.md
+++ /dev/null
@@ -1,333 +0,0 @@
-# Group applications — adding one
-
-> **Superseded by `MESHBAY_DESIGN.md`.** This was the group-application framework; its design
-> content now lives in §9.1–§9.4.
->
-> It is kept because code comments, tests and other documents cite its
-> sections and its labels, and because it records reasoning a synthesis
-> compresses. **Where it disagrees with `MESHBAY_DESIGN.md`, the design
-> document is right; where either disagrees with the code, the code is.**
-> `MESHBAY_DESIGN.md` §16 maps every section reference here onto its
-> replacement, and §13 defines every label.
-
-> Status: **superseded, and accurate as far as it goes.** Describes the plug-in architecture that
-> replaced the monolithic `static/app.js`, landed 2026-08-23. See
-> `meshbay-draft-v6.md` §2.7 for why this exists and what it changes; this
-> document is the how-to.
-
-A group has "applications" — Chat, Files, and Videos today (a poster-grid
-browser; see `docs/mediacenter.md`), Music/Photos planned (a music player, an
-album viewer). Video/audio/image files are already classified by the node's
-indexer (`meshbay_node/indexer/indexer.py`, `type: video|audio|image`) and
-flow through the same `index_sync`/`file_req`/`stream_req` messages Files and
-`VideoPlayer` already use — Music/Photos need no MNP change beyond that.
-Videos itself did need one: TMDB metadata (`media_meta_req`/`resp`), per-season
-overview (`season_meta_req`/`resp`), and operator match correction
-(`tmdb_search_req`/`resp`, `tmdb_override`/`_ack`) are all additive message
-pairs on top of the same index/chunk plumbing, not a replacement for it.
-Adding a new app is still a new file plus one registry entry — nothing about
-the group shell changes.
-
----
-
-## 1. The shape
-
-```
-group-page.js ─┬─ owns: connection (transportRef/gekRef), the file index
- (the shell) │ (entries/nodeDirs/nodeRoots), admin flags, which apps are
- │ enabled, the tab bar, the video/preview modals
- │
- ├─ apps.js ─── the registry: [{ key, icon, labelKey, Component }]
- │
- ├─ chat-app.js ──────── ChatPanel
- ├─ files-app.js ─────── FilesPanel, FilePreview
- └─ (video-app.js, music-app.js, photos-app.js — not built)
-
-group-settings.js ─── not an app. Always present, not toggleable — disabling
- it would strand an operator with no way to re-enable
- anything. Holds the "Applications" checkbox list.
-
-Shared infrastructure (imported by app.js AND every per-app file — this is
-why they exist as separate modules rather than being re-exported from app.js,
-which would make a circular import):
- icon.js — the <Icon> component and its SVG path table
- file-utils.js — formatSize/formatDate/canPreview/FILE_ICONS, the
- download/decrypt pipeline (pipelinedDownload, downloadEntry,
- _openDownloadTarget, _saveBlob), CHUNK_SIZE
- hub-client.js — HUB, hubFetch, the auth/session/token-renewal machinery,
- the group-index IndexedDB cache, the keypair-bundle cache,
- `session` (mutable {bundleKey, pendingJoinCode}), navigate
-```
-
-`app.js` itself is what's left after the split: routing, every *other* page
-(Login/Register/Home/Explore/Search/Profile/Settings/Admin/Node/
-CreateGroupWizard), and nothing group-application-specific.
-
-## 2. What every app receives
-
-`group-page.js` builds one `commonProps` object per render and spreads it into
-whichever app is active:
-
-```js
-const commonProps = {
- groupId, transportRef, gekRef, status, username, deviceReady,
- entries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex,
- isNodeAdmin, operatorPaired, mayUpload, userId, setError, onPreview,
- onRefreshIndex: refreshIndex, onActivity: touchActivity,
-};
-...
-${apps.map(a => tab === a.key && html`<${a.Component} key=${a.key} ...${commonProps} />`)}
-```
-
-Every registered component gets the same context and destructures what it
-needs — a new app does not get a bespoke prop list. Notable ones:
-
-| Prop | What it is | Why it's here, not local state |
-|---|---|---|
-| `entries`, `nodeDirs`, `nodeRoots` | the group's file index | Chat needs it too, for image attachments — lifting it avoids two copies going stale against each other |
-| `applyIndex(indexMsg)` | writes a fresh index into the three above, plus the search cache | anything that mutates files (upload, delete, mkdir) calls this so every app sees the result |
-| `onPreview(entry)` | opens the shell's video/preview modal | `entry.type === 'video'` routes to `VideoPlayer`, anything else to `FilePreview` — an app just calls this, it does not own modal state |
-| `transportRef`, `gekRef` | refs to the live MNP transport and the imported group key | never state — a ref, so reconnects don't force a re-render of every app |
-| `deviceReady` | whether this connection has identified a device to the node (`device_hello`) | **the exception to the row above, and why it is a prop.** A ref not re-rendering is right for a transport an app reaches into on demand, and wrong for a *fact about the connection* an app renders from. Chat's composer gates on this one: a reconnect clears it and settles it again inside `connect()`, and while it was read off `transportRef.current.devicePk` during render, the panel latched shut on whatever unrelated re-render came next and had no event that would open it again. See `test_chat_send.py` |
-| `mayUpload` | `memberUpload || isNodeAdmin`, computed once | Files' toolbar and Chat's composer both gate on it; a second derivation would eventually disagree with the first |
-
-### 2b. The same app, rendered by the Search page
-
-Videos, Music and Photos are mounted twice: by `group-page.js` for one group,
-and by `search-page.js` across every group the reader belongs to. The second
-caller passes the same prop shape, and the difference lives entirely on the
-**entries**, in underscore-prefixed fields the group page never sets:
-
-| Field | What it is |
-|---|---|
-| `groupId`, `groupName`, `groupOwner` | which group serves this entry |
-| `_tRef`, `_gRef` | that group's transport and key — read as `entry._tRef \|\| transportRef`, which is why a single-group mount needs no special case |
-| `_connGen` | bumped when that group reconnects; use it as a refetch key so a tile recovers instead of staying a spinner |
-| `_sources` | every group that has this file, after the de-duplication below |
-
-**A file shared by two groups is one entry, not two** (`source-merge.js`,
-`docs/refactoring-search.md`). Entries are folded on their content hash and one
-source is resolved per *unit* — a film, a show, an album — using each app's own
-grouping function to decide what a unit is. Two consequences for a new app:
-
-- if it renders a group name, use **`SourceTag`** from `group-name.js` rather
- than `entry.groupName`: a merged entry has several groups and must say
- `N sources` instead of naming one. Pass it the **whole unit** (a show's
- episodes, an album's tracks), not the entry the card was drawn from — that
- entry is usually chosen for its thumbnail, and would under-report;
-- if it needs a merge unit key of its own, add a `<name>Units()` helper to
- `search-page.js` that calls the app's **exported** grouping function. Never
- re-derive the keys there: a copy keeps agreeing until one of them changes,
- and the symptom is a show whose episodes stream from two different nodes.
-
-The Files explorer is deliberately **not** merged — there each group is a
-top-level folder, and merging would remove a file from one of them.
-`test_search_files_unmerged.py` refuses a build that changes this.
-
-An app that needs **local** state (Files' `selecting`/`sortKey`/`currentPath`,
-for instance) owns it itself with `useState`, same as before the split. One
-thing worth keeping if you add a tab with a notion of "current location within
-the group" the way Files has a path: reset it on `groupId` change.
-`files-app.js` does this —
-
-```js
-useEffect(() => { setCurrentPath(''); setSelected(new Set()); setFilter(''); }, [groupId]);
-```
-
-— because a directory from the group just left rarely exists in the one just
-entered, and without the reset the panel shows a stale path and lists
-nothing. This was a real bug, fixed before the split; carry the pattern into
-any app with similar per-group local state.
-
-## 3. Enable/disable: the mechanism
-
-Same shape as a root's `writable` flag (`refactor-groups.md` §1.1) — an
-operator-signed setting, stored on the node, enforced by absence rather than
-by the client's honesty. It used to be described against `member_upload`,
-which was the group-wide upload switch; that was removed in the same refactor.
-
-**Node side** (`meshbay_node/roster.py`):
-```python
-SETTING_ENABLED_APPS = "enabled_apps" # in the existing group_settings table
-DEFAULT_APPS = ("chat", "files") # what an unset group gets
-async def enabled_apps(group_id) -> list[str]: ...
-async def set_enabled_apps(group_id, apps, set_by="") -> list[str]: ...
-```
-`meshbay_node/ops.py` has `set_enabled_apps(state, group_id, apps)`, called
-from exactly one place: `webrtc_server.py`'s `_admin_exec_apps_enabled`, after
-`_verify_admin_sig` — nothing is applied before the signature checks out.
-
-**An app's directories are the same shape one level down** (2026-09-06):
-`ops.set_app_directories(state, group_id, app_key, paths)`, stored under
-`<app_key>_directories`, reached by one MNP message (`app_directories`) and one
-loopback route. Adding an app adds no function, no message type and no route —
-which is what "plugin architecture" has to mean to be worth the phrase.
-
-`_do_apps_enabled` in `webrtc_server.py` validates before it ever issues a
-challenge:
-- `apps` non-empty — the operator can never lock a group down to nothing.
-- every entry in `WebRTCPeerSession.ALLOWED_APPS`
- (`{"chat", "files", "video", "music", "photo"}` today) — **this is the line
- a new app's node-side registration touches.**
-- `files` is added to the list if it is absent, at both writers
- (`_do_apps_enabled` and `ops.set_enabled_apps`, both at the front so the two
- agree). It is not a toggle: MNP permits root exploration regardless of what
- this list says, so hiding the tab only ever misled.
-
-The whole set is signed in one message (`apps_enabled`, `OP_APPS_ENABLED` in
-`meshbay_common.adminop`) rather than one op per app — ticking several boxes
-in Settings costs one signature, not N. The transcript's subject is the
-sorted, comma-joined app list (`"chat,files"`), built the same way on both
-sides so the operator's browser and the node arrive at identical bytes to
-sign/verify.
-
-`enabled_apps` rides in `handshake_ack` and `node_status`, next to the roots
-table. Changing it broadcasts `apps_enabled_ack` to everyone already connected
-— `transport.js`'s `onAppsEnabled` — so a disabled tab disappears without
-waiting for a reconnection. The root ops (`root_update_ack`, `root_eject_ack`,
-`root_plug_ack`) broadcast the same way, through `onRootsChanged`.
-
-### 3b. An app's settings
-
-Each app that has settings exports a component from
-`static/<app>-app-settings.js` and names it in its `apps.js` entry. The Settings
-page renders one collapsible section per registry entry, with the app's own
-on/off switch in the header — the toggle *is* the enablement control, rather
-than a checkbox list somewhere else that could disagree with it.
-
-Every pane takes the same props, and nothing else: `roots`, `dirs`, `settings`,
-`saveDirectories` (bound to this app), `transport`, `signFn`. The split is the
-point — **what every app has, the page does generically; what one app alone
-has, the pane does itself.** Pointing an app at folders goes through
-`saveDirectories`; a TMDB credential or a link-preview switch is the pane's own
-business, made with the transport it is handed. An app that only needs
-directories therefore touches neither `group-settings.js` nor `group-page.js`,
-and `test_app_settings_plugin.py` fails if either of them starts naming apps
-again.
-
-Two constraints that are not obvious:
-
-- **A pane must not import `group-settings.js`.** That is a cycle
- (`group-settings` → `apps` → pane → `group-settings`), and ES modules answer
- it with a temporal-dead-zone `ReferenceError` at first render — the component
- does not appear, with nothing in the console to say why. The shared widgets
- (`CollapsibleSection`, `ToggleSwitch`, `useSaver`) live in `settings-ui.js`
- for this reason.
-- **A new module must be added to `_ASSETS`** in `meshbay_hub/api/webapp.py`.
- A file reached through the registry is not imported by name anywhere, so
- nothing else would notice it changing, and a browser would go on serving the
- cached copy. `test_asset_versioning` enforces it.
-
-**Client side:** `apps.js`'s `visibleApps(enabledKeys)` filters the registry;
-`group-page.js` calls it with `enabledApps` state (from the ack, `null` until
-one arrives, which `visibleApps` reads as "show everything registered" — a
-node that predates an app, or hasn't answered yet, hides nothing). The
-Settings toggle list in `group-settings.js` iterates the *same* `APPS`
-registry, so a newly-registered app gets a checkbox for free.
-
-## 4. Adding an app — checklist
-
-1. **`<name>-app.js`**, exporting a component with the standard props shape
- (§2). Use `files-app.js` as the reference if the app is file/media-centric
- (it will be, for Videos/Music/Photos — all three are views over `entries`
- filtered by `type`), or `chat-app.js` if it needs its own local realtime
- state. Import shared helpers from `file-utils.js`/`hub-client.js`/
- `icon.js` — do not re-implement `formatSize`, the download pipeline, or
- `Icon`.
-2. **Register it** in `apps.js`'s `APPS` array: `{ key, icon, labelKey,
- Component, Settings? }`. `key` is the wire identifier — it must match what
- you add to the node's allow-list next, and it is also the row an app's
- directories are stored under (`<key>_directories`). One identifier per app,
- everywhere; `test_app_settings_plugin.py` checks the registry against
- `ALLOWED_APPS`.
-2b. **`<name>-app-settings.js`**, if the app has anything to configure,
- exporting a component that takes `{ roots, dirs, settings,
- saveDirectories, transport, signFn }` and nothing else (§3b). Folders go
- through `saveDirectories`; anything only this app has, it does itself with
- the transport. **Do not import `group-settings.js`** — that is a cycle, and
- it fails as a component that silently does not render.
-2c. **The toolbar pins.** If the app has a toolbar — a row of controls above
- whatever it is the app shows — give it `position: sticky` on the pattern
- `style.css`'s "Sticky chrome" section holds, so that scrolling a library
- does not take its own controls off the screen. Two conditions come with it,
- and both are structural rather than cosmetic: the toolbar must be a
- **direct child of the page root** (an app renders a fragment, so it already
- is — do not wrap it in a container of your own), and it must be **opaque**,
- or the content scrolls visibly through it. If anything of the app's pins
- *below* that toolbar, as Files' column heads do, the toolbar has to publish
- its own height with `useStickyBand` from `sticky.js` — its height is never
- a constant, since it wraps on a phone. An app with no toolbar renders none:
- an empty band still holds a strip of the page open, which is why Photos
- draws no toolbar on the Search page.
-3. **Node-side allow-list**: add the key to `ALLOWED_APPS` in
- `webrtc_server.py`. Without this the node refuses `apps_enabled` for any
- set naming it (`"Unknown app(s): ..."`), so an operator can never turn it
- on.
-4. **i18n**: at minimum, a `group.tab_<name>` key (the tab's tooltip/label,
- reused as the Settings checkbox label) in all ten `static/locales/*.js`
- files. `test_locales.py` holds them to the same key set.
-5. **`webapp.py`'s `_ASSETS`** tuple: add both new files. This is the
- cache-busting hash's input list — a file imported by the page but missing
- here can change without the served URL changing, which is the exact bug
- class `test_asset_versioning.py` exists for. Forgetting this step used to be
- silent: nothing errors, a browser just keeps an old copy. It is now caught —
- `test_every_static_script_participates_in_the_fingerprint` holds `_ASSETS`
- to every `.js` in `static/` (`sw.js` excepted, unversioned on purpose).
- Written after `source-merge.js` shipped missing from the list.
-6. **Test coverage that scans the file set**: `test_hook_ordering.py`
- (`STATIC_FILES`), `test_sticky_header.py` (add a case to its probe if the
- app has a toolbar — a band that stopped pinning looks exactly like one that
- never did) and `test_transport_contracts.py`
- (`test_no_setter_survives_the_state_it_belonged_to`, `SPLIT_FILES`) walk a
- fixed list of files looking for a whole class of bug each — add the new
- file to both lists, or it is simply never checked, which fails silently
- rather than loudly.
-7. **`sync-ui.js`** needs no change — it copies the whole `static/` tree
- verbatim. Run `npm run sync-ui` in `meshbay-client` after adding the file
- and confirm it's reported.
-
-No protocol change, no hub change, no `daemon.py` change — steps 3 and 6 are
-the only node-side touches, and both are allow-lists, not new wire messages.
-Directories in particular need nothing server-side at all: `app_directories` is
-one generic op keyed by the app's name (§3), and an app storing its folders
-under a key nobody wrote code for is the case
-`test_app_directories.py::test_an_app_nobody_wrote_code_for_stores_its_directories`
-pins.
-
-## 5. What does not exist yet
-
-- **Thumbnails/posters — built for Videos, 2026-08-23, see `docs/mediacenter.md`.**
- The plan below (lazy, client-side, no node-side store) turned out to be
- wrong once a real design pass ran the numbers: `docs/mediacenter.md` §2
- revises `desktop-client-v1.md`'s O12 and has the node generate thumbnails
- (an `ffmpeg` frame grab, its own bounded worker pool) and cache them
- durably in its own `data_dir`, delivered over the existing `file_req`/
- chunk path addressed by their own blake3 hash. TMDB posters/metadata are
- fetched and cached by the node the same way — no client ever talks to
- TMDB directly. A virtualized grid (`IntersectionObserver`-based lazy
- mount) is built in `video-app.js`, per the note below. A future Photos
- app can reuse the same node-side machinery (thumbnail cache, chunk-path
- delivery) without re-deciding any of this.
-- **Videos, Music, Photos themselves.** Videos is now built (`video-app.js`,
- `docs/mediacenter.md`). Music is now built (`music-app.js`,
- `docs/musicbay.md`) and reuses Videos' node-side thumbnail/chunk-delivery
- machinery, with no new streaming path (a track is small enough to
- download-then-play, unlike a film). Photos is **designed, not built** —
- see `docs/photos.md` — and reuses the same `thumb_hash`/chunk-delivery
- machinery again; unlike Videos/Music it needs several root folders per
- group rather than one, has a single album-grid view with no third-party
- matching step, and reads EXIF locally on the node instead.
-- **The offline/loopback settings path.** A root's flags can be changed two
- ways: over a live MNP connection (any browser, anywhere), or — Electron
- only, and only when MNP is not connected — via the node's local HTTP API
- (`platform.node.call('PATCH', '/api/groups/<id>/roots/<name>')`,
- `SharedDirectoriesTable` in `group-settings.js`). `apps_enabled` only has
- the MNP path today. Adding the loopback twin is a `meshbay_node.ui` endpoint
- plus a branch in the table's `run()` helper, mirroring the root ops.
-
- **MNP is the path that must exist, not the fallback.** The operator of a
- node is not necessarily sitting at it. The first version of the shared
- directories table read its roots exclusively from the loopback API, which
- resolves to "not available" in a browser — so the whole section rendered for
- nobody on the web, while the controls it replaced had worked there. Any
- operator-facing setting added here needs the MNP route first.