Daf for macOS and Android: the brief¶
The brief the native shells were built from: what the Mac and Android apps had to do, and the
contract between them and the web app. The same approach had already made native shells for
Dvarim and Moshe (franz-rosenzweig/dvarim, franz-rosenzweig/moshe), so it reuses what
worked there. It was written on the server, where Daf runs, and replaces the two small briefs that
were in mac/NEXT.md and android/NEXT.md; their specs (the menu-bar minutes, the
home-screen widget) are folded in below.
What Daf is, and how it differs from Dvarim and Moshe¶
Daf is a reading app for "Year 30": an hour of reading a day, from 2026-10-09 to 2027-10-08. Readwise Reader is the model for the look and the reading experience. It brings together:
- Kindle reading time and highlights (KOReader, with a Daf plugin);
- audiobooks (Audiobookshelf);
- Calibre books, read in Daf's own EPUB reader (foliate-js);
- articles, videos and tweets (Karakeep), and the Feed (FreshRSS);
- paper books (minutes logged by hand, photos of marked pages turned into highlights);
- a Torah shelf, a daily highlight review, stats, and an Obsidian vault it writes into.
The big difference: Daf is server-first. Dvarim and Moshe are "file over app": the engine runs in the page, and the shell hands it a folder. Daf has no folder and no engine to bundle. It's a FastAPI app with SQLite on the server that reads all those sources, so:
- The shells load the live site. They don't bundle the page and don't need the folder
bridge (
BRIDGE.mdin the other two repos). What they add is a proper window, the things a web page can't do, and small always-visible surfaces (menu bar, widget). - Base URL
https://daf.tiresia.xyz(Cloudflare tunnel; username + password sign-in). The tailnet addresshttp://<server>:8440also works, but it needs an ATS exception on the Mac and cleartext permission on Android. Make the base URL a setting, with the https one as the default.
The web app the shells wrap¶
Everything is in this repo (franz-rosenzweig/daf):
daf/: the Python server.web/: vanilla JS, no build step:app.js,hl.js,reader.js,book.js(an ES module, foliate-js inweb/vendor),paper.js,torah.js,stats.js,style.css,sw.js.docs/OPERATIONS.md: how it runs.PLAN.md: the phases and decisions.
Routes are hash routes, so the history works and native Back can call history.back():
#today, #review, #stats, #library, #feed, #read/<doc>, #ebook/<book>,
#highlights, #books, #book/<id>, #torah, #paper, #prefs. #…/hl:<id> opens a
highlight at its exact spot.
Sign-in in the WebView is the page's own form. It sets a cookie named daf (Secure on
https). Keep the default website data store so the cookie survives restarts.
Native surfaces use device tokens, never the cookie: Authorization: Bearer <token>. You
make one in Preferences → Phone → Make a token, or on the server with
/srv/dev/daf/bin/daf kindle-token <name> (printed once). Keep it in the Keychain on the Mac
and in EncryptedSharedPreferences on Android.
| Endpoint (device token) | What it gives |
|---|---|
GET /api/phone/widget |
{date, minutes, goal, year_day, year_hours, highlight: {text, title}, nightstand: [{id, title, progress}]}. Private books never appear by name. |
POST /api/save {url, title?, text?} |
Saves a link to the Library (through Karakeep on the next sync). |
GET /api/phone/nudge?app_name=<pkg> |
Asks whether to nudge right now. The server decides: at most one every 25 minutes, none once the hour is done. Answers {show, title, text, url, minutes_left}. |
Web-only (cookie): POST /share (multipart: url/title/text, or photo files that become
paper pages). It's the PWA's share target, and fine to reuse from a WebView.
Things the page does that a WebView needs help with:
- Downloads:
<a download>links (highlight exports at/api/highlights/export?fmt=md|csv|json…,/api/ebooks/<id>/file) and blob URLs. On the Mac,WKDownloadDelegatewith a save panel; on Android, aDownloadListenerand the create-document screen. - Share as image:
hl.jsmakes a PNG on a canvas, then triesnavigator.share({files})andClipboardItem. Neither is dependable in a WebView, so give the page a native share (below). - Listen: Kokoro read-aloud via
new Audio(blob). The Mac is fine. On Android it stops when the screen goes off, so it needs native playback (round 3). - Reading time counts only while the page is visible and active, by
document.visibilityStateand input. Make sure a minimized or hidden window reportshidden, or Daf counts minutes nobody read. - Service worker: the page registers
/sw.jsfor offline. Check it behaves in WKWebView and Android WebView. If it doesn't, skip it in shells (seeweb/shell.jsbelow). - The book reader puts foliate-js in iframes with blob URLs, and fonts come from Google Fonts. Test a long EPUB early in WebKit; Daf was built and tested in Chromium.
The contract, and who touches what¶
Write docs/SHELL.md first: the contract between page and shell, the way BRIDGE.md is for
Dvarim and Moshe. Keep it short. It should cover:
- User agent: append
Daf/<version> (macOS shell)orDaf/<version> (Android shell). window.dafShell(the page calls it):share({text?, url?, png?}),download(url | {name, mime, base64}),openExternal(url),notify({title, text, route}). A capability list, so the page falls back to web behavior when an op is missing.window.dafCommand(name)(the shell calls it): for the menus. Names likego:today,go:review,search,toc,panel,aa,listen,highlight,bookmark,back. Pluswindow.dafState()→{route, title, minutes, goal, canHighlight, …}, so menus can be ticked and greyed.window.dafBack(): true if the page went back (it closed a sheet or popover, or moved back in history); false when there's nowhere to go.
Page-side code goes in a new file, web/shell.js, loaded by index.html and doing nothing
unless the shell user agent is present. That way the shells' work doesn't collide with work on
the web app, where app.js, hl.js, reader.js, book.js and style.css change often. If
something has to change in one of those, keep it small and say so in the PR. Native code lives
in mac/ and android/. One branch and one PR per piece.
Deploying: daf.service on the server serves web/ straight from /srv/dev/daf, so a merged
change to web/ is live once the server pulls master. Python changes also need
systemctl --user restart daf. There's no autodeploy for Daf yet, so a merged change goes
live when master is pulled on the server.
Round 1: Daf for macOS¶
Start from Moshe's mac/ (SwiftUI with an AppKit window, project.yml for XcodeGen,
build.sh, selftest.sh), renamed to xyz.tiresia.daf. macOS 13+.
- The window: a WKWebView on the base URL, styled like Readwise Reader's Mac app.
- The page runs to the top edge under a transparent title bar, and the traffic lights sit
in the sidebar's top-left. The page leaves room for them when it sees the macOS shell
user agent: a
shell-macclass fromweb/shell.js. - Dragging an empty part of the top bar moves the window.
- The frame is remembered; minimum 900 × 600.
- Only Daf loads in the window. Any other address opens in the browser.
- Menus: File (Save a link…, Log paper minutes…, Export highlights…),
View (Today ⌘1, Review ⌘2, Library ⌘3, Feed ⌘4, Highlights ⌘5, Books ⌘6, Stats ⌘7,
Contents, Side panel, Typography), Go (Back ⌘[, Search ⌘K), Reader (Highlight, Note,
Bookmark, Listen). Each item calls
window.dafCommand. The page hears keys first, the way Moshe does it. - Menu-bar extra (
MenuBarExtra): - The title is today's minutes:
38′, or60′ ✓once the hour is done. - The menu shows the ring with "38 of 60 minutes today · Year 30, day N", then the day's highlight (serif, 4 lines) with its book, then Open Daf, Review, and Settings… (base URL and token).
- Data from
/api/phone/widget: poll every 5 minutes while awake and on wake, and keep the last answer when offline. - Share extension: "Save to Daf" for links in the macOS share menu, through
POST /api/save. - Notifications: none in round 1. The morning highlight already comes through Signal (Ezer).
- Done when:
- A whole evening of reading works in the Mac app: an article, a book chapter with highlights, and the review.
- The minutes sit in the menu bar.
- One link is saved from Safari's share menu.
Round 2: Daf for Android¶
Start from Dvarim's android/shell and Moshe's android/ (WebView, release.sh, keystore),
renamed to xyz.tiresia.daf. Test on a real phone.
- The app: a WebView on the base URL.
- Back goes through
window.dafBack()first. - The keyboard resizes the window (
adjustResizeplus insets). - Only Daf loads in the window.
- In the book reader (
#ebook/…), the volume keys turn pages: call the page'sdafCommand('next')/('prev'). Nobody gets this from a browser. - Share into Daf:
- Links go to
POST /api/save. - Photos go to
POST /shareasphoto; they become paper pages for OCR. - Plain text with a URL in it counts as a link.
- Home-screen widget (the point of the app: the hour in sight on the first screen, where Instagram and Reddit are now):
- 2×2: a ring (
minutesofgoal) with the number in the middle and "Year 30 · day N" under it. - 4×2: the ring on the left. On the right, the highlight in a serif face (3 lines) and the
book title, or "That's the hour." once
minutes >= goal. - Material You colors, following the system theme.
- Refresh with WorkManager every 30 minutes, and on
ACTION_USER_PRESENTat most every 10 minutes. On failure keep the last numbers and dim them. - Tapping the ring opens
#today; tapping the highlight opens#review. - The evening nudge, natively: this replaces the Tasker setup that's still sitting in Preferences → Phone.
- With Usage Access granted, watch for Instagram, Reddit, X or Discord coming to the front between 21:00 and 03:00.
- Ask
GET /api/phone/nudge?app_name=<package>and, ifshow, post a notification (title,text) that opensurl. - The server keeps the rules and counts what was shown and followed.
- Make it a switch in the app's settings, off until it's turned on.
- App shortcuts (long-press the icon): Review, Continue reading (the first nightstand
book,
#ebook/<id>), Log paper. - Done when:
- The 4×2 widget sits on the phone's first screen and refreshes on its own.
- A link and a photo were shared in.
- The volume keys turn pages.
- Build steps are in
android/README.md, the way Dvarim's are.
Round 3, after a week of using both¶
- Listen with the screen off on Android: a media session and a foreground service playing
Daf's Kokoro paragraphs. The page hands over the paragraph texts, or the shell asks
/api/tts/textitself. - Offline reading for books on the phone (cache the EPUB and the position).
- Whatever a week of living with it asks for.
Rules carried over from Dvarim and Moshe¶
- Interface copy:
- Plain labels in the web app's voice: sentence case, no emoji, no exclamation marks.
- Say what it is: "Counting toward today", "That's the hour.", "Save to Daf".
- Never mention Claude or prompts in the UI.
- Data:
- Private books stay private: the server already reduces them to "Private reading". Never show a title the server didn't send.
- Don't put anyone's email address anywhere.
- Look: Readwise Reader.
- Ask the owner first before anything that costs money, publishes to a store, or changes the phone's system settings beyond the permissions the app asks for.