A Clark View source that models the lunar calendar and surfaces only the 15th day of each lunar month. No other lunar day is stored or served.
Built on the shared source SDK:
protocol v1 over rpc.ts, bearer-authenticated, SQLite owned by this val.
A lunar month begins on the civil day containing its new moon, so its 15th day is that day plus fourteen. "Which civil day" depends on a zone, and the calendar must fix one or the same instant lands on different dates for different readers. This source anchors on UTC+8, the conventional civil anchor for the East Asian lunisolar calendar.
New-moon instants come from Meeus, Astronomical Algorithms ch. 49, minus the small planetary terms — the residual is well under a minute, far below what a day boundary needs. The calendar is therefore computed, not fetched: there is no upstream provider, no egress, and nothing that can go stale or 403.
Two published dates pin the convention end to end in check.ts: Lunar New Year
2026 (month 1 day 1) on 2026-02-17, and Mid-Autumn 2026 (month 8 day 15) on
2026-09-25.
Lunar months are not numbered. Correct lunisolar numbering needs leap-month and solstice rules this source does not implement, and a wrong month number is worse than none.
| File | Role |
|---|---|
lib/lunation.ts | Pure arithmetic: new-moon instants, civil dates, a year of 15ths. No storage, clock or network. |
lunarSource.ts | Source definition, offset policy, item text, lunar_fifteenths storage and rebuildYear. |
rpc.ts | HTTP entry: bearer authentication around serveSource. |
ingest.ts | Annual interval; see below. |
check.ts | Contract, calendar, offset, protocol, auth and storage checks. Disposable rows are removed. |
Settings are exactly {} — there is nothing to select, and the parent's browser
form renders no controls. One item is returned: the next lunar 15th on or after
the reader's current civil date.
{ "id": "lunar15-2026-09-25", "mainText": "Lunar 15th", "subText": "Cycle from Sep 11", "caption": "DAY 15", "emphasized": false, "timestamp": 1790362800 }
(local noon on 2026-09-25 for a reader at UTC-7)
timestamp is Unix seconds at the reader's local noon on that date, so the
widget's own day label is the lunar 15th in every timezone. The lunar 15th is a
whole day rather than an instant, and caption is set so the widget does not
render that anchor time as if it meant something — the same reasoning behind
Moon's PEAK.
utcOffsetSeconds: null falls back to America/Los_Angeles, matching Moon.
It is not treated as UTC.
ingest.ts runs on cron 0 0 1 12 * — once a year, on 1 December UTC — and
rebuilds the whole of the next year, replacing that year's rows authoritatively.
It also rebuilds the current year, which is normally already stored and
identical; including it means one run of the file is also the deployment seed,
so there is no separate first-year bootstrap to forget.
The same work is reachable over the protocol as the write operation
calendar.rebuild with payload { "year": 2027 }, for backfilling a year out
of band. It rejects invalid_rebuild_payload outside 1900–2999.
The interval runs inside this val and so writes this val's SQLite directly.
Storage is provisioned by ensureSchema during ingest, never on a feed read.
- Set
SOURCE_LUNAR_V1_TOKENon this val. - Register in
plusjade/sports-today:rpc.ts's endpoint, source keylunar, protocol version 1, and a credential reference holding the same bearer value configured independently in the parent. Then assign it to a device. - Run
check.tsafter any change. With the token set it also covers the authenticated mount; without it, everything but that one assertion still runs.