Skip to content

Docket v1: self-hosted read-later app - #1

Merged
ajaydsouza merged 11 commits into
mainfrom
wz/elegant-dijkstra-lvdz1o
Oct 20, 2026
Merged

ajaydsouza merged 11 commits into
mainfrom
wz/elegant-dijkstra-lvdz1o

Conversation

@ajaydsouza

Copy link
Copy Markdown
Contributor

Docket is a single-user, self-hosted replacement for Pocket. It's built on TheLedger's stack and infrastructure. main holds only the CI workflow; this PR adds the app on top.

What's in it

  • CI (.github/workflows/ci.yml): PHP 8.3, composer install, npm ci + build, Pint, PHPStan, PHPUnit. Uploads the lockfiles as an artefact. composer.lock and package-lock.json are committed; both were resolved locally from Packagist and npm metadata.
  • From TheLedger, adapted: web installer and docket:install, EnsureNotInstalled, SecurityHeaders, the Settings service, Breeze auth with no registration, tools page, PWA, spatie/laravel-backup to an off-site S3 disk.
  • Saving links: paste, bookmarklet (/save), PWA share target (/share), and POST /api/items with Sanctum personal tokens. Links are de-duplicated on a normalised URL.
  • Extraction: the queued FetchItemContent job runs readability.php and sanitises the HTML against an allow-list with symfony/html-sanitizer. It downloads inline and lead images to private storage, records failures on the item and supports retry.
  • SSRF guard (SafeHttpClient):
    • Resolves DNS itself and refuses private, loopback, link-local and reserved addresses.
    • Pins the connection to the address it checked (CURLOPT_RESOLVE).
    • Follows redirects by hand and re-checks every hop.
    • Caps size, number of redirects and time.
  • Organising: unread, archive, favourites and tag lists; sort by newest, oldest or longest; bulk actions; tag rename and merge.
  • Search: FTS5 external-content index, kept in sync by triggers.
  • Reader: themes, typeface and size remembered per browser; scroll progress saved and restored.
  • Pocket import (web upload and docket:import-pocket): idempotent, spaced-out extraction. Export: Pocket-shaped CSV and a full JSON export.
  • README covering setup, the iOS Shortcut, deployment, backups and the security model.

Testing

composer install can't run in the session that built this, so CI on this PR is the test run. The feature tests use Http::fake(), Http::preventStrayRequests() and a fake DNS resolver, so they never touch the network.


Generated by Claude Code

ajaydsouza and others added 11 commits October 19, 2026 17:22
The base for a read-later app, taken from TheLedger without anything
ledger-specific (transactions, money, OCR, tracker import/export, Python
scripts): composer and npm
manifests with lockfiles, Tailwind v4 through its Vite plugin,
config/docket.php with DOCKET_* variables, and the Pint, PHPStan and
PHPCS configuration from TheLedger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Copied from TheLedger and adapted: the web installer (/install) and
docket:install, EnsureNotInstalled, SecurityHeaders, the Settings
service, Breeze auth without registration, the layouts and the PWA
manifest, service worker and offline page. The installer now also
checks for the dom and xml extensions and an SQLite build with FTS5.
New bookmark icon in teal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Items and tags tables, with de-duplication on a normalised URL (case,
default ports, fragments and tracking parameters removed, query sorted).
Links are saved by pasting in the app, the bookmarklet (/save), the PWA
share target (/share) and POST /api/items with a Sanctum personal access
token. Saving an existing link moves it back to the top of the unread
list, as Pocket did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
FetchItemContent fetches the page through SafeHttpClient, extracts it
with readability.php, sanitises the HTML against an allow-list, stores
images locally and records title, author, site, excerpt, lead image,
text, word count, reading time, language and published date. Failures
are recorded on the item and can be retried.

SafeHttpClient resolves hosts itself, refuses private, loopback,
link-local and reserved addresses (including embedded IPv4 in IPv6),
pins the connection to the vetted address, follows redirects by hand
re-checking each hop, and caps size, redirects and time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Unread (the home page), archive, favourites and per-tag lists sorted by
newest, oldest or longest; archive, favourite and delete per item or in
bulk; tag renaming and merging. Settings shows the bookmarklet, API
tokens and the iOS Shortcut steps alongside the editable knobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Serif or sans, adjustable size and light, sepia or dark themes, kept in
localStorage and applied before first paint. Scroll progress is saved as
you read (sendBeacon on leaving) and restored on return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
An external-content FTS5 index over title, site name and article text,
kept in sync by insert, update and delete triggers so query-builder
writes are covered too. Input is turned into quoted prefix terms, so no
search can produce an FTS syntax error; results rank title hits first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Imports Pocket's export CSV from the browser or docket:import-pocket,
keeping the added date, tags and status, and queues extraction spaced
apart. Re-running an import only merges missing tags. Exports a CSV in
Pocket's shape and a full JSON copy including article text and HTML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Covers saving links (bookmarklet, Android share, iOS Shortcut, API),
moving from Pocket, installation, deployment, backups and the security
model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Drop two checks PHPStan proves redundant, and note why the
FILTER_FLAG_GLOBAL_RANGE comparison in IpGuard stays: PHPStan does not
model the flag and wrongly treats it as always passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
The tag list gets its own controller action: route defaults and bound
parameters are passed positionally, so the shared index() signature
received the slug as $view. Web requests that ask for JSON now get
JSON validation errors instead of a redirect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
@ajaydsouza
ajaydsouza merged commit 8adae16 into main Oct 20, 2026
1 check passed
@ajaydsouza
ajaydsouza deleted the wz/elegant-dijkstra-lvdz1o branch October 20, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant