Files
hub/README.md

40 KiB
Raw Permalink Blame History

NE Hub

One installable web app (PWA) for Seafile, OnlyOffice, Plex, Signal, Outlook.com mail and more. 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

Install extras: Settings has an Install button where the browser offers one (Chrome, Edge, Android). The installed app's icon badge counts unread Signal chats, unread mail and tasks that are overdue or due today. On Android and desktop Chrome/Edge, NE Hub appears in the Share menu: share files to it, pick a Seafile folder, and they upload.

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).
  • Settings is split into General, Connections, Security and (for the admin) Admin tabs; the last one used is remembered on each device.
  • 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 calendars: make as many as you like (Manage calendars -> New calendar), each with its own name and color (a picker; iCloud, linked and shared calendars can be recolored too). Events carry their calendar's color everywhere and you pick the calendar when adding one. Older events stay in the first calendar.
    • Sharing: a calendar is private until you share it. In its Edit sheet, choose per person whether they can view or edit; the server enforces it. Each calendar also has its own private subscribe link (copy or make a new one), and there is still one link for all of yours, so Outlook, Google or Apple Calendar can show them (read-only there).
    • ICS links (Google secret address, Outlook/M365 published calendar) are read-only.
  • 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.
  • Plex PINs and editing: a Plex Home profile that has a PIN in Plex (the owner's included) asks for it every time you switch to it; plex.tv checks it and five wrong guesses make the hub wait ten minutes. The Edit button on a movie, show, season or episode (only when you are the Plex server's owner) opens the fields Plex's own Edit dialog has: title, sort title, original title, release date, content rating, studio, tagline, summary and genres, plus choosing a poster or background (or adding one from an image link). Only changed fields are saved, and each is locked in Plex so a refresh does not undo it. The hub's Plex proxy allows just those edit requests (nothing else on the server can be changed).
  • 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.json and 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. Plex Home users: if your Plex account has a Plex Home (managed users, kids, guests), the Music tab shows "Playing as " with a switch link; pick another person (and enter their PIN if they have one) and Music, playlists, ratings and the Plex dashboard follow that person. The account that signed in is kept so switching back needs no PIN. It is per hub person, remembered across restarts, and has only been tried against a fake plex.tv. 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 .lrc file next to it, or Plex Pass), otherwise from the bundled Lyrica service (lyrica container; 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/lyrics so 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 the lyrica service and the hub's LYRICS_API_URL line to skip the online lookup (Plex lyrics still work). No vocal removal.
  • Movies & TV (Plex): a tab under Media for your Plex movie and TV libraries, using the same Plex sign-in and Plex Home user as Music (sign in once in the Music tab). Continue Watching, a poster grid per library with Show more, search, a page for each movie, show, season and episode (summary, cast, genres, runtime), Mark watched/unwatched, and Resume or From the start. The player plays the file straight when the browser can (MP4 with H.264 and AAC); otherwise Plex converts it as it plays (HLS: native in Safari, with the bundled hls.js in public/vendor/ elsewhere), with a Quality choice (Auto, Original, 1080p, 720p, 480p), an audio-track choice and subtitles (drawn into the picture by Plex, so choosing one converts the video). It reports your position to Plex every 10 seconds (so Continue Watching and the Plex dashboard follow), marks an item watched at the end, offers the next episode with a 10-second countdown, stops the conversion when you leave, and pauses Music while a video plays. Controls: a seek bar with elapsed and remaining time, play/pause, back 10 s and forward 10 s, previous and next episode, volume, speed (0.75 to 2×), Quality & tracks, picture in picture and full screen, which hide after 3 seconds (click plays/pauses and double click goes full screen; on a touch screen tap shows the controls and double tap on the left or right side skips 10 s). Keys: space or k play/pause, left/right or j/l skip (shift or j/l = 30 s), up/down volume, 0-9 jump to 0-90%, n and p next and previous episode, f full screen, m mute. Bottom bar and mini picture: the video's controls live in the same bottom bar Music uses, on every tab: poster, title, time, a seek line you can tap or drag, back and forward 10 s, play/pause, and on wider screens previous and next episode and close. Tap the title to open the full player. Leave the player page and the picture keeps playing in a small window at the bottom right (click it to go back). On the player page, volume, speed, Quality & tracks, picture in picture, full screen and close are along the top; the on-video controls only appear in full screen. Music and video end each other (starting one stops the other). Every page shows where you are (library, show, season) with links back, and a pill for each library. Give it to people in Settings -> People (Movies & TV). Tried only against a fake Plex with a real test video, in a Safari-engine browser and through hls.js; how it behaves with your real files (conversion speed, subtitles, odd codecs) is the thing to try. Not included: downloads, casting (Chromecast/AirPlay), skip intro, text subtitles you can switch without converting.
  • Audiobooks is a built-in screen for the audiobook player at AUDIOBOOKS_URL: your library with covers, Continue listening, search, chapters, and playback in the same player as Music. The hub signs in to the player as you with the audiobook sign-in saved in Settings (or, if you have none, the shared AUDIOBOOKS_LOGIN_LINK), so no password reaches the browser. Everyone sees the whole library, but where you got to in each book is your own: the hub keeps each person's positions on the server (so they follow you between devices), and with a personal audiobook-player sign-in they are also kept in step with the player's own page. Search finds your books too. Set OPENAUDIBLE_URL to talk to OpenAudible directly instead: then the username and password in Settings -> Audiobooks sign-in are OpenAudible's own login (or OPENAUDIBLE_USER / OPENAUDIBLE_PASSWORD on the server), and the hub keeps each person's listening position itself (as it does in both modes). The old frame is still there as Classic player (#/audio/classic). (Classic player) Back-door sign-in: set AUDIOBOOKS_LOGIN_LINK to 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 .env backups. Or set AUDIOBOOKS_SSO_SECRET (and optionally AUDIOBOOKS_SSO_URL, default <AUDIOBOOKS_URL>/sso) for short-lived signed links: ?token=<body>.<sig>, where body is base64url JSON {u, id, exp, n} and sig is the base64url HMAC-SHA256 of body. Without either, the plain site opens and the saved username can be copied.
  • Books: an e-book reader for EPUB, PDF, DOCX, MOBI, AZW and AZW3 (Media group; give it to people in Settings -> People as Books). Add files with the button or by dropping them on the page (up to 300 MB each; the server checks a file really is what its name says and does not store the same book twice). Titles, authors and covers are read out of the books in the browser; long-press or right-click a book to edit, download or delete it. Each person has their own shelf in /data/users/<id>/books/.
    • Reading: EPUB uses epub.js (pages or scroll, two-page spreads on wide screens), PDF uses pdf.js (continuous scroll, zoom, selectable text, dark and sepia tints) DOCX is converted to HTML by mammoth, and MOBI, AZW and AZW3 are read by @lingo-reader/mobi-parser; both are shown in a frame that cannot run scripts (scroll only, with contents from the book or its headings). Contents, search inside the book, a progress slider, tap the edges or use the arrow keys, swipe on touch screens, tap the middle to hide the bars. Appearance (theme, text size, font, line spacing, margins, layout, zoom) is remembered on each device.
    • Synced: where you got to, bookmarks, highlights (four colours) and notes are kept on the server, so a book opens where you left off on any device; two devices adding notes at once keep both (the newest edit of the same note wins). Continue reading on the library page, sorting and search, and Copy as text exports a book's notes.
    • Book store (Books -> Book store): search free, DRM-free books from Project Gutenberg (its own catalogue feed), Standard Ebooks (its public website: its OPDS feed is for paying members only, so the hub reads the public search pages and downloads the free EPUB), the Internet Archive (public-domain EPUBs only; borrow-only items are refused) and any OPDS catalogue an admin adds under Catalogues (Calibre-Web, Kavita, Komga and so on; an optional username and password are kept encrypted on the server and never sent to the browser). One search covers every source, results show their cover and source, and Add to library downloads the book on the server straight onto your shelf. The server only ever contacts a source's own addresses (redirects to other sites are refused), covers come through the hub, and a catalogue book must live on that catalogue's own address. Settings: GUTENBERG_URL, STANDARD_EBOOKS_URL and ARCHIVE_URL can point at mirrors. node tools/store-test.js tests it against fake catalogues; it was also tried against the live Project Gutenberg, Standard Ebooks and Internet Archive (searches, and real EPUBs downloaded); if one of them changes its pages or feed, the line under the search box names the source that failed.
    • The reader scripts are in public/vendor/reader/ (epub.js, JSZip, pdf.js, mammoth, the MOBI parser) and load only when a book is opened or added. node tools/books-test.js tests the server; python3 tools/mkbooks.py <dir> makes a small EPUB, PDF and MOBI to try. DRM-protected books are refused (a Kindle file with copy protection is detected on upload and the reason is shown); only DRM-free books work. The MOBI reader was tried on a generated MOBI 6 file, not on real Kindle files, so tell me about any book that opens oddly. Not included: text-to-speech, offline reading. Free audiobooks (Audiobooks -> Free audiobooks; works without any audiobook sign-in): search LibriVox, thousands of public-domain books read aloud by volunteers, by title or author. A book shows its length, language, description and chapters; Play (or Resume) queues every chapter in the same player as Music so the next one follows by itself, and starting a book puts it on your shelf (My free audiobooks, with a progress bar; Remove from shelf in the book's sheet). Where you got to is kept on the server per person, so it follows you between devices. The hub searches LibriVox and hands the browser each chapter's address, but only addresses on LibriVox or the Internet Archive, which hosts the recordings; covers come through the hub. LIBRIVOX_URL points at a mirror. node tools/librivox-test.js tests it against a fake LibriVox; it has not been run against the live site from the build environment.
  • Radio: internet radio, with The Journey FM (Liberty University, Lynchburg, Virginia; MP3 192 kbps, http://ic.liberty.edu:8000/WRVL) always on top, showing what is playing now and the last songs played from the station's own feed. Below it, search thousands of stations from the community Radio Browser directory (free, no key) by name or with quick topic chips (Christian, Worship, Gospel, Talk, News, Classical, Jazz...). The heart saves a station to My stations (your own list). Stations play in the same player as Music, so they keep playing as you move between tabs. The hub fetches each stream itself and passes it on, because many stations only offer plain http, which a browser refuses to play inside an https page; it also reads the song title carried in the stream (the player shows it) and fetches station logos. The hub never connects to private, local or link-local addresses for a station. Give it to people in Settings -> People (Radio). JOURNEYFM_STREAM_URL, JOURNEYFM_NOW_URL and RADIO_BROWSER_URL can override the addresses. Lyrics come from the Lyrica service if you run one (LYRICS_API_URL), and otherwise (or when Lyrica finds nothing) from LRCLIB, a free lyrics database that needs no key: the artist and song title are sent to it, and set LYRICS_LRCLIB=0 to never ask it. node tools/radio-test.js and node tools/lrclib-test.js test it against fakes; the real Journey stream could not be reached from the build environment, so if it will not play, open the station's own Website button and tell me what changed. Song and artist: the player shows the song as the title with the artist under it (and the station as the album), also on the mini player, the Now Playing sheet, the Radio page and the phone's lock screen; it follows the station every 10 seconds. The Journey FM's own feed gives the artist and when each song began; other stations send Artist - Title inside the stream, which the hub splits. Cover art: the cover of the song that is on (from Apple's free iTunes search, no key; the song's artist and title are sent to it), else the station's logo, else a coloured tile with the station's initials, so there is never an empty picture. A live station shows LIVE in place of a length, and has no seek bar, queue or skip buttons. Lyrics (the Lyrics button on the player or the Radio page) are looked up for the song that is on now and change when the song does. A station that says when a song began (The Journey FM) gets synced lyrics that follow along; because what you hear is a few seconds behind the station's clock, it starts 5 seconds early and the - and + buttons move it a second at a time (remembered for that station). Other stations show the words without following along. Not included: HLS-only stations, recording, alarms.
  • Verse By Verse (Podcast -> Verse By Verse; needs only the Podcast tab, not Podgrab): free verse-by-verse Bible teaching from Verse By Verse Ministry International. Browse the ministry's studies (about 80: Old and New Testament books, topical series, single teachings, women's and youth), search them, and open a study to see its lessons with dates and summaries. Start / Continue plays the study lesson after lesson in the same player as Music, a lesson resumes where you left off (kept on the server per person, so it follows you between devices), and each lesson's handout (PDF) opens from Notes. The hub reads the study list from the ministry's category pages and the lessons from each study's own podcast feed (/bible-studies/<study>/podcast.xml), keeps what it read for 30 to 60 minutes so the ministry's site is not asked on every visit, and sends the browser straight to the ministry's media server for the audio. Only the ministry's own addresses are ever used. VBV_URL can point at a mirror. node tools/vbv-test.js tests it against a fake copy of the site; it was also tried against the live site (80 studies, a study's lessons, audio and handout links). If the ministry redesigns its pages, the study list is the first thing that would break, and the screen would say so.
  • Save to Files: copy things into your own Seafile from where you find them. Books (long-press or right-click a book -> Save to Files), free audiobooks (a book's sheet -> Save to Files, one numbered MP3 per chapter in a folder) and Verse By Verse (a study's Save to Files button saves every lesson and handout, or just the audio or just the handouts; the download button on a lesson saves that one). You pick the library and folder (it remembers the last one); the hub makes a tidy subfolder (Books, Audiobooks/<title>, Verse By Verse/<study>), names files safely and in order, skips anything already there (so running it again fills in what is missing) and counts lessons that have no handout instead of failing. The hub does the downloading and uploading itself with the Seafile sign-in already saved for you, so a whole study (several GB) does not pass through your phone and keeps going if you tap Hide; a sheet shows progress, Stop cancels, and Open folder jumps to the result. It needs the Files tab and the tab the content comes from (Books, Audiobooks or Podcast); each person can have two saves running at once; files up to 500 MB. node tools/savefiles-test.js tests it against a fake Seafile.
  • Bible: the Berean Standard Bible (BSB, public domain: https://berean.bible/), all 66 books, with its section headings, poetry layout and footnotes. The whole text is one file (public/vendor/bible/bsb.json, about 1.5 MB compressed, made by python3 tools/make-bsb.py from the free Hello AO Bible API) that the browser loads once and keeps, so reading is instant and works offline. Choose a book and chapter, search for words (every word must be in the verse) or type a place (John 3:16, 1 cor 13:4-7, ps 23), swipe or use the arrow keys to turn chapters, and tap verses to highlight (four colours), add a note, or copy them with their reference. Tap a small letter for a footnote. My highlights and notes lists everything. Your place, highlights and notes are kept on the server per person, so they follow you between devices; text size and font are remembered per device. Give it to people in Settings -> People (Bible). node tools/bible-test.js tests the saving and checks all 31,086 verses are in the file.
  • Menu on a phone: related tabs share one entry (Listen: Music, Radio, Podcast, Audiobooks; Read: Bible, Books; each part still needs its own permission), with a row of buttons under the top bar to switch between the parts (it remembers the last one you used). The bottom bar is yours to choose: More -> Edit bottom bar (up to five apps, in the order you tap them; it is saved to your account). More opens the Apps sheet: every app as a big icon, grouped, with the ones you used lately on top, a search box (typing rad finds Radio even though it lives under Listen) and unread badges. Home still shows one tile for each part. On a tablet or computer the side menu uses the same merged entries, and Ctrl/Cmd-K finds every page by name.
  • 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-api bridge, which is one more linked device of your account (like Signal Desktop; not an official Signal product). The signal container in docker-compose.yml runs it; remove that service and the hub's SIGNAL_API_URL line 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 the nehub-signal volume (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:1000 it 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, a frame-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). /healthz answers without signing in and the container has a health check. In-app OnlyOffice editing now requires ONLYOFFICE_JWT_SECRET (or ALLOW_UNSIGNED_OO=1 for testing). npm test runs every test script; CI also runs npm audit.
  • Planner and grouped menu: Calendar and Tasks are one menu entry, Planner, with a Calendar | Tasks switch at the top (each still needs its own permission in Settings -> People, and #/calendar and #/tasks still work as addresses). Tasks with a due date appear on the calendar (month chips, week/day all-day strip, agenda) with a tick to complete them; tapping one opens the task. The menu, the phone "More" sheet and the Home tiles are grouped by kind: Work (Planner, Files, Office, Links), Communicate (Mail, Signal), Media (Music, Audiobooks, Podcast, Podcast Admin), with Home/Search at the top and Settings at the bottom.
  • Files, sturdier: the Seafile pass-through gives Seafile 30 seconds to answer (2 minutes to start a file transfer), retries a failed read twice, says plainly when Seafile is slow or down, and stops reading from Seafile as soon as you close a download. In the app, an expired Seafile sign-in asks for your password again in a sheet and then carries on where you were. Each folder you open is saved (names, sizes, dates) so it shows instantly while the real list loads, and stays visible with a note when you are offline or Seafile cannot be reached; very large folders show 300 rows at a time with Show more, and the scroll position comes back when you return. Uploads wait for the connection instead of failing, retry with growing pauses (5 tries), and when a name is already there you choose Replace (the old version stays in the file's history), Keep both, Skip or Cancel. Deleting offers Undo (from Seafile's recycle bin), and Recycle bin in the folder menu (the three-dots button in the toolbar) restores anything deleted from the library. The same menu has Search this library and Download as zip (also in Select mode and on a folder's menu). Share links now offer a password and an expiry, list the links that already exist and can delete them. Text files (txt, md, json, csv, logs, config files, code) open in a viewer, and small ones can be edited and saved there. Not included: resumable chunked uploads (a dropped connection restarts that file; I could not confirm Seafile's chunk protocol without a server to test), editing files over 256 KB in the viewer. The recycle bin, Undo, zip, share options and text saving use Seafile's documented API but have only been tried against a fake Seafile, not a real one.
  • 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; set VAPID_SUBJECT to a mailto: 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.log and 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 in index.html (boot.js last); the versioned ?v= addresses and the cache version are computed from every file in public/.
  • Podcast is a built-in screen for Podgrab at PODCAST_URL: your shows, their episodes (played state, download all, pause, remove), adding a show by feed address, and playback in the same player as Music. The hub signs in to Podgrab for you with PODGRAB_PASSWORD (Podgrab's PASSWORD setting; the user name is podgrab), so that password never reaches the browser. Each person has their own list of shows (the admin starts with everything already in Podgrab, everyone else starts empty); adding a show someone already follows just puts it on your list, and removing one only takes it off yours (Podgrab drops it when nobody follows it). Where you got to in each episode is kept per person on the server, so it follows you between devices. Search finds your shows too. Add podcast also searches Apple's public podcast directory for new shows (the words you type go to Apple). Not here: resume position.
  • Podcast Admin is no longer a menu entry: it is a built-in link on the Links page (opens VERSE_URL in a new tab, cannot be edited or deleted) for people who have the Podcast Admin permission in Settings -> People; the old #/verse address goes to Links.
  • Office: make, find and open Word, Excel and PowerPoint files kept in Seafile (they open in OnlyOffice inside the app). New document / spreadsheet / presentation buttons, search, a filter by kind, sorting (newest, name, size), Pinned and Recently opened at the top, an Upload button and drop-files-on-the-page (with the same Replace / Keep both / Skip choice as Files), a three-dots menu on every document (share link, download, version history, rename, move, delete), and a saved copy of the list that shows at once while the real one loads (and stays if Seafile cannot be reached). Pins are saved with your settings and follow you across devices. The Ctrl/Cmd-K palette can create a new document, spreadsheet or presentation.
  • Mail: several mail accounts per person in one place: Gmail, iCloud Mail and (optionally) Outlook.com (personal). Folders with unread counts, message list with search, reader, compose with attachments (up to about 3 MB) and a From choice, reply, reply all, forward, archive, delete, flag, mark unread, an unread badge on the Mail tab (all accounts added up) and a Home card with the latest unread across accounts. Each person adds their own accounts (Mail tab, or Settings -> Mail -> Add an account); everything is private to them and stored encrypted in their own folder.
    • Gmail and iCloud use IMAP/SMTP with an app password, so there is nothing to set up on the server. Gmail: turn on 2-Step Verification, then make an app password at myaccount.google.com/apppasswords. iCloud: appleid.apple.com -> Sign-In and Security -> App-Specific Passwords. The password is checked with the provider before it is saved, and wrong ones are explained in plain words. iCloud does not keep a copy of mail sent over SMTP, so NE Hub files one in Sent itself (not yet confirmed against a real iCloud account).
    • Outlook.com goes through Microsoft Graph (Outlook.com no longer allows IMAP passwords), which needs the one-time Microsoft app below; without it the Outlook.com button simply is not offered.
    • Message text is shown in a sandboxed frame with no scripts, and pictures from the internet stay hidden until you press Show pictures. Not included: a combined inbox across accounts (switch account at the top), message previews in the list for Gmail/iCloud, drafts autosave, contacts, Microsoft 365 work accounts, other IMAP providers (easy to add the same way). One-time Outlook.com setup (step by step, with troubleshooting, in docs/mail-setup.md; needs a free Microsoft account and takes five minutes): go to entra.microsoft.com -> App registrations -> New registration. Name it NE Hub; supported account types Personal Microsoft accounts only; Redirect URI: platform Web, value <your hub address>/api/mail/callback (for example https://hub.necloud.us/api/mail/callback; it must match PUBLIC_URL exactly). Then API permissions -> Add -> Microsoft Graph -> Delegated: Mail.ReadWrite, Mail.Send, User.Read, offline_access. Copy the Application (client) ID and paste it, with the secret, into Settings -> Microsoft app (admin) (or set MS_CLIENT_ID / MS_CLIENT_SECRET in the environment, which win). If you registered it as a Web app, also make a client secret (Certificates & secrets) and put its value in MS_CLIENT_SECRET; if you used platform "Mobile and desktop applications" instead, no secret is needed. Leave MS_CLIENT_ID empty to switch Mail off. (The old bundled Cypht webmail and its nehub-cypht volume are gone; you can delete that volume.) Connecting from the installed iPhone/iPad app may open Microsoft's page in Safari; if the app does not notice, reopen it.

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 lyrics container from lyrica/ to git.necloud.us/nate/hub-lyrica. The hub image is amd64 and arm64; the lyrics image is 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, MS_CLIENT_ID (for Mail), etc.
docker compose pull && docker compose up -d
docker compose logs hub | grep "Setup code"   # first sign-in: create your passkey

Transcripts for podcasts and audiobooks

The lyrics screen shows a Transcript for podcast episodes and audiobooks. If the show publishes its own (Podcast 2.0 <podcast:transcript>: SRT, VTT or JSON) it is used straight away. Otherwise a Transcribe on this server button makes one with speech-to-text: the bundled whisper service in docker-compose.yml (faster-whisper behind an OpenAI-compatible API) is reached at TRANSCRIBE_API_URL (default http://127.0.0.1:8000), model TRANSCRIBE_MODEL (default Systran/faster-whisper-small, downloaded on first use). CPU only: about a quarter of the running time or more. One job at a time; results are kept in /data/transcripts. Remove the service and the variable to switch the button off.

In the background the server also keeps transcripts up to date by itself: every ten minutes it looks at the newest TRANSCRIBE_KEEP (default 5) episodes of every show in Podgrab, uses the show's published transcript when there is one, and otherwise transcribes the episode (one at a time, at most two new ones per round, a failing one retried a few times). TRANSCRIBE_AUTO=0 switches this off; playing an episode or pressing the button still works. Settings -> Health shows how many are saved and what it is working on.