-
v1.0.0
StableAll checks were successfulCI / Alembic migration check (push) Successful in 39sCI / Frontend lint, test & build (push) Successful in 1m12sCI / Pre-commit hooks (push) Successful in 1m29sCI / Python lint & type-check (push) Successful in 1m51sCI / Python tests (push) Successful in 1m58sRelease / Publish image & release (push) Successful in 2m59sCI / Docker build, health smoke & E2E (push) Successful in 5m28sreleased this
2026-09-01 19:59:22 +00:00 | 29 commits to main since this releaseAdded
-
CI runs the pre-commit hooks (#58) —
.pre-commit-config.yamlexisted but
nothing executed it, so five of its seven hooks (whitespace, EOF, YAML,
merge-conflict and large-file checks) had no equivalent anywhere and were
enforced only where a contributor had runpre-commit install. APre-commit hooksjob now runs them all. Three things surfaced on first execution:
check-yamlwas failing ondocker-compose.registry.yml(Compose's!reset
tag is not standard YAML — now excluded, Compose validates that file itself);
the ruff hook was pinned to 0.6.9 while the backend resolved 0.15.21, a skew
that would show as one job failing on formatting another considers clean (now
pinned together, with Renovate'spre-commitmanager enabled to keep them
moving); and the prettier hook was removed, having never run, not being a
frontend dependency, and sourcing from an archived upstream mirror — it would
have reformatted 62 files. Frontend style remains gated by ESLint's
--max-warnings 0. -
Browsable WLED preset import (#48) — spec §3.6 has always called for parsing a
presets.jsonand "displaying them in a browsable import panel" where the user
"selects which presets to import"; what existed was a textarea, a dropdown, and one
preset per round trip. The Import dialog is now a file picker (paste still available)
over a scrollable list showing each preset's colour swatch, name and source segments,
with a checkbox and per-row event/phase selector. Segment mapping is a row per source
segment mapped to this install's named segments, replacing the raw JSON field.
Collisions resolve against the current library before sending — an approved slot
blocks, a candidate warns it will be replaced — because the new
POST /import/presets/batchapplies the whole selection in one transaction under a
single backup, so one bad row would otherwise discard every good one. -
Quick push accepts an arbitrary colour, not just a scheme ID (#48) — the spec
has always described quick push as pushing "an arbitrary color or scheme", but only
the scheme half was implemented.POST /wled/quick-pushnow takes either
scheme_idorcolor(an RGB triple) and rejects a body carrying both or neither;
the colour is applied solid to every themed segment with effect and palette forced
to 0. NoSchemerow is involved, so an ad-hoc push leaves nothing behind in the
library, and the revert paths (next scheduler event,duration_mins, manual cancel)
are identical either way. The toolbar's Quick push dialog gained a Scheme / Colour
tab pair with a colour picker. -
Scheduled daily DB backup with a tested restore path (#57) — a new 04:00
quiet-hour job (daily_backup) snapshots the SQLite DB every day, independent of
bulk operations, retained viaDAILY_BACKUP_RETAIN_COUNT(default 14) separately
fromBACKUP_RETAIN_COUNT(pre-bulk-op backups). Backups now use SQLite's online
backup API (sqlite3.Connection.backup) instead of a raw file copy, so a live DB
is never copied mid-write. The restore procedure is documented in
docs/troubleshooting.mdand exercised by an automated test
(test_restore_from_backup_recovers_original_data). -
A
:mainimage is published on every merge (#64) — after the health smoke
test and E2E pass on a push tomain,ci.ymlpushes to
git.rhoving.com/rbrooks/iris-wledas:mainand:sha-<short>. The image
is the one the E2E ran against, retagged rather than rebuilt, so the published
artifact is bit-for-bit what was tested. Registry auth uses the built-in
Actions token; pull-request runs never authenticate. This is what lets the dev
server deploy by pulling instead of building on the host (#65). -
Deploying from the published image (#65) —
docker-compose.registry.yml
runsgit.rhoving.com/rbrooks/iris-wled:${IRIS_TAG:-main}instead of building
on the host, withIRIS_TAGselecting a:sha-<short>tag to roll back to a
specific commit. It is a dedicated file, notdocker-compose.override.yml, so
the defaultdocker compose up -dstill builds from source and the README
quickstart is unaffected. The deploy sequence is documented in
docs/setup.md§6. -
Event data pipeline — unified
eventsmodel with stableevent_keys and self-referential
overrides. Recurrence resolvers forfixed,nth_weekday,hebrew(pyluach),islamic
(hijri-converter),hindu_lunar,computed(Easter, Lunar New Year, solstices/equinoxes), and
span. Integrates theholidayslibrary, bundledconfig/events.yaml, mounted custom events,
and an optional Calendarific layer. Priority resolution (month-long backdrop vs. single-day
override), per-date primary pins, user-event shadowing, and a current+next-year cache. -
Segments & schemes (data layer) — segment CRUD, phase resolver
(eve/opening/mid/closing), scheme resolution order (user-saved → built-in library by
event_key/tag), daily preset generation per(event, phase), preset invalidation with
candidate auto-regen and approved-preset stale flagging, a typed settings service, an audit
writer, and a timestamped-backup service with retention pruning. -
WLED integration — a single-device
WLEDControllerabstraction (structured for future
multi-controller support), httpx client with timeout/retry, JSON state/info/presets wrappers, a
scheme→WLED translator, single + carousel preview with auto-restore, quick-push with auto-revert,
bounded preset sync with capacity safety checks, a connection-status tracker, and configurable
fallback policies (retry/last_known_good/silent). -
Scheduler & time envelope — timezone-aware APScheduler with a persistent job store; solar
scheduling viaastraland fixed-clock scheduling; daily on / late-night-dim / off jobs; a weekly
candidate-generation job; a yearly Jan 1 backup + full-year candidate job; aschedule_log; and
fallback retry wrapping every push. DST and tz-change handling covered by tests. -
Authentication & authorization — OIDC via Authlib, server-side
sessions,HttpOnly/Secure
cookies with CSRFstatehandling, role extraction withrequire_admin/require_viewerguards on
every endpoint, aDISABLE_AUTH=truedev mode with a UI banner, and authenticated actor identity
bound into the audit log with before/after diffs. -
Year review UI — generated TypeScript API types, a TanStack Query client, an app shell with
connection status / year selector / alerts / user menu, an auth guard, a 12-month calendar grid
with month-long backdrop bars and per-segment gradient strips, a day detail panel, an inline scheme
editor (per-segment HSV/RGB pickers, effect picker, sliders, save-as-named, live preview), action
buttons, a settings page, filter chips, bulk approve, quick push, a toast/alert surface, and a
responsive accessible theme. Covered by Vitest component tests and a Playwright happy-path E2E. -
AI suggestion pipeline — a
SuggestionProviderabstraction withOllama,Anthropic, and
OpenAICompatibleimplementations, a provider factory, a context-rich prompt builder with few-shot
examples, Pydantic response validation with one retry then built-in fallback, and a rolling-24h
request counter enforcingAI_DAILY_LIMIT. -
Notifications — an async webhook dispatcher with HMAC-SHA256 signing, verbatim custom headers,
per-endpoint event subscriptions, retry with backoff, wired toscheme_pushed,push_failed,
review_queue_ready, andquick_push, plus a webhooks settings page with a test-send button. -
Import / export & built-in content —
presets.jsonexport (full + per-event), WLED
presets.jsonimport with a pre-import backup and a segment-mapping wizard, and a built-in scheme
library covering every bundled event with tag-based templates and headlineevent_keyentries. -
Hardening & release readiness — a Docker
HEALTHCHECK, compose resource-limit guidance,
structured JSON logging, an optional env-gated Sentry hook, a setup guide (docs/setup.md), a
troubleshooting guide (docs/troubleshooting.md), an OIDC/OWASP-ASVS review, and dependency,
bundle-size, and query-index audits.
Changed
-
The preview carousel cycles upcoming schemes, not one day's (#48) — it
previously ran over[scheme.id], the selected day's single scheme, which is
a one-element carousel and duplicated the Preview button beside it. Starting
one moved to the toolbar, where a window (next 7 / 30 / 90 days) is a
meaningful choice; "upcoming" is not a property of whichever day is selected.
It walks the resolved day map so it shows what will actually run — no-event
days contribute nothing and a multi-day event appears once, not once per day —
and it is available to viewers, since the spec allows them to trigger bounded
previews. The stop control stays in the side panel and header. -
Preset export is available to viewers, not just admins (#48) — the spec's
rule for the API surface is that "GET endpoints allowviewerunless noted
otherwise", and neither export row is noted admin-only (unlike/audit-log).
BothGET /export/presets.jsonandGET /export/events/{id}.jsonnow take
require_viewer, and the toolbar's Export button moved outside the admin-only
block — un-gating the API alone would have left viewers with no way to reach it. -
Single-platform container images (#79) — Iris now publishes
linux/amd64
only. Thelinux/arm64half of the build is gone fromci.ymland
release.yml, together with the QEMU emulation step and the redundant second
image build that ran on every push tomain. Spec §13.4 had called arm64 "a
hard requirement" on the assumption that self-hosters would run Iris on a
Raspberry Pi, but no arm64 image was ever run or validated. Iris is an HTTP
client to the WLED controller with no affinity to the lighting hardware, so it
runs on any always-on host on the LAN. Re-adding a platform is a one-line
platforms:change in the two build workflows. -
CI cancels superseded runs (#78) —
ci.ymlgains a top-levelconcurrency
group keyed on workflow + ref withcancel-in-progress, so a new push
supersedes the previous run instead of both occupying the runner pool shared
across every repo on the instance. Because the E2E cleanup is run-scoped and a
cancelled job is not guaranteed to reach it, the docker job now also sweeps
iris-*/pw-*containers andiris-net-*networks older than 2h — old enough
that they cannot belong to a live concurrent run. -
Dependency auditing moved out of CI onto a schedule (#81) —
pip-audit
andnpm auditnow run weekly from.forgejo/workflows/audit.ymlinstead of
as a job in every CI run. The job was markedcontinue-on-error: trueand
described as advisory, but on this runner a failingcontinue-on-errorjob
still marks the whole runfailure— so every CI run was red regardless of
the change under test, and enabling #56 (require green CI before merge) would
have blocked every merge, including whenever a new CVE landed on a transitive
dependency. The audit now reports on its own schedule and gates nothing.
Fixed
-
The lights are switched off again (#106) — the nightly reschedule was
cancelling the pending sunrise-off before it could fire, so Iris never turned
the lights off at all: they came on and dimmed on schedule, then stayed lit
through the following day. A lighting day runs sunset→sunrise, not
midnight→midnight, but the schedule was computed from the calendar date, so
the 00:01 reschedule recomputed the off as tomorrow's sunrise and — because
every run writes the singleoff_jobid withreplace_existing=True—
discarded the one due a few hours later. The failure was silent: a job that
never runs writes no log row, and every job that did run reported success.
Newget_active_times()carries a still-pending off forward from the previous
cycle;/schedule/statususes it too, so the reported off is the one that will
actually fire. -
Playwright image tag now follows the lockfile (#38, #39) —
ci.ymlpinned
mcr.microsoft.com/playwright:v1.60.0-noblewhilepackage.jsondeclares
@playwright/test: ^1.47.0. Any Renovate bump inside that caret range left the
browsers baked into the image out of step with the packagenpm ciinstalled,
and every E2E test failed with "Executable doesn't exist ... Please update
docker image as well". The tag is now read from
frontend/package-lock.jsonat run time, so the image and the package cannot
drift apart again. Resolves the E2E failure blocking the Renovate queue. -
shadcn/ui primitives no longer fail lint on fast-refresh warnings (#35) —
newereslint-plugin-react-refreshreleases emit
react-refresh/only-export-componentsfor the vendoredsrc/components/ui/
files, which export components alongside re-exported Radix parts. With
npm run lintrunning at--max-warnings 0, 15 warnings and zero errors
failed the build. The rule is now off forsrc/components/ui/**only, and
stays on for all application code. -
Year-rollover job completes the boundary refresh — the Jan 1
yearly_jobnow
populates event sources into the DB and warms the in-memory year cache for the new
and next year before regenerating candidate schemes, so rolled-over events are
queryable without a restart (M1/M8). It also fires thereview_queue_readywebhook
on completion, matching the weekly candidate job (M4). Previously the job only ran a
backup and scheme regeneration, leaving next-year events absent until a restart.
Security
- The dependency audit is clean again (#81) —
npm audit --audit-level=high
reports 0 vulnerabilities. Four of the six advisories cleared through ordinary
Renovate bumps (react-router-domv7,nanoid,postcss, and the
typescript-estreeroute tobrace-expansion). The last two were reachable
only through eslint's own dependency tree at the latest published eslint —
minimatch@3 → brace-expansionand@eslint/eslintrc → js-yaml— so no
dependency PR could ever have fixed them. Both are pinned to patched in-range
versions viaoverrides, scoped so the healthybrace-expansion@5used by
@typescript-eslintis untouched. Renovate does not manageoverrides, so
removal conditions are tracked in #101 and documented in CONTRIBUTING.md.
Infrastructure
- CI moved to Forgejo Actions — lint, type-check, backend + frontend tests, E2E, dependency
audits, a migration-consistency check, and a Docker image build with a booted-container health
smoke test, all running ongit.rhoving.com. - Authoritative images —
linux/amd64images published to the Forgejo container registry at
git.rhoving.com/rbrooks/iris-wled. - Release automation — pushing a
vX.Y.Ztag builds and publishes images and creates the
matching Forgejo release from this changelog.
Deferred
- Progressive HSV phase interpolation (spec §4.2) — deferred to v1.1. The scheme
generator emits one preset per phase (eve/opening/mid/closing) with colors
resolved independently per phase; smooth HSV interpolation across phases requires a
per-day preset model and per-event opt-in and is tracked as a v1.1 follow-up (see the
note inservices/scheme_generator.py). Not half-implemented.
Year-rollover QA (#8) passed against a clock-shifted instance. The end-to-end operational
dry run (#7) verified every leg against real hardware — configure/approve, push, the
scheduler's on/dim/off, and all three fallback policies; it stays open underv1.1.0only
to confirm one ordinary unattended night, the mechanism itself having been reproduced and
fixed (#106). HSV phase interpolation is deferred to v1.1 as described above. The v2
backlog (scheme version history, Home Assistant/MQTT, multi-controller) is tracked under
thev2.0.0label.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-