summaryrefslogtreecommitdiffstats
path: root/docs/apps.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/apps.md')
-rw-r--r--docs/apps.md192
1 files changed, 192 insertions, 0 deletions
diff --git a/docs/apps.md b/docs/apps.md
new file mode 100644
index 0000000..94116c6
--- /dev/null
+++ b/docs/apps.md
@@ -0,0 +1,192 @@
+# Group applications — adding one
+
+> Status: **current, as built.** 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 and Files today, Videos/Music/Photos planned
+(Netflix-style browsing, Spotify-style playback, an album viewer). None of the
+planned ones need an MNP protocol change: 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. Adding one is 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,
+ 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 |
+| `mayUpload` | `memberUpload || isNodeAdmin`, computed once | Files' toolbar and Chat's composer both gate on it; a second derivation would eventually disagree with the first |
+
+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 `member_upload` (`meshbay-draft-v6.md` §2.1b) — an
+operator-signed setting, stored on the node, enforced by absence rather than
+by the client's honesty.
+
+**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.
+
+`_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"}` today)
+ — **this is the line a new app's node-side registration touches.**
+
+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
+`member_upload`. 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 same as `member_upload`'s live
+broadcast.
+
+**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 }`. `key` is the wire identifier — it must match what you add to
+ the node's allow-list next.
+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 the new file. 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 is
+ silent: nothing errors, a browser just keeps an old copy.
+6. **Test coverage that scans the file set**: `test_hook_ordering.py`
+ (`STATIC_FILES`) 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.
+
+## 5. What does not exist yet
+
+- **Thumbnails/posters.** No generation mechanism, client or node side. A
+ Netflix-style grid or a photo album needs *something* here; the pragmatic
+ v1 (decided but not built) is lazy, client-side: decrypt the image, or a
+ video's first segment, only for tiles actually visible, with an in-memory
+ cache — no new MNP message, no node-side store. This is real per-tile cost
+ (a full chunk-pipelined decrypt per thumbnail), acceptable for a browsing
+ grid, not for hundreds of tiles rendered at once — a virtualized grid is
+ part of building Videos/Photos, not optional.
+- **Videos, Music, Photos themselves.** Deliberately out of scope for the
+ refactor that built this — see `meshbay-draft-v6.md` §2.7. The
+ infrastructure above is proven end-to-end with the two apps that already
+ existed (Chat, Files); a new one is additive.
+- **The offline/loopback settings path.** `member_upload` can be toggled two
+ ways: over a live MNP connection, or (Electron only) via the node's local
+ HTTP API when MNP isn't connected (`platform.node.call('PUT', .../member-
+ upload')`, `group-settings.js`). `apps_enabled` only has the MNP path today.
+ Adding the loopback twin is a `meshbay_node.ui` endpoint plus a
+ `group-settings.js` branch, mirroring the existing `member_upload` one.