Fredrik Johansson 4a6a373f1e Restyle UI to match flit/wisp theming, add delete + token revoke/list
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>
2026-07-13 19:00:36 +02:00

stash

A self-hosted read-later tool. Click "Send to stash" in the browser, the current page's article content is extracted and stored on your own server — full-text searchable, with a plain reader view. No third party ever sees your reading list.

Two pieces, neither useful alone:

  • Server (src/server) — Express + SQLite (via better-sqlite3), FTS5 full-text search, a minimal server-rendered reading list + reader view, and the /api/* endpoints the extension talks to.
  • Extension (extension/) — Manifest V3, sideloaded (not published to any store). Runs @mozilla/readability against the current page to strip nav/ads/chrome and keep just the article, falling back to the raw rendered page if extraction comes back too thin to be useful.

Full design and the reasoning behind the scope boundaries: PROPOSAL.md.

Server setup

cp .env.example .env
npm install
npm run token -- my-laptop   # prints a bearer token — paste it into the extension's options
npm run dev:server

Extension setup

  1. chrome://extensions → enable Developer mode → "Load unpacked" → select extension/
  2. Click the extension icon once, or use "Options" from its context menu, to open the options page
  3. Enter your server's URL and the token from npm run token

Then either click the toolbar icon or right-click a page → "Send to stash".

Reading

Visit http://your-server:3070/?t=<token> — a plain list of what's been captured, with search and an unread filter. Click through to /read/:id for the cleaned reader view.

Status

Built and verified end-to-end: capture → server storage → full-text search → mark-read → reader view, driven through the actual extension (loaded in a real Chromium instance, not just unit-tested logic) against a live server. Not yet deployed anywhere.

License

MIT.

Description
No description provided
Readme 104 KiB
Languages
JavaScript 80.1%
TypeScript 18.5%
HTML 0.7%
Dockerfile 0.7%