Memorial events: commemorative lighting for people who have died #80
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 project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#80
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Requested by the operator 2026-08-30, prompted by wanting to light the house for a public figure who had just died. The general want: a first-class way to commemorate a specific person on a specific date.
Most of this already works — do not rebuild it
A memorial is structurally a user-defined event, and that path is complete today:
Event(models/events.py:12) already carriesname,description,tags,start/end,priority,duration_class,phase_configEventCreate(schemas/events.py:94) already defaultstype="user_defined",priority=100prompt_builderreceives the wholeEvent, soname,description, andtagsalready reach the modelSo today, with no code changes,
POST /eventswith a name, a date, and a description yields a generated candidate scheme you can review, approve, and push. The work below is the gap around that, not a new subsystem.Gap 1 — the AI prompt is holiday-shaped (highest value)
prompt_builder._SYSTEMopens with "generate a JSON lighting scheme for a holiday or observance event", and_few_shot_examples()pulls festive entries frombuiltin.yaml. Asked for a memorial, it will produce celebratory output — high brightness, fast effects.global_brightness, slow//gentleeffect_idandeffect_speed, longtransition_msevent_keysin the fileGap 2 — no non-AI fallback
AI_PROVIDERis optional and capped byAI_DAILY_LIMIT. Events without AI are covered by tag templates inschemes/builtin.yaml, and there is nomemorialtag — so with AI unconfigured this feature produces nothing appropriate.memorialtag template toschemes/builtin.yaml: dignified default (soft amber/white, slow breathe), works with no AI configuredGap 3 — recurrence
Decision: one-off by default, with an opt-in annual remembrance.
User events are concrete date rows; only bundled YAML events get rule-based recurrence. A one-off works today. For the annual case:
yearly_jobmaterialise next year's occurrence for annual memorials, reusing the sameevent_keyWorth noting: because approved schemes are reused by
event_key+ phase, an annual memorial gets scheme reuse for free — approve Dolly once and the anniversary lights the same way every year with no AI call. That mechanism already exists; this just has to create the row.Gap 4 — no UI affordance
Priority
Decision: configurable, shipping at the existing 100 default.
No new priority band and no resolver change — memorials inherit the user-defined default of 100 and therefore outrank everything, including public holidays. The create form surfaces the priority field prominently so it can be lowered per person. The existing per-date pin (§2.5) remains the escape hatch for a specific collision.
Consequence to keep in mind: a memorial on Dec 23 will override Christmas Eve unless the priority is lowered or the date is pinned. That is the intended default.
Scheme source
Decision: generic dignified default, AI person-specific as opt-in.
The
memorialtag template is what a new memorial gets. Asking the AI for a palette drawn from the person's associations is an explicit action whose output lands as a candidate, reviewed before it ever reaches the lights.This ordering is deliberate: mischaracterising a real person is worse than a tasteful generic scheme, and Iris's existing candidate → review → approve gate already ensures nothing unreviewed goes live. The AI suggests; a human always decides.
Out of scope
Acceptance
Landed in #138 (squash-merged to
main). All four gaps, all five acceptance criteria.A Memorial button on the calendar toolbar takes a name, a date, optional notes and a "remember every year" toggle, and produces a candidate scheme to review. The issue is right that most of this already worked, so the change is deliberately thin — there's a test asserting the generic
POST /eventspath still does the job, because that claim is load-bearing for the whole design.Gap 1 (the one that mattered). The prompt opened "generate a JSON lighting scheme for a holiday or observance event" with festive few-shot examples — asked for a memorial it would reliably have returned something bright and fast for someone's funeral. The system prompt now branches, and the examples are replaced rather than appended: showing both invites the model to average two incompatible briefs. Branching is also the right shape — filtering a celebratory scheme into a sombre one afterwards gives a muddy version of the wrong idea. The brief names what to avoid (strobes, chases, sparkles) rather than only asking for dignity, because those are what a model reaches for when it thinks "event".
There's a limit to what a test can assert about a prompt — it can check the model was asked the right question, not that it answered well. So the tests assert the brief, and Gap 2 is what guarantees a good result when nothing is asked at all. The branch was mutation-tested: removing it fails three tests.
Gap 2. A
memorialtag template — soft amber candlelight on a slow Breathe, brightness 120, eight-second transition. Without it a memorial with no AI provider fell through to solid white. Tested relative to the rest of the library (dimmer and slower than the patriotic template), which is the comparison that actually means "dignified".Gap 3. Annual recurrence is opt-in on a separate tag, and the Jan 1 job materialises the next occurrence reusing the same
event_key— which is what makes the anniversary reuse the approved scheme with no AI call. It rolls from the most recent occurrence, so an edit to the name carries forward instead of being reverted every January.A 29 February anniversary is skipped in non-leap years rather than moved. Someone who died on a leap day has an anniversary that genuinely only occurs every four years, and picking the 28th or 1 March on their behalf isn't this code's decision.
Gap 4. There was no event-create UI at all, so this is the first. Priority is on the form: a memorial on 23 December replaces Christmas Eve, which the issue calls the intended default — but a default that surprising belongs in front of someone when they choose it, not discovered in December. The field says so.
Also corrected:
builtin.yaml's header claimed65=Colorful, 66=Traffic. They're Palette and Fire 2012 (#132) — the same class of error, in the file most likely to be read by someone choosing an effect.One thing I got wrong: the first CI run failed on frontend lint. I'd been running bare
npx eslint .locally, which omits the--max-warnings 0thatnpm run lintapplies, so a warning passed my gate and failed CI's. The rule was right — exporting a function beside a component breaks fast refresh — and the fix isn't a suppression: the event key moved tolib/memorialKey.ts, where it belongs anyway, since scheme reuse is keyed on it.Verified: 34 new backend tests, 11 new frontend,
ruff/mypyclean, 845 backend tests, 132 frontend, all seven CI jobs green.Out of scope as the issue says: no external death feed, no automatic detection. Memorials are created by hand, on purpose.