summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 00:48:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 00:48:55 +0200
commita36080742287bad8f657f688d7fec0022dd696a1 (patch)
tree3da5c9eedb9c0d2f22967ebf61bd777e32c7bb5e /packages/meshbay-hub/src/meshbay_hub
parent6828e64a256caea3c4e51829ae1aa09dfa725324 (diff)
downloadmeshbay-a36080742287bad8f657f688d7fec0022dd696a1.tar.gz
feat(client): HelloWorld, the reference application
Every other test of the plugin architecture reads source for the *absence* of app names. That proves nobody wrote a special case for Videos; it cannot prove a genuinely new application works, because there was no new application. This is one. It stores directories, appears as a tab, has a settings pane and lists files, and the node has never heard its name outside a single allow-list entry. Two files and one registry line, which is the claim `docs/refactor-groups.md` §4.1 makes. It ships hidden behind `?dev=1` (`dev: true` in the registry, the same opt-in shape as transport.js's `?trace=1`). Registering it normally would put a toy app in every operator's group; not registering it would prove nothing, since registration is exactly what is claimed to be sufficient. **Adding it found two places where the claim was nearly true rather than true, and both are fixed by making the code less app-specific:** `group-settings.js` fell back to the whole registry when a group had no `enabled_apps` yet — which would have turned a hidden app on for everyone. It asks `availableApps()` now. `group-page.js` wrote out `videoDirectories` / `musicDirectories` / `photoDirectories` by hand, so a fifth app would have needed that file edited. It derives `<key>Directories` from the registry. Neither was found by reading; both were found by adding the app, which is the whole reason it exists. Verified in a real Electron window as well as by the tests: hidden by default, present with the flag, offered its own settings section, and listing exactly the files under its configured folder and its subfolders — not the ones beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/webapp.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/apps.js47
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js9
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js50
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js62
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css12
17 files changed, 254 insertions, 14 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
index 58040b6..3cfb208 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
@@ -46,6 +46,10 @@ _ASSETS = ("style.css", "keyderive.js", "crypto.js", "transport.js", "app.js",
"settings-ui.js", "folder-tree.js",
"chat-app-settings.js", "video-app-settings.js",
"music-app-settings.js", "photos-app-settings.js",
+ # The reference app (docs/refactor-groups.md §4.1). Hidden behind
+ # `?dev=1` client-side, but it is still served and still cached, so
+ # it participates in the hash like anything else here.
+ "helloworld-app.js", "helloworld-app-settings.js",
# Pages extracted from app.js — statically imported or lazy-loaded,
# but all must participate in the content hash.
"auth-page.js", "explore-page.js", "create-group-page.js",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/apps.js b/packages/meshbay-hub/src/meshbay_hub/static/apps.js
index 6e62a42..461bd57 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/apps.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/apps.js
@@ -7,6 +7,33 @@ import { ChatSettings } from './chat-app-settings.js';
import { VideoSettings } from './video-app-settings.js';
import { MusicSettings } from './music-app-settings.js';
import { PhotoSettings } from './photos-app-settings.js';
+import { HelloWorldApp } from './helloworld-app.js';
+import { HelloWorldSettings } from './helloworld-app-settings.js';
+
+/**
+ * Whether apps marked `dev` are shown. Opt in once with `?dev=1`, off with
+ * `?dev=0` — persisted, the same shape as transport.js's `?trace=1`.
+ *
+ * HelloWorld exists to prove that adding an application is a registry entry
+ * and two files. Registering it normally would put a toy app in every
+ * operator's group; leaving it unregistered would prove nothing, since the
+ * claim is precisely that registration is enough. So it is registered, and
+ * hidden behind a flag that changes nothing else.
+ */
+const DEV_KEY = 'mb_dev_apps';
+(function _initDevFlag() {
+ try {
+ const params = new URLSearchParams(location.search);
+ if (params.has('dev')) {
+ if (params.get('dev') === '0') localStorage.removeItem(DEV_KEY);
+ else localStorage.setItem(DEV_KEY, '1');
+ }
+ } catch { /* localStorage unavailable — dev apps stay hidden */ }
+})();
+
+function devAppsShown() {
+ try { return localStorage.getItem(DEV_KEY) === '1'; } catch { return false; }
+}
/**
* Every group "application", in tab order.
@@ -43,18 +70,30 @@ const APPS = [
Component: MusicApp, Settings: MusicSettings },
{ key: 'photo', icon: 'image', labelKey: 'group.tab_photos',
Component: PhotosApp, Settings: PhotoSettings },
+ // The reference implementation (docs/refactor-groups.md §4.1). `dev` keeps
+ // it out of an operator's way; everything else about it is an ordinary
+ // entry, which is the point.
+ { key: 'helloworld', icon: 'chat', labelKey: 'group.tab_helloworld',
+ Component: HelloWorldApp, Settings: HelloWorldSettings, dev: true },
];
+/** Everything a reader of this page is allowed to see, in registry order. */
+function availableApps() {
+ const dev = devAppsShown();
+ return APPS.filter(a => dev || !a.dev);
+}
+
/** The registry filtered to what this group has enabled, in registry order. */
function visibleApps(enabledKeys) {
+ const registered = availableApps();
const enabled = new Set(
- enabledKeys && enabledKeys.length ? enabledKeys : APPS.map(a => a.key));
- return APPS.filter(a => a.alwaysEnabled || enabled.has(a.key));
+ enabledKeys && enabledKeys.length ? enabledKeys : registered.map(a => a.key));
+ return registered.filter(a => a.alwaysEnabled || enabled.has(a.key));
}
/** The apps the Settings page offers a section for, in registry order. */
function configurableApps() {
- return APPS.filter(a => !a.alwaysEnabled && a.Settings);
+ return availableApps().filter(a => !a.alwaysEnabled && a.Settings);
}
-export { APPS, visibleApps, configurableApps };
+export { APPS, availableApps, visibleApps, configurableApps };
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 0a43724..6f41426 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -9,7 +9,7 @@ import {
HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken,
_loadBundleKey, _loadRecoveryKey, _storeBundleKey,
} from './hub-client.js';
-import { visibleApps } from './apps.js';
+import { APPS, visibleApps } from './apps.js';
import { GroupName } from './group-name.js';
import { FilePreview } from './files-app.js';
import { VideoPlayer } from './video-player.js';
@@ -654,10 +654,19 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// `group-settings.js` untouched — that page renders the panes without
// knowing what any of them is for, which is what makes adding an app a
// registry entry rather than an edit to the page.
+ // `<key>Directories` for every registered app, derived from the registry
+ // rather than written out. Naming them here would mean adding an app
+ // required editing this file, which is the one thing the plugin
+ // architecture is supposed to have removed — and the reference app
+ // (docs/refactor-groups.md §4.1) is what made the difference visible.
+ const perAppDirectories = useMemo(() => {
+ const out = {};
+ for (const app of APPS) out[`${app.key}Directories`] = appDirs(app.key);
+ return out;
+ }, [appDirs]);
+
const appSettings = useMemo(() => ({
- videoDirectories: appDirs('video'),
- musicDirectories: appDirs('music'),
- photoDirectories: appDirs('photo'),
+ ...perAppDirectories,
chatDirectory,
chatLinkPreview,
tmdbEnabled: tmdbConfig ? tmdbConfig.enabled !== false : true,
@@ -665,7 +674,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
tmdbTokenCustomized: Boolean(tmdbConfig && tmdbConfig.tokenCustomized),
musicbrainzEnabled: musicbrainzConfig
? musicbrainzConfig.enabled !== false : true,
- }), [appDirs, chatDirectory, chatLinkPreview, tmdbConfig, musicbrainzConfig]);
+ }), [perAppDirectories, chatDirectory, chatLinkPreview, tmdbConfig,
+ musicbrainzConfig]);
const commonProps = {
groupId, transportRef, gekRef, status, username,
@@ -676,9 +686,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// Plural everywhere: Videos and Music read a list now, and Photos always
// did. The scalar `videoRoot`/`audioRoot` shapes survive only on the wire,
// for a node that speaks MNP 1.0 — nothing in the client carries them.
- videoDirectories: appDirs('video'),
- musicDirectories: appDirs('music'),
- photoDirectories: appDirs('photo'),
+ ...perAppDirectories,
tmdbConfig,
musicbrainzConfig, onPlayQueue,
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index 6c6f3d9..c80cff5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -5,7 +5,7 @@ import { t } from './i18n.js';
import { Icon } from './icon.js';
import { CollapsibleSection, ToggleSwitch } from './settings-ui.js';
import { hubFetch, navigate } from './hub-client.js';
-import { APPS, configurableApps } from './apps.js';
+import { availableApps, configurableApps } from './apps.js';
import * as platform from './platform.js';
@@ -559,7 +559,12 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
const [appsBusy, setAppsBusy] = useState(false);
const [appsMsg, setAppsMsg] = useState('');
- const activeApps = enabledApps && enabledApps.length ? enabledApps : APPS.map(a => a.key);
+ // `availableApps()` rather than the raw registry: an app the reader is not
+ // shown must not be turned on for the whole group by falling back to "all of
+ // them". Found by adding one that is hidden by default — an ordinary app
+ // would never have exposed the difference.
+ const activeApps = enabledApps && enabledApps.length
+ ? enabledApps : availableApps().map(a => a.key);
/**
* Toggle one app in or out of the group's enabled set. Same shape as
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js
new file mode 100644
index 0000000..9058199
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js
@@ -0,0 +1,50 @@
+import { html, useState, useEffect } from './vendor/htm-preact.js';
+import { t } from './i18n.js';
+import { useSaver } from './settings-ui.js';
+import { FolderPickerField } from './folder-tree.js';
+
+/**
+ * HelloWorld's settings: one folder, and nothing else.
+ *
+ * This is the whole server-side surface an application needs — no op, no MNP
+ * message, no route, no roster accessor. `saveDirectories` is bound to this
+ * app's registry key by the page, and `ops.set_app_directories` stores the row
+ * under that name without knowing what it is.
+ *
+ * It takes the shared props and no others (`docs/apps.md` §3b), which is what
+ * `test_app_settings_plugin.py` checks of every pane — including this one, so
+ * the reference implementation is held to the contract it demonstrates.
+ */
+function HelloWorldSettings({ roots, dirs, settings, saveDirectories }) {
+ const { busy, msg, run } = useSaver();
+ const [directories, setDirectories] = useState(
+ settings.helloworldDirectories || []);
+
+ useEffect(() => {
+ setDirectories(settings.helloworldDirectories || []);
+ }, [settings.helloworldDirectories]);
+
+ const current = settings.helloworldDirectories || [];
+ const dirty = directories.length !== current.length
+ || directories.some((d, i) => d !== current[i]);
+
+ return html`
+ <div class="app-settings">
+ <${FolderPickerField}
+ label=${t('settings_app.helloworld_directory_label')}
+ hint=${t('settings_app.helloworld_directory_hint')}
+ roots=${roots} dirs=${dirs} mode="single"
+ value=${directories[0] || ''} disabled=${busy}
+ onChange=${(path) => setDirectories(path ? [path] : [])} />
+
+ <button class="btn btn-small btn-secondary" style="margin-top:4px"
+ disabled=${busy || !dirty}
+ onClick=${() => run(() => saveDirectories(directories))}>
+ ${busy ? t('settings_app.saving') : t('settings_app.save')}
+ </button>
+ ${msg && html`<p class="settings-hint">${msg}</p>`}
+ </div>
+ `;
+}
+
+export { HelloWorldSettings };
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js
new file mode 100644
index 0000000..01b4ed4
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js
@@ -0,0 +1,62 @@
+import { html, useMemo } from './vendor/htm-preact.js';
+import { t } from './i18n.js';
+import { Icon } from './icon.js';
+import { formatSize } from './file-utils.js';
+
+/**
+ * The smallest application this platform can host, and the proof that adding
+ * one costs nothing outside its own two files.
+ *
+ * Everything else in `docs/refactor-groups.md` §3 is asserted by tests that
+ * read source. This is the other kind of evidence: an app nobody wrote a line
+ * of plumbing for, that stores directories, appears as a tab, and lists files —
+ * because the registry entry beside it is genuinely all there is.
+ *
+ * **Not shipped to operators.** It is registered with `dev: true`, which keeps
+ * it out of the tab bar and the settings page unless the reader opts in with
+ * `?dev=1` (same shape as transport.js's `?trace=1`). Deleting the flag would
+ * put a toy app in everybody's group; deleting the app would leave the claim
+ * resting entirely on tests that read text.
+ *
+ * It takes the standard props — see `docs/apps.md` §2 — and reads
+ * `helloworldDirectories`, which nothing on the node knows about by name:
+ * `ops.set_app_directories` keys the row by whatever the app is called.
+ */
+function HelloWorldApp({ entries, availableEntries, helloworldDirectories, status }) {
+ const dirs = helloworldDirectories || [];
+ const pool = availableEntries || entries || [];
+
+ const files = useMemo(() => pool.filter((e) => {
+ const p = e.path || '';
+ return dirs.some((d) => p === d || p.startsWith(d + '/'));
+ }).slice(0, 200), [pool, dirs]);
+
+ if (status !== 'connected') {
+ return html`<p class="page-message">${t('status.connecting_short')}</p>`;
+ }
+
+ return html`
+ <div class="page-content">
+ <h2>${t('helloworld.greeting')}</h2>
+ ${dirs.length === 0 ? html`
+ <p class="page-message">${t('helloworld.no_directory')}</p>
+ ` : html`
+ <p class="settings-hint">
+ ${t('helloworld.counted', { n: files.length })}
+ ${' — '}${dirs.join(', ')}
+ </p>
+ <ul class="hw-list">
+ ${files.map((e) => html`
+ <li key=${e.id} class="hw-item">
+ <${Icon} name="folder" />
+ <span class="hw-name">${e.name}</span>
+ <span class="hw-size">${formatSize(e.size || 0)}</span>
+ </li>
+ `)}
+ </ul>
+ `}
+ </div>
+ `;
+}
+
+export { HelloWorldApp };
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index c6d0331..d05d3a3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -126,6 +126,12 @@ export default {
'group.tab_video': 'Videos',
'group.tab_music': 'Musik',
'group.tab_photos': 'Fotos',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Hallo, Welt!',
+ 'helloworld.no_directory': 'Noch kein Ordner gewählt. Wählen Sie einen in den Einstellungen.',
+ 'helloworld.counted': '{n} Datei(en)',
+ 'settings_app.helloworld_directory_label': 'Ordner',
+ 'settings_app.helloworld_directory_hint': 'Der Ordner, den diese Referenz-App auflistet. Sie zeigt, dass eine neue Anwendung nichts außer ihren beiden Dateien braucht.',
'group.tab_members': 'Mitglieder',
'group.tab_settings': "Einstellungen",
'members.danger_leave_hint': "Sie verlieren den Zugriff auf die Dateien und den Chat dieser Gruppe.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 0831aa3..6dbb70f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -127,6 +127,12 @@ export default {
'group.tab_video': 'Videos',
'group.tab_music': 'Music',
'group.tab_photos': 'Photos',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Hello, World!',
+ 'helloworld.no_directory': 'No folder chosen yet. Pick one in Settings.',
+ 'helloworld.counted': '{n} file(s)',
+ 'settings_app.helloworld_directory_label': 'Folder',
+ 'settings_app.helloworld_directory_hint': 'The folder this reference app lists. It exists to show that adding an application needs nothing beyond its own two files.',
'group.tab_members': 'Members',
'group.tab_settings': "Settings",
'members.danger_leave_hint': "You will lose access to this group's files and chat.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 18570cc..aa950cc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -124,6 +124,12 @@ export default {
'group.tab_video': 'Vídeos',
'group.tab_music': 'Música',
'group.tab_photos': 'Fotos',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': '¡Hola, mundo!',
+ 'helloworld.no_directory': 'Aún no hay carpeta elegida. Elige una en los ajustes.',
+ 'helloworld.counted': '{n} archivo(s)',
+ 'settings_app.helloworld_directory_label': 'Carpeta',
+ 'settings_app.helloworld_directory_hint': 'La carpeta que lista esta aplicación de referencia. Existe para mostrar que añadir una aplicación no requiere más que sus dos archivos.',
'group.tab_members': 'Miembros',
'group.tab_settings': "Ajustes",
'members.danger_leave_hint': "Perderá el acceso a los archivos y al chat de este grupo.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 65021e3..daa9f94 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -125,6 +125,12 @@ export default {
'group.tab_video': 'Vidéos',
'group.tab_music': 'Musique',
'group.tab_photos': 'Photos',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Bonjour, monde !',
+ 'helloworld.no_directory': 'Aucun dossier choisi. Choisissez-en un dans les réglages.',
+ 'helloworld.counted': '{n} fichier(s)',
+ 'settings_app.helloworld_directory_label': 'Dossier',
+ 'settings_app.helloworld_directory_hint': 'Le dossier que cette application de référence liste. Elle existe pour montrer qu\'ajouter une application ne demande rien de plus que ses deux fichiers.',
'group.tab_members': 'Membres',
'group.tab_settings': "Paramètres",
'members.danger_leave_hint': "Vous perdrez l’accès aux fichiers et à la discussion de ce groupe.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 7f58764..364c26c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -125,6 +125,12 @@ export default {
'group.tab_video': 'Video',
'group.tab_music': 'Musica',
'group.tab_photos': 'Foto',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Ciao, mondo!',
+ 'helloworld.no_directory': 'Nessuna cartella scelta. Scegline una nelle impostazioni.',
+ 'helloworld.counted': '{n} file',
+ 'settings_app.helloworld_directory_label': 'Cartella',
+ 'settings_app.helloworld_directory_hint': 'La cartella elencata da questa applicazione di riferimento. Serve a mostrare che aggiungerne una non richiede altro che i suoi due file.',
'group.tab_members': 'Membri',
'group.tab_settings': "Impostazioni",
'members.danger_leave_hint': "Perderai l’accesso ai file e alla chat di questo gruppo.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 0e2dd42..f778fc4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -123,6 +123,12 @@ export default {
'group.tab_video': '動画',
'group.tab_music': '音楽',
'group.tab_photos': '写真',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'ハロー、ワールド!',
+ 'helloworld.no_directory': 'フォルダーが未選択です。設定で選んでください。',
+ 'helloworld.counted': '{n} 件のファイル',
+ 'settings_app.helloworld_directory_label': 'フォルダー',
+ 'settings_app.helloworld_directory_hint': 'この参照アプリが一覧表示するフォルダーです。アプリの追加に必要なのは自身の 2 つのファイルだけであることを示すために存在します。',
'group.tab_members': 'メンバー',
'group.tab_settings': "設定",
'members.danger_leave_hint': "このグループのファイルとチャットにアクセスできなくなります。",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 9f74e19..6e6842e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -126,6 +126,12 @@ export default {
'group.tab_video': "Video's",
'group.tab_music': 'Muziek',
'group.tab_photos': "Foto's",
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Hallo, wereld!',
+ 'helloworld.no_directory': 'Nog geen map gekozen. Kies er een bij Instellingen.',
+ 'helloworld.counted': '{n} bestand(en)',
+ 'settings_app.helloworld_directory_label': 'Map',
+ 'settings_app.helloworld_directory_hint': 'De map die deze referentie-app toont. Hij bestaat om te laten zien dat een app toevoegen niets meer vergt dan zijn eigen twee bestanden.',
'group.tab_members': 'Leden',
'group.tab_settings': "Instellingen",
'members.danger_leave_hint': "U verliest de toegang tot de bestanden en de chat van deze groep.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 083d72d..c8cbd16 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -130,6 +130,12 @@ export default {
'group.tab_video': 'Wideo',
'group.tab_music': 'Muzyka',
'group.tab_photos': 'Zdjęcia',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Witaj, świecie!',
+ 'helloworld.no_directory': 'Nie wybrano folderu. Wybierz go w ustawieniach.',
+ 'helloworld.counted': 'Plików: {n}',
+ 'settings_app.helloworld_directory_label': 'Folder',
+ 'settings_app.helloworld_directory_hint': 'Folder wypisywany przez tę aplikację referencyjną. Istnieje, aby pokazać, że dodanie aplikacji nie wymaga niczego poza jej dwoma plikami.',
'group.tab_members': 'Członkowie',
'group.tab_settings': "Ustawienia",
'members.danger_leave_hint': "Utracisz dostęp do plików i czatu tej grupy.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index bdc6c3b..6eb2242 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -126,6 +126,12 @@ export default {
'group.tab_video': 'Vídeos',
'group.tab_music': 'Música',
'group.tab_photos': 'Fotos',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': 'Olá, mundo!',
+ 'helloworld.no_directory': 'Nenhuma pasta escolhida. Escolha uma nas configurações.',
+ 'helloworld.counted': '{n} arquivo(s)',
+ 'settings_app.helloworld_directory_label': 'Pasta',
+ 'settings_app.helloworld_directory_hint': 'A pasta que este aplicativo de referência lista. Existe para mostrar que adicionar um aplicativo não exige nada além de seus dois arquivos.',
'group.tab_members': 'Membros',
'group.tab_settings': "Configurações",
'members.danger_leave_hint': "Você perderá o acesso aos arquivos e ao chat deste grupo.",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 0a1fe1a..044611e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -123,6 +123,12 @@ export default {
'group.tab_video': '视频',
'group.tab_music': '音乐',
'group.tab_photos': '照片',
+ 'group.tab_helloworld': 'HelloWorld',
+ 'helloworld.greeting': '你好,世界!',
+ 'helloworld.no_directory': '尚未选择文件夹。请在设置中选择。',
+ 'helloworld.counted': '{n} 个文件',
+ 'settings_app.helloworld_directory_label': '文件夹',
+ 'settings_app.helloworld_directory_hint': '该参考应用所列出的文件夹。它的存在是为了说明:新增一个应用只需要它自己的两个文件。',
'group.tab_members': '成员',
'group.tab_settings': "设置",
'members.danger_leave_hint': "您将无法再访问该群组的文件和聊天。",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index c5d5ce3..9c0d869 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -4176,3 +4176,15 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
.folder-field-tbl { margin-bottom: 8px; }
.folder-field-tbl .sdt-col-dir { font-size: 0.9em; }
.folder-field-actions { display: flex; align-items: center; gap: 10px; }
+
+/* HelloWorld (docs/refactor-groups.md §4.1) — the reference app, hidden
+ behind ?dev=1. Deliberately plain: it exists to prove the plumbing, and
+ anything decorative here would be a second thing to keep working. */
+.hw-list { list-style: none; margin: 12px 0 0; padding: 0; }
+.hw-item {
+ display: flex; align-items: center; gap: 8px;
+ padding: 6px 0; border-bottom: 1px solid var(--border);
+}
+.hw-item .icon { width: 15px; height: 15px; flex-shrink: 0; }
+.hw-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.hw-size { flex: 0 0 auto; color: var(--text-dim); font-size: 0.85em; }