18 KiB
NE Hub
One installable web app (PWA) for Seafile, OnlyOffice and your four webmail accounts. Mobile-first; bottom tabs on phones, side rail on iPad and desktop (tuned for 12.9" at 1024 and 1366 wide). On a tablet or computer the menu collapses to icons (button at the bottom of the rail) or hides completely (the menu button at the top, or Ctrl/Cmd+B); the choice is remembered on that device.
Configure
cp .env.example .env # set ONLYOFFICE_JWT_SECRET (same value as the Document Server) and PUBLIC_URL
PUBLIC_URL must be an address the OnlyOffice server can reach (not localhost): it posts saved edits back there.
Run
docker compose up -d --build # http://localhost:8080
# or: node server.js
Put it behind your reverse proxy with HTTPS (needed for "Add to Home Screen" and the service worker),
e.g. hub.necloud.us. Settings: SEAFILE_URL, ONLYOFFICE_URL env vars in docker-compose.yml.
What each part does
- Files: signs in with your Seafile account, browses libraries, uploads, creates folders, downloads, deletes.
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.
- 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). - People: you (the admin) add people in Settings -> People, choose which tabs each may use (Files, Office, Music, Audiobooks, Podcast, Podcast Admin, Mail,
Calendar, Tasks, Search, Links; Home and Settings are always on), and give them a one-time invite code to create their own passkey. Everyone's tasks, NE Hub events,
settings, iCloud connection, calendar links and Plex sign-in are private to them (
/data/users/<id>/). Limits are enforced by the server, not just hidden. You can change a person's tabs, sign them out everywhere, turn them off, or delete them and their data. An existing single-user install becomes the admin automatically on first start (the original files are left in place as a backup). - Files: a Finder/Explorer-style browser. Tap Select (or long-press on a phone, Cmd/Ctrl-click or Shift-click on a computer) to pick several items and Move, Copy (to any folder in any library, with a New folder button in the picker) or Delete them together. Rename checks for clashes, sort by name/date/size/kind (folders stay on top), filter a folder, right-click for the item menu, and keys: Ctrl/Cmd-A, Delete, F2, Enter, Esc. Drag files or whole folders from your computer onto the page (or onto a folder in the list) to upload; uploads show progress, retry on a bad connection, and can be cancelled. Drag items onto a folder or a breadcrumb to move them (hold Alt/Option to copy). Drag a single file out to your desktop to download it (Chrome and Edge only; browsers cannot drag out folders or several files). Also share links, image/PDF/video/audio preview, and New document/spreadsheet/presentation.
- Calendar: month grid and agenda. Add, edit and delete events:
- NE Hub calendar: events you create are stored in
/data/events.jsonand published as a private subscribe link (Manage calendars -> copy link) so Outlook, Google or Apple Calendar can show them (read-only there). - iCloud (two-way): connect with an Apple ID and an app-specific password (appleid.apple.com). Credentials are
encrypted in
/data/icloud.json; only*.icloud.comis contacted. Edits keep alarms and attendees intact. - ICS links (Google secret address, Outlook/M365 published calendar) are read-only.
- NE Hub calendar: events you create are stored in
- Tasks: to-do lists stored in NE Hub (
/data/tasks.json), with due dates, notes and completion. If the connected iCloud account exposes reminder lists over CalDAV they appear too and sync both ways. Apple hides Reminders lists that were upgraded for iOS 13+, so for most accounts only NE Hub lists are available. - Settings sync: your preferences (mail addresses, audiobooks sign-in (encrypted), Plex library, Office save location, calendar/task views, recent files) are saved on the server in
/data/settings.jsonand shared by every device. The Seafile sign-in token is saved there too (encrypted), so Settings shows Seafile as signed in on every device. Only the music queue stays in the browser. - 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 current and next two tracks are downloaded in full so changing songs is instant and a slow or lost connection does not stop playback; the Home dashboard and the Music tab show a live Now Playing card (follows the song, seek bar, shuffle/repeat, previous/next, lyrics, queue) above the rest; the queue (play next / add to queue) and position are remembered. The player reports what is playing to Plex (play, pause, stop, position every 10 s), so it appears under Now Playing / Activity in the Plex dashboard as "NE Hub", under the Plex account you signed in with. Lyrics and karaoke: the Lyrics button (on the Now Playing card, in the full player, and in the wide mini player) opens a full-screen lyrics view that follows the song, tap a line to jump to it, A−/A+ change the size, and Karaoke shows just the current line big with the colour sweeping across it as it is sung (word by word when the source has word timing, otherwise across the line). Lyrics come from Plex first if the song has them (embedded, a
.lrcfile next to it, or Plex Pass), otherwise from the bundled Lyrica service (lyricacontainer; sources LRCLIB, Musixmatch via Lrcmux, NetEase, Megalobiz; YouTube auto-captions are never used, and an answer for a recording of a very different length is refused). Lookups send the artist and title to those public sources and are kept in/data/lyricsso a song is looked up once. If the timing is off, the − / + buttons shift it half a second at a time (remembered per song); "Wrong lyrics?" lets you search with a corrected artist or title. Remove thelyricaservice and the hub'sLYRICS_API_URLline to skip the online lookup (Plex lyrics still work). No vocal removal. - Audiobooks: embeds audiobooks.necloud.us in a frame that keeps playing while you use other tabs, with reload, full screen and open-in-new-tab buttons.
Back-door sign-in: set
AUDIOBOOKS_LOGIN_LINKto the site's secret link (the key is part of it). The frame loads/api/audiobooks/enter, which checks the person may use the Audiobooks tab and redirects to that link, so the key stays in the server's environment and never appears in the app's page, script or logs. Anyone holding the link is signed in there with full access, so keep it out of the repo and.envbackups. Or setAUDIOBOOKS_SSO_SECRET(and optionallyAUDIOBOOKS_SSO_URL, default<AUDIOBOOKS_URL>/sso) for short-lived signed links:?token=<body>.<sig>, wherebodyis base64url JSON{u, id, exp, n}andsigis the base64url HMAC-SHA256 ofbody. Without either, the plain site opens and the saved username can be copied. - Signal: your Signal chats inside the app, on desktop-class screens only (a window at least 900 px wide with a mouse or trackpad; phones and touch tablets don't
see it). Besides the Signal tab there is a chat button on every screen that opens a panel over whatever you are doing (close it with the x; the
expand button opens the full page). New messages get your attention with a toast, a chime, the unread count in the browser tab title and on the installed app's icon, and
(if you switch it on in Settings -> Signal alerts) a desktop notification when the app is in the background; sound and notifications are saved per device. It talks to your account through the community
signal-cli-rest-apibridge, which is one more linked device of your account (like Signal Desktop; not an official Signal product). Thesignalcontainer indocker-compose.ymlruns it; remove that service and the hub'sSIGNAL_API_URLline to skip Signal. First time: open the Signal tab, then on your phone Signal -> Settings -> Linked devices -> Link new device and scan the code. You can send and receive text, photos, files, voice messages (played, not recorded), replies, reactions, groups and read receipts; there are no calls. In a chat, the ... button edits the contact (the name you give is shown in the app and sent to Signal), clears the messages, or removes the chat from the app (it stays on your phone, and a new message starts it again); right-click a chat in the list for the same menu. A diagnostic list of what arrived lately (kinds only, no message text) is at/api/signal/debug. Only messages from after linking appear (Signal does not copy older ones to a new device) and they are kept in/data/signal. The link keys live in thenehub-signalvolume (a Docker volume, not a host folder: the bridge runs as user 1000 and a root-owned host folder gives "permission denied"; if you do want a folder,chown -R 1000:1000it first): whoever gets into your server can read your chats, so only give the Signal tab to people you would trust with your Signal (it is one shared account, admins always have it). Signal drops a linked device that is not connected for about a month; the hub stays connected, so this only matters if the stack is switched off. - Links: your own bookmarks page. Add a name, address and optional group; filter as you type; opens in a new tab. Saved with your settings on the server, private to you, same on every device. Only http(s) addresses are accepted.
- Security and backups: every response carries
nosniff, aframe-ancestors 'self'policy and a same-origin referrer policy; writes that come from another website are refused; JSON files are written atomically (temp file, then rename). Admins get Settings -> Storage -> Download a backup (a .tar.gz of/data; keep it private)./healthzanswers without signing in and the container has a health check. In-app OnlyOffice editing now requiresONLYOFFICE_JWT_SECRET(orALLOW_UNSIGNED_OO=1for testing).npm testruns every test script; CI also runsnpm audit. - Command palette and quick add: press Ctrl/Cmd-K anywhere (or the Command palette button on Home) to jump to any page, create things, open a saved link or find a task/event. Typing a sentence offers Add task, and understands a date at the end ("Call mom tomorrow", "Pay rent in 3 days", "Dentist fri", "2026-12-25"). Quick add on Home does the same plus new event and new link. Search also finds your links.
- Tasks: repeating tasks (daily, weekdays, weekly, monthly, yearly; finishing one moves it to its next date) and one level of subtasks, in NE Hub lists (iCloud reminder lists stay as Apple stores them).
- Calendar: Month, Week, Day and Agenda. Week and Day show a time grid with overlapping events side by side, an all-day strip and a "now" line; tap an empty slot to add an event.
- Music playlists and sleep timer: playlists can be created, added to ("Add to playlist" on any track, or "Save as playlist" from the queue), renamed, have tracks removed and be deleted. The hub only lets playlist writes through to Plex; everything else on the Plex server stays read-only. The Sleep button (Now Playing card, or the queue) fades the music out after 15-90 minutes or when the song ends.
- Offline and accessibility: the app page and its scripts are cached so it opens without a connection, with a notice while offline. Icon buttons have names, there is a skip link, sheets close with Escape and keep focus inside, and the command palette is a proper combobox.
- Reminders (Web Push): Settings -> Reminders turns on notifications on that device for tasks due today (one morning summary, at the time you choose) and events about to start. Works with the app closed; on iPhone/iPad only from the installed home-screen app. Keys are made once in
/data/vapid.json; setVAPID_SUBJECTto amailto:address if you want one in the push messages. Needs HTTPS. - Recovery codes and activity log: Settings -> Recovery codes makes 8 one-time codes; if you lose every device, the sign-in page's "Use an invite or recovery code" adds a new passkey with one (codes are stored only as hashes). Admins see an Activity list in Settings (sign-ins, failed sign-ins, passkeys added or removed, invites, recovery code use, people changes, backups); it is
/data/audit.logand never contains codes or passwords. - File version history: on any Seafile file, "Version history" lists earlier versions with who and when, and can download or restore one (restoring keeps the current version in the history). This uses Seafile's history API and has not been tried against a live Seafile server yet.
- Code layout:
public/holds one script per feature (tasks.js,calendar.js,music.js,player.js,files.js,settings.js, ...). They are plain scripts that share one global scope and load in the order listed inindex.html(boot.jslast); the versioned?v=addresses and the cache version are computed from every file inpublic/. - Podcast and Podcast Admin: launchers that open
PODCAST_URL(a home-network address) andVERSE_URLin their own tabs. - Office: embeds onlyoffice.necloud.us in a frame, with an "open in new tab" fallback if it blocks framing.
- Mail: Cypht comes with the project (a second container in
docker-compose.yml), so the Mail tab can open a full webmail inside the app (the shortcut cards are the default page; the Open webmail button there opens it, and the envelope button in the webmail goes back), built for several accounts at once (iCloud, Gmail, Outlook.com, Office365, Yahoo, Fastmail and others are presets, plus any IMAP server) with combined views of them all. The hub passes/webmail/through to it, so it is not exposed to the internet, sits behind your passkey sign-in and shares the hub's address (which is what lets it be framed). SetCYPHT_PASSWORD(any password you choose; required) in.envor your stack; the hub uses it to sign in to Cypht for you, so you never see a second login. Then, once, in the Mail tab: Add an E-mail Account, pick the provider, and follow the steps (iCloud, Yahoo and most others: your address and an app password, e.g. appleid.apple.com -> App-Specific Passwords; Gmail needs 2-step verification, then Google account -> App passwords). Everything is kept in thenehub-cyphtvolume. Everyone who has the Mail tab shares this one Cypht account; for separate mailboxes give only the people who should share it the tab, or rundocker exec <cypht container> php /usr/local/share/cypht/scripts/create_account.php <name> <password>and sign in as that person in Cypht (the automatic sign-in is then skipped by logging out of Cypht once). The default Mail page is the Gmail / Outlook.com / Microsoft 365 / iCloud web shortcuts, and Settings -> Mail apps chooses which of those cards show. Sign in with Google or Microsoft instead of app passwords (optional): create an OAuth app and put its ID and secret in the environment of thecyphtservice (GMAIL_CLIENT_ID/GMAIL_CLIENT_SECRETfrom console.cloud.google.com, with<your hub address>/webmail/?page=homeas the authorized redirect URI;OUTLOOK_CLIENT_ID/OUTLOOK_CLIENT_SECRETfor a personal Outlook.com account andOFFICE365_CLIENT_ID/OFFICE365_CLIENT_SECRETplusM365_TENANTfor a work or school account, from a Microsoft Entra app registration with the same redirect URI and the delegated permissionsIMAP.AccessAsUser.All,SMTP.Send,offline_access). SetPUBLIC_URL(your hub address) so the redirect address is right. The provider's sign-in page cannot be shown inside a frame, so that step opens in the whole window and returns you to the Mail tab. The Gmail and Microsoft sign-ins have not been tried against the real providers; the Microsoft ones use a patched permission list because Cypht's own preset asks for a retired one. To use a webmail you run elsewhere instead, setWEBMAIL_URL(it must allow being framed: noX-Frame-Options, andContent-Security-Policy: frame-ancestors https://<your hub address>). With neither, Mail is just the shortcut cards.
Install on iPhone / iPad (PWA)
Open the site in Safari (it must be the https:// address), tap Share -> Add to Home Screen. The home-screen app has proper
icons (iPhone, iPad), a splash screen for each iPhone and iPad size (including iPad Pro 12.9"), and opens full screen without Safari's bars.
iOS keeps a separate sign-in for the home-screen app, so sign in once inside it with your passkey; it then stays signed in
(SESSION_DAYS, default 180, renewed whenever you use it). Regenerate the artwork with python3 tools/make-icons.py (needs Pillow).
Build and deploy with Gitea
.gitea/workflows/build.yml builds the container on every push to main (and v* tags) and pushes it to
git.necloud.us/nate/hub (tags: latest, short commit sha, git tag), and the webmail container from cypht/ (the official Cypht plus the hub's settings) to
git.necloud.us/nate/hub-cypht. The hub image is amd64 and arm64; the webmail and lyrics images are amd64 only.
One-time setup in Gitea: create an access token with the write:package scope, then add it to the repo under
Settings -> Actions -> Secrets as REGISTRY_TOKEN (optionally REGISTRY_USER). The runner needs Docker.
On the server:
cp .env.example .env # fill in secrets, PUBLIC_URL, CYPHT_PASSWORD (choose one), etc.
docker compose pull && docker compose up -d
docker compose logs hub | grep "Setup code" # first sign-in: create your passkey