builds useful tools, fast
← Back to projects

Thousand Nights

● live reading app no framework works offline GitHub Pages

A reading machine for a 1,000 day list: one story, one essay and one poem per day.

🔍 Overview

The list came first: 1,000 days of reading, three texts per day, 3,000 selections in total. The problem with a list like that is friction. You have to find the text, decide if you have time for it, and remember what you already read.

So the list became an app. It opens on your phone, suggests the next unread text in the category you want, tells you how long it takes to read, and opens the text itself. Nothing is locked behind a day counter: every selection is available from the first day. Mark a text as finished and it goes into your reading log with the date and your own note.

Vanilla JavaScript
Service worker
Python importer
Supabase Auth
PostgreSQL + RLS
GitHub Pages
3,000
selections, 1,000 days
1,313
texts open in the app
921
full source collections
0
runtime dependencies

✨ What it does

  • Next unread: picks your next story, essay or poem, and prefers texts that open directly in the app.
  • Time filters: up to 5, 10, 15 or 30 minutes. "Start with a short read" shows the unread texts under five minutes, shortest first.
  • Reading in the app: adjustable text size, estimated reading time and a saved reading position, in a dark or light theme.
  • Reading log: read and unread status, reading dates and personal notes, with overall and per category progress.
  • Reading packs: a 3, 7, 14 or 30 day pack, exported as an HTML reading list or as a selection file.
  • E-reader export: a script turns a pack into real EPUB files for a Crosspoint e-reader, with a reading list and a report of what failed.
  • Offline: after one visit the app and catalogue work without internet. Texts you opened stay cached.
  • Backups: JSON export and import. Imports merge, the later reading date wins, and nothing is ever removed by an import.

📚 Honest about the texts

Getting 3,000 public domain texts into one app is mostly a matching problem, and the app says out loud how well each match went instead of pretending every link is perfect:

  • 1,313 extracts have a unique title match with defined text boundaries and open directly.
  • 921 source books match on author and collection, but the exact selection is not reliably delimited, so the app opens the searchable collection instead.
  • 432 author mismatches point at a different author. The app explains the mismatch rather than serving the wrong book as the right one.
  • 315 external sources and 19 unavailable selections have no local text yet, and keep their original link.

Texts come from Project Gutenberg EPUBs with explicit public domain metadata. Source text is rendered as plain text, never as HTML, so nothing from a source file can execute in the reader. A label such as "selection #5" is left ambiguous instead of being filled in by a guess.

🔒 Your reading log stays yours

Progress works as a guest, stored in that browser. Sign in with an email link and the log syncs privately between your devices: on edits, on reconnect, on returning to the tab, and every 30 seconds while the page is visible. Offline edits are queued, and a conflict between two devices asks you which version to keep.

Row level security limits reads to the signed-in user, writes go through an authenticated and version checked function, and retry IDs prevent duplicate writes. Only the public key is in the repository. No reading logs are committed.

🛠️ How it is built

No framework, no bundler and no backend during reading. The app is static files on GitHub Pages, published by an action that first rebuilds the catalogue and runs the tests. The catalogue parser checks exactly 3,000 unique selection IDs, three per day. Parser tests cover paragraph continuity, verse line breaks, chapter boundaries and ambiguous matches, and browser tests cover reading, persistence, backups, exports and reopening offline.

A separate Python importer downloads and extracts the source texts, reuses its download cache, and writes a report of what it could and could not import. That import work stays out of the published app.

Open the app →