Library curation + navigation: delete, upload, site header, kobo removal fix #7

Merged
arne merged 21 commits from curation-impl into main 2026-04-11 23:41:25 +02:00
Owner

Summary

Implements the full curation spec from docs/superpowers/specs/2026-04-11-curation-and-nav-design.md: delete books from the web UI, upload books through a header action, and replace the corner dropdown with a thin top-bar nav. Plus an incidental but necessary Kobo bug fix that the delete flow would otherwise trip.

Twenty small commits, one per plan task (with two gofmt fixups along the way), closed out through Tasks 1–18 of docs/superpowers/plans/2026-04-11-curation-and-nav.md. Full go test ./... green across all nine packages. Final full-branch code review completed against the spec — one Important finding (EXDEV safety in library.Archive/Restore) was addressed inline in the last commit.

What you get

  • Delete books from the UI. Book detail page has a two-click inline-confirm delete book button. First click arms it in stamp red with a "really?" prefix; second click within 5s commits. No modal. Soft-delete: the row stays (so Kobo removal tracking can still find the entitlement UUID), the files move to <BOOKS_DATA_DIR>/archived-books/<hash>.<ext>, the book auto-unshelves in the same DB transaction.
  • Upload books from the header. + upload action in the site header opens a hidden file picker, POSTs the .epub to /upload, runs it through the same importer pipeline as the inbox watcher, and lands on the new book's detail page. Uploading a file whose hash matches a previously-archived book restores the original row from the archive instead of creating a duplicate — the book's ID, UUID, and all metadata survive the round-trip. 50 MB cap enforced before ParseMultipartForm is called.
  • Thin site header. Corner dropdown is gone. Every page now carries a top-aligned shelf · library · errata nav with an active-state stamp underline, plus the upload action on the right. Active page is driven by the existing PageData.Section field. The ← library back link on the book detail page went away with it — the library tab in the header now serves that role.
  • Kobo removal bug fix. buildRemovedEntitlement used to synthesize a fake UUID via uuidFromInt(bookID). In the pre-delete world this never mattered — nothing ever routed a book through the removed branch after we'd previously delivered a real UUID. The moment delete-of-a-shelved-book ships, every removal envelope would carry a UUID Nickel never saw, and the device would silently fail to forget the book. Fixed by a new store.GetBookByIDIncludingDeleted method that survives the WHERE deleted_at IS NULL filter, plus changing buildRemovedEntitlement(book) to read book.UUID via entitlementID. uuidFromInt deleted as dead code. Regression test pinned.
  • Flash messages. Short one-string messages after redirects ("deleted X", "already imported as Y"). One documented deviation from the spec: the spec called for an HMAC-signed cookie, but the auth package has no signing key and adding one for a single string was over-engineering. Instead, flashes travel through a ?flash=<url-encoded> query parameter on the redirect target. html/template auto-escapes on render (&#34; in the test assertions proves it), the flash clears on the next navigation when the URL no longer carries the parameter, and an explicit test asserts the XSS-safe rendering path. Single-user tool, minimal surface area, no new crypto.

What's not in this PR (deliberate)

  • No bulk delete.
  • No visible archive view — restore is by re-upload (detected via file hash).
  • No drag-and-drop upload. Explicitly cut during brainstorming; the header button is the whole upload UX.
  • No hard delete / GC of archived files.
  • No orphan cleanup on book_authors / device_removals / reading_state / kobo_sync_session_books — those rows stay put and the Kobo delta still handles them correctly.

File structure

Plan-compliant: new handlers (delete.go, upload.go) are their own files in internal/web/, test files (delete_test.go, upload_test.go, flash_test.go, roundtrip_test.go) similarly split. No monster-file growth.

Test plan

  • go test ./... — all 9 packages pass
  • go build ./... — clean
  • gofmt -l — clean on every touched file (two fixup commits for drift along the way)
  • TDD-style tests for every task (failing test → implementation → passing test → commit)
  • End-to-end round-trip test: upload → delete → re-upload → restore to the same row, library file back in place, archive cleared
  • Final full-branch code review (superpowers:code-reviewer subagent) — one Important finding addressed inline in the last commit
  • Smoke test on fismen: push the built binary to the books container, trigger a sync, walk through delete + upload + archive-restore end-to-end. The existing Assassins Quest row should survive the schema migration untouched.

Intended to be squash-merged. The 20 commits are a breadcrumb trail, not a curated history.

🤖 Generated with Claude Code

## Summary Implements the full curation spec from `docs/superpowers/specs/2026-04-11-curation-and-nav-design.md`: delete books from the web UI, upload books through a header action, and replace the corner dropdown with a thin top-bar nav. Plus an incidental but necessary Kobo bug fix that the delete flow would otherwise trip. Twenty small commits, one per plan task (with two gofmt fixups along the way), closed out through Tasks 1–18 of `docs/superpowers/plans/2026-04-11-curation-and-nav.md`. Full `go test ./...` green across all nine packages. Final full-branch code review completed against the spec — one Important finding (EXDEV safety in `library.Archive`/`Restore`) was addressed inline in the last commit. ## What you get - **Delete books from the UI.** Book detail page has a two-click inline-confirm `delete book` button. First click arms it in stamp red with a "really?" prefix; second click within 5s commits. No modal. Soft-delete: the row stays (so Kobo removal tracking can still find the entitlement UUID), the files move to `<BOOKS_DATA_DIR>/archived-books/<hash>.<ext>`, the book auto-unshelves in the same DB transaction. - **Upload books from the header.** `+ upload` action in the site header opens a hidden file picker, POSTs the `.epub` to `/upload`, runs it through the same importer pipeline as the inbox watcher, and lands on the new book's detail page. Uploading a file whose hash matches a previously-archived book *restores* the original row from the archive instead of creating a duplicate — the book's ID, UUID, and all metadata survive the round-trip. 50 MB cap enforced before `ParseMultipartForm` is called. - **Thin site header.** Corner dropdown is gone. Every page now carries a top-aligned `shelf · library · errata` nav with an active-state stamp underline, plus the upload action on the right. Active page is driven by the existing `PageData.Section` field. The `← library` back link on the book detail page went away with it — the library tab in the header now serves that role. - **Kobo removal bug fix.** `buildRemovedEntitlement` used to synthesize a fake UUID via `uuidFromInt(bookID)`. In the pre-delete world this never mattered — nothing ever routed a book through the `removed` branch after we'd previously delivered a real UUID. The moment delete-of-a-shelved-book ships, every removal envelope would carry a UUID Nickel never saw, and the device would silently fail to forget the book. Fixed by a new `store.GetBookByIDIncludingDeleted` method that survives the `WHERE deleted_at IS NULL` filter, plus changing `buildRemovedEntitlement(book)` to read `book.UUID` via `entitlementID`. `uuidFromInt` deleted as dead code. Regression test pinned. - **Flash messages.** Short one-string messages after redirects ("deleted X", "already imported as Y"). One documented deviation from the spec: the spec called for an HMAC-signed cookie, but the auth package has no signing key and adding one for a single string was over-engineering. Instead, flashes travel through a `?flash=<url-encoded>` query parameter on the redirect target. `html/template` auto-escapes on render (`&#34;` in the test assertions proves it), the flash clears on the next navigation when the URL no longer carries the parameter, and an explicit test asserts the XSS-safe rendering path. Single-user tool, minimal surface area, no new crypto. ## What's not in this PR (deliberate) - No bulk delete. - No visible archive view — restore is by re-upload (detected via file hash). - No drag-and-drop upload. Explicitly cut during brainstorming; the header button is the whole upload UX. - No hard delete / GC of archived files. - No orphan cleanup on `book_authors` / `device_removals` / `reading_state` / `kobo_sync_session_books` — those rows stay put and the Kobo delta still handles them correctly. ## File structure Plan-compliant: new handlers (`delete.go`, `upload.go`) are their own files in `internal/web/`, test files (`delete_test.go`, `upload_test.go`, `flash_test.go`, `roundtrip_test.go`) similarly split. No monster-file growth. ## Test plan - [x] `go test ./...` — all 9 packages pass - [x] `go build ./...` — clean - [x] `gofmt -l` — clean on every touched file (two fixup commits for drift along the way) - [x] TDD-style tests for every task (failing test → implementation → passing test → commit) - [x] End-to-end round-trip test: upload → delete → re-upload → restore to the same row, library file back in place, archive cleared - [x] Final full-branch code review (`superpowers:code-reviewer` subagent) — one Important finding addressed inline in the last commit - [ ] **Smoke test on fismen:** push the built binary to the `books` container, trigger a sync, walk through delete + upload + archive-restore end-to-end. The existing Assassins Quest row should survive the schema migration untouched. Intended to be **squash-merged**. The 20 commits are a breadcrumb trail, not a curated history. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
arne added 21 commits 2026-04-11 23:19:14 +02:00
Breaks the curation spec into 18 TDD-ordered tasks covering schema,
store methods, Kobo removal-envelope fix, library Archive/Restore,
importer split with live + archived dedup branches, web handlers,
flash-via-query-param, site header and delete button templates, and
an end-to-end integration test.

One deviation from the spec noted in the plan preamble: the
HMAC-signed flash cookie becomes a ?flash= query parameter, because
the auth package has no secret key to sign with and adding one for a
single short string is over-engineering. html/template auto-escapes
on render so the XSS surface is covered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds DeletePost handler, registers the route, wires importer into
Handler struct and New() constructor (also updating main.go), extends
newTestHandler to construct an Importer, and adds delete_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds UploadPost handler that stages the file to inboxDir/.pending/,
calls imp.ImportFile, and redirects to the book detail page with a
flash on success (or to / with an error flash on failure). Includes
happy-path and extension-rejection tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the corner page-nav dropdown with a thin site-header
carrying shelf · library · errata on the left and a +upload
action on the right. Active-state highlighting via the existing
PageData.Section field. Upload action triggers a hidden file
input that auto-submits POST /upload on change — no modal, no
drag-and-drop.

On the book detail page: remove the "← library" back link (the
header's library tab serves that role now) and add a two-click
inline delete button in the action row. First click arms the
button (stamp color via .is-armed CSS), second click within 5s
submits the DELETE form, otherwise reverts to the resting state.
~15 lines of inline vanilla JS.
arne merged commit 05991346f0 into main 2026-04-11 23:41:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
arne/books!7
No description provided.