Repository navigation
Docket v1: self-hosted read-later app - #1
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Docket is a single-user, self-hosted replacement for Pocket. It's built on TheLedger's stack and infrastructure.
mainholds only the CI workflow; this PR adds the app on top.What's in it
.github/workflows/ci.yml): PHP 8.3, composer install, npm ci + build, Pint, PHPStan, PHPUnit. Uploads the lockfiles as an artefact.composer.lockandpackage-lock.jsonare committed; both were resolved locally from Packagist and npm metadata.docket:install, EnsureNotInstalled, SecurityHeaders, the Settings service, Breeze auth with no registration, tools page, PWA, spatie/laravel-backup to an off-site S3 disk./save), PWA share target (/share), andPOST /api/itemswith Sanctum personal tokens. Links are de-duplicated on a normalised URL.FetchItemContentjob 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.SafeHttpClient):CURLOPT_RESOLVE).docket:import-pocket): idempotent, spaced-out extraction. Export: Pocket-shaped CSV and a full JSON export.Testing
composer installcan't run in the session that built this, so CI on this PR is the test run. The feature tests useHttp::fake(),Http::preventStrayRequests()and a fake DNS resolver, so they never touch the network.Generated by Claude Code