node_modules/.git/dist were being sent on every build with no
.dockerignore at all — same class of fix as goonk's recent deploy-speed
pass. Verified by building with the new file in place (adventure and
new spot-checked with a full container run + health check; the rest
share the same multi-stage Dockerfile pattern).
.article-card form had margin-left: auto applied to each form
individually inside a flex row that also held the meta text (hostname,
date, unread badge) — spacing shifted depending on whether the unread
badge was present, since the auto-margin was competing with a
variable number of sibling elements instead of anchoring against a
fixed layout.
Restructured into two explicit flex groups per row — meta info on the
left, action buttons on the right, justify-content: space-between on
the container — so the buttons land at a consistent x-position
regardless of read/unread state or article title length. Same fix
applied to both the list view and the reader view's meta row.
Verified visually against both states (read and unread cards) in a
screenshot — buttons now align consistently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
UI: ported flit/wisp's dark, JetBrains-Mono-accented theme wholesale
(same CSS vars, same card/button/input vocabulary) so stash reads as
part of the same family of tools instead of a bare unstyled list. List
view is now card-based with unread indicators; reader view got proper
article typography.
Delete: the list and reader views now have a delete button (with a
confirm prompt) wired to the existing DELETE /api/articles/:id via a
new POST /delete/:id UI route, since browser forms can't issue DELETE
directly. express.urlencoded() added to parse the form posts this and
the existing mark-read buttons need.
Token management: added listTokens/revokeToken to db.ts, plus
`npm run tokens` (lists label/created_at/last-4-chars only — never
re-prints a full secret) and `npm run revoke-token -- <label>`. This
was a real, previously-missing gap: there was no way to invalidate a
single compromised token short of wiping the entire database. Built
after a token got printed in plaintext during a debugging session and
there was no way to kill just that one credential.
Verified: full theme renders correctly (screenshotted list + reader
views), revoke-token correctly 401s the targeted token while leaving
others untouched (tested against a real leaked-then-revoked token),
list-tokens never displays a recoverable secret.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dockerfile/docker-compose.yml mirror keep's pattern — same
better-sqlite3 native-addon build story, same pull_policy: always +
named volume for the SQLite DB so redeploys don't lose captured
articles. Gitea Actions workflow builds and pushes to the Gitea
container registry on push to main (needs a TKNTKN repo secret, same
as keep/wisp/npm-statuspage).
TESTING.md is the step-by-step for running the server + loading the
extension unpacked, written up from what was actually verified
end-to-end in the previous session.
Verified: image builds, server starts and responds inside the
container, capture works, and data survives a container restart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Server (Express + better-sqlite3 + FTS5, mirroring keep's stack):
/api/capture (upsert-on-url, per PROPOSAL.md's dedup decision),
/api/articles (list/search/mark-read/delete), plus a minimal
server-rendered reading list and reader view at / and /read/:id
(token passed as ?t= there since browser navigation can't set an
Authorization header). Bearer-token auth generated via `npm run
token` — no login flow, matching keep's one-time identity
registration pattern.
Extension (Manifest V3, sideloaded, no store distribution): toolbar
click or context menu "Send to stash" runs @mozilla/readability
(vendored into extension/lib/) against the current page, falling back
to raw rendered HTML when extraction comes back too thin. Options
page for one-time server URL + token entry.
Verified end-to-end with the real extension loaded in a live Chromium
instance (not just unit-tested extraction logic): capture against a
real page, dedup-on-recapture, mark-read, FTS5 search, 401 on missing
auth, and both server-rendered views all confirmed working.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>