summaryrefslogtreecommitdiffstats
path: root/docs/apps.md
diff options
context:
space:
mode:
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.