summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/apps.md192
-rw-r--r--docs/meshbay-draft-v6.md42
2 files changed, 234 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.
diff --git a/docs/meshbay-draft-v6.md b/docs/meshbay-draft-v6.md
index 2ea73c4..72cfdbf 100644
--- a/docs/meshbay-draft-v6.md
+++ b/docs/meshbay-draft-v6.md
@@ -26,6 +26,7 @@
| `docs/invite-pairing-v1.md` | invitations, pairing codes, the node roster — **as built** |
| `docs/per-node-identity-v1.md` | identity keys are per node; the hub stores none |
| `docs/desktop-client-v1.md` | the desktop client in full — shell, device linking, roots, packaging, execution order |
+| `docs/apps.md` | the group UI's plug-in architecture — as built, and how to add an application |
| `devel-phases-next.md` | the roadmap |
---
@@ -43,6 +44,7 @@
| 7 | Accounts | Native registration is **hybrid**: passphrase-derived `auth_key` (the recovery path) plus a device Ed25519 key for day-to-day authentication | E3 / decision 4 |
| 8 | Authorship | Chat senders are **cryptographically authenticated to each other**; an upload has a **provable owner** who may delete it, as the operator may. v5's node-asserted attribution is replaced | operator decision, §2.4b |
| 9 | Node authority | The operator may **close uploading to everyone but themselves**, per group. Signed MNP op, stored on the node, enforced by the node — the hidden button is a courtesy, the refusal is the control | §2.1b |
+| 10 | Client | A group's UI is a **set of pluggable applications** (Chat, Files today), not one monolithic page. Which are shown is a per-group, operator-signed setting on the same pattern as change 9 | §2.7 |
---
@@ -222,6 +224,43 @@ No parameter changes. `keyderive.py` now has a third consumer: the desktop clien
it, and the standing warning is unchanged — **never change those parameters in one
place**; a mismatch does not look like an error, it looks like an account nobody can open.
+### 2.7 The group UI becomes a set of applications
+
+New (2026-08-23). A group had two fixed tabs, Chat and Files, both defined inside one
+monolithic `static/app.js`. Two things motivated splitting it before adding to it: the file
+had become the thing every unrelated change touched, and the roadmap wants three more
+group-level surfaces — a Netflix-style video browser, a Spotify-style music player, a photo
+album viewer. None of the three need a protocol change: the node's indexer already
+classifies files as `video`/`audio`/`image`, and they would read the same `index_sync` /
+`file_req` / `stream_req` messages Files and the video player already use. What they need is
+somewhere to live that is not one file, and a way for an operator to turn one off.
+
+**The shape.** `group-page.js` is now the shell: the WebRTC connection, the file index, the
+tab bar, and the video/preview modals, none of which are Files- or Chat-specific. `apps.js`
+holds the registry — `[{ key, icon, labelKey, Component }]` — and every registered
+component receives the same props object from the shell, spread rather than hand-listed, so
+adding an app changes no code in the shell itself. Chat and Files each moved to their own
+file (`chat-app.js`, `files-app.js`) to prove the mechanism; nothing else exists yet.
+
+**Enablement is the same pattern as change 9, on purpose.** `apps_enabled` is a per-group
+setting: lives on the node (`roster.db`, not the hub, not `node.toml`, for the identical
+reason `member_upload` does — a hub or a config file that decided this would have authority
+over the node), changed by a signed operator instruction (`OP_APPS_ENABLED`), enforced by
+the node refusing to store an unrecognised or empty set rather than by the client's
+honesty. **Settings itself is not an app** and cannot be disabled — the one way back if
+everything else were turned off.
+
+**What this does not change.** No new server state on the hub (change 5 stands unmoved: the
+enabled-apps set is group-related state, and it lives on the node like everything else in
+that category). No new adversary or trust boundary — this is a display policy, not a key or
+a permission over content; a member whose client shows a hidden tab's data anyway would
+still be a member the node already serves that data to.
+
+Full detail — the props contract, the file layout, and a checklist for adding a new
+application — is `docs/apps.md`, on the same basis `docs/desktop-client-v1.md` holds the
+desktop client's detail: this document states what changed and what holds, not how to build
+on it.
+
---
## 3. Filesystem portability as a security property
@@ -302,3 +341,6 @@ design, which is long and belongs in one place:
**`docs/desktop-client-v1.md`** — shell requirements, device-linking protocol and schema,
account creation, node management over signed MNP ops, several roots per group, filesystem
portability, packaging and first run, the web tier, and the execution order for all of it.
+
+**`docs/apps.md`** — the group UI's plug-in architecture (§2.7): the props every
+application receives, the enablement mechanism end to end, and a checklist for adding one.