Security headers, cross-site write refusal, atomic JSON writes, admin backup download, /healthz and Docker health check, OnlyOffice requires a JWT secret, npm test, CI audit, arm64 hub image. New Links tab for personal bookmarks, saved with per-person settings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Home Now Playing block is now the same live card as the Music tab.
The player sends Plex timeline updates (playing/paused/stopped, position every 10 s).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- full-screen lyrics view that follows the song: highlighted current line with a colour sweep (word by word when the source has word timing), tap a line to jump, text size, timing nudge, 'Wrong lyrics?' search
- Karaoke mode: the current line large and centered
- lyrics from Plex first, then the bundled Lyrica service (LRCLIB, Musixmatch via Lrcmux, NetEase, Megalobiz); YouTube captions never used; wrong-length recordings refused; answers kept in /data/lyrics
- lyrica/: Lyrica pinned to one commit, built by the workflow as hub-lyrica; compose service sharing the hub's network
- tools/lyrics-test.js
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- read every JSON value in a frame (lines, arrays), binary frames too
- a person seen by number and by private id is one conversation (merged)
- delivery/read receipts match on message time, not on who sent them
- reconnect every 1-5 s; the chat list says when the link was down
- /api/signal/debug: kinds of messages seen lately (no text)
- edit a contact (kept in the app and sent to Signal), clear messages, remove a chat; right-click menu in the list; name for a new number
- tests: 54 checks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Music: a card with art, seek bar and controls above the albums, artists and playlists
- Signal: chime, optional desktop notification when the app is in the background, (n) in the tab title, app badge; Settings -> Signal alerts (per device)
- README, player test stand-in updated
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- signal.js: talks to signal-cli-rest-api (a linked device of your Signal account); keeps conversations in /data/signal; live updates; send/receive text, files, replies, reactions, groups, receipts
- Signal tab and a floating chat panel on every screen, shown only on desktop-class windows; new Signal permission per person
- compose: signal bridge container sharing the hub's network
- tools/signal-test.js (fake bridge), README
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes 'cannot reach the webmail: ENOTFOUND' on hosts (ZimaOS/CasaOS) that keep network_mode: bridge, where container names do not resolve.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- cypht/: official Cypht 2.13.0 on a SQLite file, served under /webmail/; workflow builds git.necloud.us/nate/hub-cypht
- the hub passes /webmail/ through behind the passkey sign-in and signs in to Cypht for you (CYPHT_PASSWORD)
- provider sign-in pages (Google, Microsoft) open in the whole window and return to the Mail tab; other links open in a new tab
- Gmail / Outlook.com / Office365 OAuth settings (Microsoft permissions patched to the current ones)
- Roundcube removed; README, compose and webmail tests updated
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- roundcube/: official Roundcube plus hub.php settings
- workflow builds and pushes git.necloud.us/nate/hub-roundcube, and runs the player and webmail tests
- compose uses the image instead of an inline config
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- docker-compose: roundcube service with inline config (Gmail/iCloud/Yahoo presets, framing allowed, persistent key); only reachable through the hub
- hub passes /webmail/ through to Roundcube behind the passkey sign-in; Microsoft's sign-in page opens in the whole window and returns to the Mail tab
- M365_* settings add 'Login with Microsoft 365' (README has the Entra app registration steps)
- Side menu collapses to icons or hides completely (remembered per device, Ctrl/Cmd+B)
- tools/webmail-test.js
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Mail page redesigned as one card per account (inbox + compose)
- Settings -> Mail apps: tick the apps you use; saved on the server per person
- WEBMAIL_URL shows your own webmail (e.g. Roundcube) inside the Mail tab; provider shortcuts one tap away
- README: Roundcube setup and framing headers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Files: multi-select, batch move/copy/delete, rename checks, sort, filter, shortcuts, drag in (files and folders) to upload with progress and retries, drag to move, drag a file out to download
- Mini player: previous, seek bar with buffered range, time, shuffle/repeat, buffering spinner, swipe for next/previous, close
- Back/Forward bar that knows what history is possible
- Audio tab is now Audiobooks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Seafile token is saved on the server (encrypted) so Settings shows the real signed-in state everywhere
- Player: whole-track cache for current and next two, stall recovery, instant track changes, shuffle/repeat, Plex play counts, transcode fallback, Plex request timeout
- Audio tab: toolbar, full-height kept-alive frame; /api/audiobooks/enter adds the secret login link on the server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The People screens did not appear after the redeploy even after a hard refresh: a caching layer
(for example Nginx Proxy Manager's "Cache Assets") was still serving the old app.js. The pages now
reference /app.js?v=<build>, /style.css?v=<build> etc., where the build changes with every release,
so a stale cached file at the old address is never used. The service worker no longer precaches
unversioned app code.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An open page kept running the old version after a deploy (the single page never reloads), so the new
People screens did not appear. Static files are now sent with no-cache + ETag (304 when unchanged), icons
are cached for a week, /api/config reports a build stamp, and the app shows "A new version is available -
Reload" when it comes back into view or every 10 minutes. The service worker cache name is bumped.
The server also logs, at most once a minute per reason, why it refused a session cookie (unknown account,
turned off, signed out everywhere, bad signature, expired) without printing the cookie.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The saved Plex server token or address can stop working (token rotated, server moved). That
looked like "signed out" and sent the person back to the sign-in screen. The proxy now asks plex.tv
(using the saved account sign-in) for a fresh token and address, retries once, and saves them.
Sign-ins saved before the server id was recorded are matched by server name. A revoked account
sign-in signs the person out cleanly; repairs are one at a time and pause 30s after a failure.
The server logs [plex] lines for sign-in, connect, repair and failure.
tools/plex-test.js (31 checks, fake plex.tv + fake Plex) runs in the Gitea workflow.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Accounts: the setup code (or an existing single-user install, migrated automatically) creates the admin;
the admin adds people in Settings -> People and gives each a one-time invite code to enrol their own passkey
- Each person's tasks, NE Hub events and subscribe link, settings, calendar links, iCloud connection and Plex
sign-in are private, stored in their own folder (/data/users/<id>/); Plex sign-in moved from browser cookies
into that folder (encrypted), so it also follows them across devices
- Tab access is enforced by the server (403) and reflected in the app (nav, Home, Settings, search, /api/config
hides addresses of tabs a person cannot use); browser storage is namespaced per person
- Admin tools: change tabs, invite a device, sign out everywhere (session epoch), turn off, make/remove admin,
delete a person and their data; the last admin cannot be removed
- Sessions carry the person (uid.epoch.expiry.signature); old single-user cookies still work as the admin
- tools/users-test.js (83 checks: isolation, access limits, invites, sign-out/disable/delete, Plex and iCloud
per person, migration) now runs in the Gitea workflow before the image is built
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Restore ddgAnswer, which an earlier edit deleted: /api/ddg threw a ReferenceError and killed the
whole server process whenever Search ran
- One failing request can no longer take the server down (errors are logged and answered with a 500)
- tools/smoke-test.js starts the real server and calls every endpoint; the Gitea workflow runs it before
building the image, so a missing function or a crash stops the build
- Search: also searches NE Hub tasks and calendar events; Seafile file search falls back to scanning file
names when the search index returns nothing; Plex search falls back to the library's artists/albums;
a status line shows what was searched, per-source counts and errors, with sign-in links
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
"mounted" was true even when /data was the hidden anonymous volume Docker creates for the
Dockerfile's VOLUME, so the app looked fine while the user's host folder stayed empty and the
data was lost when the container was recreated. Read the mount source and report host folder,
named volume or anonymous volume (flagged), in Settings and the startup log.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Preferences lived only in the browser, so clearing it, using another device or the separate iOS
home-screen storage lost them. They are now saved in /data/settings.json (the audiobooks sign-in
encrypted), loaded at start-up, and existing browser-only values are uploaded once. A failed save
is reported instead of silent.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Apple's listing was recognised as calendars but then discarded by the events/tasks filter.
Read component names from anywhere in the entry (any quoting or prefix), treat a missing or
empty list as events, and keep calendars as event calendars if every one would otherwise be
dropped. Errors now show each item with its component types.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Apple writes <calendar xmlns="urn:ietf:params:xml:ns:caldav"/> (attributes, no prefix), which the
previous pattern missed, so a valid sign-in reported "No calendars found". Match it with a
lookahead, check the HTTP status of the listing, and name what iCloud returned if nothing matches.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- iOS ignores SVG touch icons: add 120/152/167/180 apple-touch-icons, 192/512 and maskable manifest icons,
and 25 splash screens (every iPhone size, iPad portrait+landscape incl. 12.9")
- Icons and splash screens are public so iOS can fetch them without a session
- Manifest: id, scope, categories, shortcuts; iOS standalone polish (no callout, no overscroll)
- Sessions last SESSION_DAYS (default 180) and renew daily while in use
- tools/make-icons.py regenerates the artwork
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Lists, quick add, due dates, notes, complete/reopen, delete; Home card for overdue/due today
- iCloud VTODO lists read and written over CalDAV (If-Match, edits patch the latest copy so
priority/alarms survive); NE Hub lists work regardless
- Fix: text inside a nested VALARM was read as the task's / event's own description and then
written back on edit; nested components are now skipped (regression tests added)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Passkeys, calendars and sessions were being lost on redeploy. The server now
reports whether /data is a mounted volume, whether it is writable, what it
holds, and where the session secret comes from, so a wiped volume is obvious.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The server asks DuckDuckGo's instant-answer service (summary, definition, quick
answers) and Search shows it as a card above the app's own results when there
is one. Lookups are cached for 10 minutes and a failure never breaks Search.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both refuse to be framed and Duck.ai has no public API, so Search shows
"Ask Duck.ai" and "Search the web" buttons that open a new tab with the
query pre-filled.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Audio and Podcast now share kept-alive frames that stay loaded while hidden.
A plain-http address still falls back to a launcher (a secure page cannot
frame http content).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Add, edit and delete events; month/agenda views open them on tap
- NE Hub calendar stored on the server and published as a private ICS subscribe link
- iCloud CalDAV read/write with an app-specific password (encrypted at rest,
only *.icloud.com contacted, event addresses checked against the calendar)
- Edits patch the existing entry so alarms/attendees survive; writes use If-Match / If-None-Match
- Time zone definitions (VTIMEZONE) generated so other apps read times correctly
- Podcast tab (PODCAST_URL) alongside Podcast Admin
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The site sends X-Frame-Options: SAMEORIGIN, so it cannot be embedded; the tab
and a Home tile open it in its own browser tab. Address is VERSE_URL.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A signed-in device creates a short-lived, single-use code in Settings
("Invite a device"); the new device enters it on the sign-in page to create
its own passkey. Codes are throttled like other secrets and cannot be used
by signed-out clients to create more codes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lists every music library on the server, remembers the choice per server,
and shows it under the title with a picker.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Month view (default) with event dots on phones and event labels on
tablet/desktop, tap a day to see its events, previous/next/today.
The agenda list stays available.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
fetch() decompresses transparently, but the proxies forwarded the upstream
content-length, so browsers cut compressed Plex/Seafile responses short and
JSON.parse failed. Ask Plex for identity encoding and drop the stale length
whenever the upstream response was encoded.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The frame was rebuilt with the page content on every route change, which
stopped playback. It now lives outside the view and is only shown or hidden.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>