Admin control center (Phase 1), token search, feedback archive & more
Admin control center — Phase 1
- Reworked
#view-admininto a tabbed control center (switchAdmin()+.adm-tab/.adm-mod): Overview, Catalogue, Data Quality, Users, Series & Collections, Import/Export, Reports. Entry viaenterAdminPanel(). Keyboard nav:g+o/c/d/u/s/i/rjumps,/focus search, and in Cataloguej/kmove,xtoggle,aselect-all,Escclear. - New endpoints (all
requireAdmin, globalbooks.idbased, cross-user):GET /api/admin/overview,GET /api/admin/books(joins owner + series_name),PATCH /api/admin/books/bulk(registered before/:idto avoid capture),PATCH|DELETE /api/admin/books/:id,POST /api/admin/books/bulk-delete,GET /api/admin/quality+POST /api/admin/quality/{fix-whitespace,fix-orphan-series,purge-orphan-collections},GET /api/admin/series,GET /api/admin/collections,POST /api/admin/series/merge,POST /api/admin/series/bulk-delete,GET /api/admin/export(csv/json, owner filter),GET /api/admin/backup(onlinedb.backup()→ timestamped.dbdownload). - Catalogue: all books fetched once into
adminBooks, filtered/sorted/selected client-side (token search, owner/status/sort). Inline read-toggle + delete by id; bulk read/unread, set type, delete via the selection bar. Single edits instant; bulk delete + cross-owner delete confirm. - Data Quality: 8 detection queries (dup ISBN, orphan series link, orphan collection rows, untrimmed title/author, empty series, missing cover/pages/genres). Deterministic groups get "Fix all"; dupes get per-row delete; missing cover gets OpenLibrary "Find cover" (
GET /api/lookup/:isbn→PATCH); missing pages get an inline input. Counts surface as Overview action cards + sidebar badges. - Bulk safety: all bulk mutations run in a single better-sqlite3
db.transaction(); bulk field edits whitelisted viaADMIN_BULK_FIELDS;shapeAdminBook()returns owner + series_name + read_date_start for the status filter. - No schema changes in the original Phase 1 cut. Activity log, error log, broken-cover scanning, command palette and CSV import are deferred to Phase 2/3. ⚠️
frontend/Dockerfileunaffected (no new static files).
Admin follow-up fixes
- Data Quality no longer collapses on edit.
loadQuality()now captures the set of expanded.adm-q-card.openkeys before re-render and re-applies them (admQCard(g, isOpen)); the "Scanning…" placeholder only shows on first paint. Inline fixes (set pages, find cover, delete dupe) keep their card open. - Admin book editor. New
#adm-edit-modal+openAdmEdit(id)/saveAdmEdit()editing title, author, ISBN, year, cover (URL, or "Fetch" via/lookup), pages, type, language, binding, genres — by global id viaPATCH /api/admin/books/:id. Reachable from every book-based Data Quality item and from Catalogue rows. Fixes the "wrong/missing ISBN ⇒ can't get a cover" case (paste a URL, or correct the ISBN then fetch) and lets you fill genres. Registered in the ESC / back-gesture close stacks. - Series length / missing volumes. ⚠️ migration
ALTER TABLE series ADD COLUMN length INTEGER;PATCH /api/series/:idnow also acceptslength. The series detail modal shows the declared length with an inline editor (editSeriesLength()) and a "Missing N of L: #…" / "Complete set" line computed from owned integerseries_partvalues against the length. - Series-detail follow-ups. Editing a book from a series row no longer closes the series: dropped
closeSeriesDetail()from the row's onclick, lowered#series-detail-modaltoz-index:90so the book editor (and other overlays) stack above it, andsaveBook()now callsrefreshSeriesDetailIfOpen().formatSeriesPartsLabel()gained ahideMissingparam (passedtruewhenseries.lengthis set) so the "missing …" suffix on the parts line no longer duplicates the red length-based missing box.
Configurable webhook (admin → Webhook tab)
- ⚠️ migration
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT);webhook_urlseeded from the env/default on first run.getWebhookConfig()resolves all settings with defaults. - Webhook layer split into
postWebhook(url, embed)(low-level, returns 2xx) andnotifyDiscord(embed, category)(respectswebhook_enabled;category==='security'additionally gated bynotify_security). The two password/reset notifications now pass'security'. - Summary is now config-driven:
sendFeedbackSummary(force)builds fields fromsummary_sections(open / in_progress / resolved) and honoursquiet_when_empty. Replaced the fixed 12:30 timer with a self-reschedulingscheduleSummary()+msUntilSummary()supportingoff/daily/weekly(+ weekday) at a configurableHH:MM; reschedules on every settings save. - Endpoints:
GET /api/admin/settings,PATCH /api/admin/settings(whitelisted + coerced keys, thenscheduleSummary()),POST /api/admin/settings/test(posts to the URL in the request body so you can test before saving),POST /api/admin/settings/send-now(forces a summary). New frontend module#adm-webhook(g wjump) with URL + Test, master toggle, frequency/weekday/time, section checkboxes, quiet-when-empty, security toggle, Save + Send-now.
Search
- Added
matchesSearch(query, ...fields)helper — tokenises the query on whitespace and requires every token to appear in the joined, lowercased fields (AND semantics, order-independent). So "Berserk 1" matches "Berserk Deluxe Vol. 1". - Replaced all substring
.toLowerCase().includes(q)checks across the five search sites:getFiltered(books — now also searchesseries.name+series_part),renderSeries,renderCollections,filterStartReadingList,filterMarkReadList. Main search placeholder updated.
Feedback UI
- Admin "Reports & requests" and the submitter "My reports" panel now split items into active vs. a collapsible
<details>archive fordone/declined(fbIsArchivedhelper). Active list shows an all-caught-up message when empty. - My reports archive predicate tightened to
fbIsArchived(f) && f.seen_by_user === 1: a newly-resolved (unseen) item stays in the active list with an Updated badge and only moves to the archive after it's been seen (render happens beforemark-seen, so the fetchedseen_by_userstill reflects the unseen state on first open). Admin view unchanged (archives by status only).
Discord
- Removed the per-submission webhook from
POST /api/feedback(flow is managed on-site). AddedsendDailyFeedbackSummary()+scheduleDailyFeedbackSummary()— one embed per day at 12:30 with open / in-progress / resolved breakdowns (bug vs. feature), skipped when nothing is open or in progress. Container pinned toTZ=Europe/Amsterdam(docker-compose) so 12:30 is local wall-clock time and DST is handled automatically.
Fixes
- Series detail rows now use the same 3-state status badge as book cards: read →
openReadModal, reading/unread →openStatusMenu(start reading, update progress, mark done, stop, edit). The floating#status-menu(z-index 9999) renders above the modal overlay (z-index 100). AddedrefreshSeriesDetailIfOpen(), called fromsmStart/smStop/saveProgress/saveReadInfo/markUnreadso the open series detail re-renders after a change. Replaced the earlier read-onlyquickToggleReadtoggle. New.sbr-read.readingstyle. - Feedback textarea
#fb-messagebumped from14pxto16pxto stop iOS focus-zoom that lingered after the modal closed.