Warn when the years outrun the recurrence lookup tables (#61) #113
No reviewers
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED!113
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/recurrence-horizon-guard"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Addresses #61 — the guard, the naming and the docs. Not the table extension; see below for why that is deliberate.
The problem
Six recurrences are table lookups rather than computations, all covering 2020–2035:
HINDU_LUNAR_TABLELUNAR_NEW_YEAR_TABLEASTRONOMICAL_DATESPast the last tabulated year they raise,
upsert_yaml_eventscatches, logs one line per event, and continues. From 2036 those six events would simply be absent from the calendar with nothing to explain why.Three changes
table_horizon()/tables_missing_year(), derived from the table data rather than hardcoded. The horizon is the minimum across tables — one running out is enough to drop its events, so extending only some of them changes nothing.One aggregated warning instead of per-event lines lost in startup noise:
GET /api/v1/recurrence-horizon+ a header banner, so the condition is visible without reading logs. Naming the events is the point — "Diwali is missing" is actionable, "some events failed to resolve" is not.The endpoint sits at
/recurrence-horizon, not under/events/, whose/{event_id}route would shadow a literal segment unless declared first — the trap #93 fixed for/events/resolved.Why the tables aren't extended here
#61 also asks to extend them to 2045. I checked whether that could be done from what is installed, and it can't:
holidays— already a dependency, and it stops at 2035 itself:holidays.country_holidays("IN")warns "Requested Holidays are available only from 2001 to 2035". It also returns no Chinese New Year entries. So the obvious escape hatch has the same wall.astral— already a dependency, computes sunrise/sunset, but has no solstice or equinox function.Extending properly needs an ephemeris library (skyfield/ephem) for the solstices, a Chinese-calendar library for Lunar New Year, and a panchanga source for Diwali, which is tithi-based. I'm not hand-writing 60 astronomical dates — silently wrong dates are worse than absent ones, and would be far harder to notice.
Filed as its own issue.
docs/troubleshooting.mdrecords both findings plus the useful bit for whoever takes it: validate any computation against the existing sixteen known years first — reproducing 2020–2035 exactly is good evidence it can be trusted onward.Verification
ruff,ruff format,mypyclean; all seven pre-commit hooks pass.tsc -b,eslint,vitest(76),npm run buildall clean.🤖 Generated with Claude Code
Six recurrences are table lookups rather than computations -- Diwali, Lunar New Year, and the four solstices/equinoxes -- covering 2020-2035. Past the last tabulated year they do not raise anything a user sees: `upsert_yaml_events` catches the error, logs at warning level per event, and continues. From 2036 those events would simply be absent from the calendar with nothing to explain why. Three changes: - `table_horizon()` and `tables_missing_year()` in recurrence.py, derived from the table data. The horizon is the *minimum* across tables, since one running out is enough to drop its events. - The loader collects failures and emits one warning naming every affected event and every exhausted table, instead of a per-event line lost among startup noise. The per-event detail drops to debug. - `GET /api/v1/recurrence-horizon` plus a header banner in the app shell, so the condition is visible without reading logs. Named events matter here: "Diwali is missing" is actionable, "some events failed to resolve" is not. The endpoint deliberately sits at /recurrence-horizon rather than under /events/, whose /{event_id} route would shadow a literal segment unless declared first -- the trap #93 fixed for /events/resolved. Not extending the tables here, which the issue also asked for. The dates cannot be derived from what is installed: `holidays` warns "available only from 2001 to 2035" for India and has no Chinese New Year entries, and `astral` computes sunrise/sunset but no solstices. Doing it properly needs an ephemeris library and a panchanga source, or hand-curated data from an authority -- filed separately rather than guessed at here. Both findings are recorded in docs/troubleshooting.md along with the advice to validate any computation against the existing sixteen known years first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>The "clicking a day opens the side panel" E2E asserted on `getByText(/Side panel|No events|January/i)`. That regex also matches the calendar's own month headings, so once both the calendar and the panel had rendered it resolved to two elements and tripped Playwright's strict mode: strict mode violation: ... resolved to 2 elements: 1) <h2>January</h2> 2) <div>No events on this date.</div> It only ever passed by racing -- asserting in the window before the second match appeared. Adding the horizon query to the app shell shifted render timing enough to lose that race, which is how a latent test bug became a red run. The assertion now targets the panel itself rather than text that happens to be near it. SidePanel's root becomes `<aside aria-label="Day details">`, so the test can use `getByRole("complementary", ...)` -- a stable target that does not depend on whether the selected day has events, and a named landmark for screen readers, which the bare div was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>