diff --git a/README.md b/README.md
index 3fa3fbc..505b7dd 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,11 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke
The server proxies `/sf/*` to Seafile because Seafile sends no CORS headers. Token lives in the browser only.
Office files open in Seafile's own OnlyOffice integration.
- **Edit here**: opens docs inside the app via OnlyOffice. The server signs each editor config with the JWT secret and saves edits back to Seafile.
-- **Music**: Plex player. Sign in goes through plex.tv (no password handled), the server proxies your Plex server and adds the token itself, and a mini player keeps playing as you move between tabs.
+- **Sign-in**: the whole app sits behind a passkey (WebAuthn). First run: open the site and enter the setup code (`HUB_SETUP_CODE`, or the one printed in the server log) to create a passkey; add more devices from Settings. Passkeys and the session secret live in `/data` (a Docker volume).
+- **Files**: also rename, move, share links, image/PDF/video/audio preview, and New document/spreadsheet/presentation.
+- **Calendar**: agenda from private ICS links (Google secret address, Outlook/M365 published link, iCloud public calendar). Links are stored on the server in `/data`.
+- **Search**: libraries, file names (scans libraries if Seafile has no search index) and Plex artists/albums/tracks.
+- **Music**: Plex player. Sign in goes through plex.tv (no password handled), the server proxies your Plex server and adds the token itself, and a mini player keeps playing as you move between tabs; the queue (play next / add to queue) and position are remembered.
- **Audio**: embeds audiobooks.necloud.us; sign-in details saved in Settings can be copied with one tap.
- **Office**: embeds onlyoffice.necloud.us in a frame, with an "open in new tab" fallback if it blocks framing.
- **Mail**: deep links to Gmail, Outlook.com, Microsoft 365 and iCloud webmail (inbox + compose).
diff --git a/public/app.js b/public/app.js
index 04bac56..f64cde3 100644
--- a/public/app.js
+++ b/public/app.js
@@ -167,6 +167,24 @@ function viewHome() {
if (typeof renderAgenda === 'function') renderAgenda($('#agenda'), 'today');
}
+// Name search by listing each library recursively (cached for the session).
+const treeCache = new Map();
+async function walkSearch(repos, term) {
+ const out = [];
+ for (const r of repos.filter(x => !x.encrypted).slice(0, 12)) {
+ try {
+ let t = treeCache.get(r.id);
+ if (!t || Date.now() - t.at > 120e3) { t = { at: Date.now(), items: await sf.req(`/api2/repos/${encodeURIComponent(r.id)}/dir/?p=/&recursive=1`) }; treeCache.set(r.id, t); }
+ for (const it of t.items) if (it.name.toLowerCase().includes(term)) {
+ const parent = (it.parent_dir || '/').replace(/\/?$/, '/');
+ out.push({ repo_id: r.id, repo_name: r.name, name: it.name, fullpath: parent + it.name, is_dir: it.type === 'dir', size: it.size });
+ if (out.length >= 40) return out;
+ }
+ } catch {}
+ }
+ return out;
+}
+
// ---- Search: Seafile files + libraries and Plex music, all best-effort (a missing backend never blocks the others)
function viewSearch() {
$('#view').innerHTML = `