Finding of the Holy Cross (May 3), the Transfiguration (Aug 6), the Exaltation of the Holy Cross (Sep 14), and Dedication of the Basilica of the Most Holy Savior (Nov 9, the Lateran) were all excluded under the same stale "fixed feast of the Lord, not modeled" note that turned out wrong for Our Lady of the Snows. All four have real, complete content in the reference engine. Presentation of the BVM (Nov 21) was excluded under the equivalent "fixed BVM feast" note and is added the same way. - finding-of-the-holy-cross: fully proprium, own collect/antiphon/Matins throughout; now correctly outranks ss-alexander-eventius-theodulus-and-juvenal, matching what that saint's own file already documented as the live reality. - transfiguration: same proprium shape. Its subordinate saints (Ss. Xystus/Felicissimus/Agapitus) stay excluded -- blocked by the real, separate octave/subordination-rendering gap, not by an unmodeled Transfiguration. - exaltation-of-the-holy-cross: reuses the Finding of the Holy Cross's content wholesale (ex Sancti/05-03) -- collect/antiphon via a cross-id `propers` pointer, Matins nocturn-readings duplicated (no cross-id lookup exists for that mechanism). - dedication-of-the-basilica-of-the-most-holy-savior: reuses the existing Common-of-a-Dedication (collect-c8, benedictus-antiphon- common-of-a-dedication), real unique Nocturn 2 vita of her own. Her secondary, St. Theodore, stays excluded for the same octave/subordination reason as above. - presentation-of-the-bvm: real unique collect of her own; her Benedictus antiphon is a substituted Common reference (truncate-and-add-alleluia edit specific to this feast), authored as her own file. Also: our-lady-of-the-snows.yml was missing its own Nocturn 3 (she defers to Commune/C11's own Gospel-incipit-plus-Bede-homily, same as Presentation of the BVM) -- added. Test updates (not regressions): three tests had fixed proof dates colliding with the newly-added feasts, updated to reflect the new, correct behavior. npm test (365 tests) and tsc --noEmit both pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vu
A static, offline-first PWA for praying the Divine Office — Matins, Lauds, Prime, Terce, Sext, None, Vespers, and Compline — in bilingual Latin/English, for any given date.
This isn't a reconstruction of any single historical breviary. It's a personal design that
draws on several historical sources without reproducing any one of them exactly: hour
structure chiefly from the Monastic Tridentinum (1617), precedence/rubrics from Divino
Afflatu, and a calendar merged from both the Tridentine 1906 and Divino Afflatu calendars,
with more generous handling of commemorations and octaves than any one historical rite used.
See CLAUDE.md (gitignored, not shipped in this repo) or the project's own notes for the full
rationale if you're extending it.
There's no backend and no database — everything is a pre-built static bundle, and the app is designed to keep working fully offline once it's been loaded once.
Development
Requires Node.js (developed against Node 20+).
npm install
npm run dev # start the Vite dev server
npm run build # type-check (tsc --noEmit) and produce a production build in dist/
npm run preview # serve the production build locally
npm test # run the test suite (vitest)
Run a single test file or a subset by name:
npx vitest run tests/hours/vespers.test.ts
npx vitest run -t "some test name"
Running in production
The app is entirely static once built, so "running it" just means serving dist/ — any static
file host works, as long as it falls back to index.html for unknown paths (the app does its
own client-side routing for URLs like /2026-08-09/prime).
Docker
A Dockerfile builds the static bundle with Node and serves it with Caddy:
docker build -t vu .
docker run -p 8080:80 vu
Or with Docker Compose (docker-compose.yml is included):
docker compose up -d --build
This serves the app on http://localhost:8080. Put a reverse proxy/TLS terminator in front of
it for a real deployment; the container itself just listens on port 80 internally and has no
configuration, secrets, or external dependencies.
Without Docker
Any static file server works — build the app, then serve dist/ with SPA-style fallback
routing. For example, with Caddy directly:
npm run build
caddy file-server --root dist --listen :8080
(For a real deployment you'll want a fallback rule so unknown paths resolve to index.html —
see the Caddyfile in this repo for the pattern.)