SoftDelete now reports whether it performed the delete (conditional
UPDATE ... WHERE deleted_at IS NULL via RowsAffected). RegisterRead
returns an admitted flag: legacy burn pastes admit exactly one reader
(atomic soft-delete win), and burn-after-N pastes increment reads_used
via a conditional UPDATE guarded on reads_used < reads_limit, so
concurrent readers cannot both consume the final read. API/HTML/raw
read paths return 404 when the reader loses the burn claim.
Adds concurrency regression tests: 24 parallel readers of a burn paste
(exactly one receives content) and 30 parallel readers vs a 3-read
budget (exactly 3 admitted).
- attachments: filesystem-on-PVC recommended, blob interface keeps MinIO
as a later drop-in; size limits via admin setting; sniffed-mime +
nosniff/sandbox serving rules
- storage backend: stay SQLite-only (WAL, modernc); no Postgres/Redis and
no backend abstraction until documented trigger conditions fire
- new web/static/table.js (PaletteTable): shared live search, client-side
sort with indicators, pagination, row rendering + click-through
- history.html and mine.html both consume it; data endpoint, columns,
empty-state text and row extras (delete buttons) are page-supplied
- verified in-browser: search, sort, delete on /mine; sort, pager on /history
Closes#57
- #50 gutter/code line misalignment: shared --code-lh/--code-fs tokens;
.code .gutter drops its own vertical padding (was 14px vs 0, 14px offset)
- #51 history Paste column shows only the paste name, or slug pill when untitled
- #52 sort indicator arrow sits LEFT of the column label for all sortable columns
- #53 copy buttons give in-place success feedback ('Success!' in --ok for 2s):
paste view copy button and new-page result copy button
- #54 collapsed stats summary bar: roomier padding (14px 18px) and item gap (16px);
wraps gracefully on mobile
- #55 paste view title bar: slug pill only shown for custom slugs, id not repeated
Closes#50, closes#51, closes#52, closes#53, closes#54, closes#55
- vwr cookie middleware: random browser id set on first visit (reused by #49)
- pastes table gains viewer_id column, set server-side at creation from the cookie
- GET /api/mine lists pastes for the requesting browser (title/lang/size/created)
- DELETE enforcement: 403 when client-sent vwr doesn't match the paste's viewer_id
- /mine page reuses history table styling, delete buttons, empty state
- nav: 'Saved' item between Public and Git; Git gets external-link arrow (#56)
- tests: create-with-cookie appears in /mine, other cookie doesn't, delete enforcement
Closes#37
- Three new light [data-preset] blocks alongside midnight (default) and smooth
- --ok/--warn/--err semantic tokens with 4.5:1 contrast per preset
- .toast success/error variants wired in new.html
- ?theme= query param preview hook in layout head (screenshots only)
- Preview screenshots in docs/palette-previews/
Closes#16
- layout.html: inline SVG gear icon top-right, styled as .iconbtn.gear pill
- new settings.html template reusing standard layout with under-construction card
- register GET /settings route in main.go
#1: server-side regex highlighter (highlight.go) for go/python/js/json/bash/sql;
token span classes styled in app.css; per-line so gutter stays aligned.
#2: in-memory token-bucket rate limiter (ratelimit.go) on POST /api/pastes,
/api/guess-language and unlock POST; 429 + Retry-After + X-RateLimit headers.
#26: new-page JS POSTs the password to /{id} with ?next= after creation; the
unlock handler honors same-origin ?next= redirect so the creator lands on the
unlocked paste. POST /{id} route added.
Tests: ratelimit_test.go (burst/429, refill, unlock limit, highlight, auto-
unlock e2e); existing tests updated for per-test limiter isolation.