• v1.0.0 0251a6ab9b

    v1.0.0
    All checks were successful
    CI / Alembic migration check (push) Successful in 39s
    CI / Frontend lint, test & build (push) Successful in 1m12s
    CI / Pre-commit hooks (push) Successful in 1m29s
    CI / Python lint & type-check (push) Successful in 1m51s
    CI / Python tests (push) Successful in 1m58s
    Release / Publish image & release (push) Successful in 2m59s
    CI / Docker build, health smoke & E2E (push) Successful in 5m28s
    Stable

    claude-bot released this 2026-09-01 19:59:22 +00:00 | 29 commits to main since this release

    Added

    • CI runs the pre-commit hooks (#58) — .pre-commit-config.yaml existed 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 run pre-commit install. A Pre-commit hooks job now runs them all. Three things surfaced on first execution:
      check-yaml was failing on docker-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's pre-commit manager 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.json and "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/batch applies 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-push now takes either
      scheme_id or color (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. No Scheme row 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 via DAILY_BACKUP_RETAIN_COUNT (default 14) separately
      from BACKUP_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.md and exercised by an automated test
      (test_restore_from_backup_recovers_original_data).

    • A :main image is published on every merge (#64) — after the health smoke
      test and E2E pass on a push to main, ci.yml pushes to
      git.rhoving.com/rbrooks/iris-wled as :main and :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
      runs git.rhoving.com/rbrooks/iris-wled:${IRIS_TAG:-main} instead of building
      on the host, with IRIS_TAG selecting a :sha-<short> tag to roll back to a
      specific commit. It is a dedicated file, not docker-compose.override.yml, so
      the default docker compose up -d still builds from source and the README
      quickstart is unaffected. The deploy sequence is documented in
      docs/setup.md §6.

    • Event data pipeline — unified events model with stable event_keys and self-referential
      overrides. Recurrence resolvers for fixed, nth_weekday, hebrew (pyluach), islamic
      (hijri-converter), hindu_lunar, computed (Easter, Lunar New Year, solstices/equinoxes), and
      span. Integrates the holidays library, bundled config/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 WLEDController abstraction (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 via astral and fixed-clock scheduling; daily on / late-night-dim / off jobs; a weekly
      candidate-generation job; a yearly Jan 1 backup + full-year candidate job; a schedule_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 CSRF state handling, role extraction with require_admin/require_viewer guards on
      every endpoint, a DISABLE_AUTH=true dev 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 SuggestionProvider abstraction with Ollama, Anthropic, and
      OpenAICompatible implementations, 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 enforcing AI_DAILY_LIMIT.

    • Notifications — an async webhook dispatcher with HMAC-SHA256 signing, verbatim custom headers,
      per-endpoint event subscriptions, retry with backoff, wired to scheme_pushed, push_failed,
      review_queue_ready, and quick_push, plus a webhooks settings page with a test-send button.

    • Import / export & built-in contentpresets.json export (full + per-event), WLED
      presets.json import 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 headline event_key entries.

    • 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 allow viewer unless noted
      otherwise", and neither export row is noted admin-only (unlike /audit-log).
      Both GET /export/presets.json and GET /export/events/{id}.json now 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. The linux/arm64 half of the build is gone from ci.yml and
      release.yml, together with the QEMU emulation step and the redundant second
      image build that ran on every push to main. 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.yml gains a top-level concurrency
      group keyed on workflow + ref with cancel-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 and iris-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
      and npm audit now run weekly from .forgejo/workflows/audit.yml instead of
      as a job in every CI run. The job was marked continue-on-error: true and
      described as advisory, but on this runner a failing continue-on-error job
      still marks the whole run failure — 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 single off_job id with replace_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.
      New get_active_times() carries a still-pending off forward from the previous
      cycle; /schedule/status uses it too, so the reported off is the one that will
      actually fire.

    • Playwright image tag now follows the lockfile (#38, #39) — ci.yml pinned
      mcr.microsoft.com/playwright:v1.60.0-noble while package.json declares
      @playwright/test: ^1.47.0. Any Renovate bump inside that caret range left the
      browsers baked into the image out of step with the package npm ci installed,
      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.json at 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) —
      newer eslint-plugin-react-refresh releases emit
      react-refresh/only-export-components for the vendored src/components/ui/
      files, which export components alongside re-exported Radix parts. With
      npm run lint running at --max-warnings 0, 15 warnings and zero errors
      failed the build. The rule is now off for src/components/ui/** only, and
      stays on for all application code.

    • Year-rollover job completes the boundary refresh — the Jan 1 yearly_job now
      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 the review_queue_ready webhook
      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-dom v7, nanoid, postcss, and the
      typescript-estree route to brace-expansion). The last two were reachable
      only through eslint's own dependency tree at the latest published eslint —
      minimatch@3 → brace-expansion and @eslint/eslintrc → js-yaml — so no
      dependency PR could ever have fixed them. Both are pinned to patched in-range
      versions via overrides, scoped so the healthy brace-expansion@5 used by
      @typescript-eslint is untouched. Renovate does not manage overrides, 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 on git.rhoving.com.
    • Authoritative imageslinux/amd64 images published to the Forgejo container registry at
      git.rhoving.com/rbrooks/iris-wled.
    • Release automation — pushing a vX.Y.Z tag 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 in services/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 under v1.1.0 only
    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
    the v2.0.0 label.

    Downloads