Memorial events: commemorative lighting for people who have died #80

Closed
opened 2026-08-31 02:48:26 +00:00 by claude-bot · 1 comment
Contributor

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 carries name, description, tags, start/end, priority, duration_class, phase_config
  • EventCreate (schemas/events.py:94) already defaults type="user_defined", priority=100
  • Spec §2.4 puts user-defined at priority 100, "always wins"
  • prompt_builder receives the whole Event, so name, description, and tags already reach the model

So today, with no code changes, POST /events with 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._SYSTEM opens with "generate a JSON lighting scheme for a holiday or observance event", and _few_shot_examples() pulls festive entries from builtin.yaml. Asked for a memorial, it will produce celebratory output — high brightness, fast effects.

  • Branch the system prompt for memorial events: restrained palette, low global_brightness, slow//gentle effect_id and effect_speed, long transition_ms
  • Use memorial-appropriate few-shot examples rather than the first three event_keys in the file

Gap 2 — no non-AI fallback

AI_PROVIDER is optional and capped by AI_DAILY_LIMIT. Events without AI are covered by tag templates in schemes/builtin.yaml, and there is no memorial tag — so with AI unconfigured this feature produces nothing appropriate.

  • Add a memorial tag template to schemes/builtin.yaml: dignified default (soft amber/white, slow breathe), works with no AI configured

Gap 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:

  • Mark the event as annually recurring (flag or reserved tag)
  • Have the Jan 1 yearly_job materialise next year's occurrence for annual memorials, reusing the same event_key

Worth 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

  • A "Memorial" quick-create: person's name, date, optional notes, optional "remember annually", and a visible priority field (see below)
  • A button to request an AI person-specific palette, distinct from the generic default

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 memorial tag 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

  • Any external death-notification feed or automatic detection. Memorials are created deliberately, by hand.
  • Changes to the §2.4 priority table itself.

Acceptance

  • Creating a memorial for a person and date produces a dignified scheme with no AI configured
  • With AI configured, requesting a person-specific palette returns a memorial-appropriate candidate — subdued, not festive — that requires approval
  • An annual memorial reappears on its anniversary and reuses its approved scheme without an AI call
  • A memorial's priority is visible and editable at create time
  • Docs note the feature and the Dec-23-style collision behaviour
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 carries `name`, `description`, `tags`, `start`/`end`, `priority`, `duration_class`, `phase_config` - `EventCreate` (`schemas/events.py:94`) already defaults `type="user_defined"`, `priority=100` - Spec §2.4 puts user-defined at priority 100, "always wins" - `prompt_builder` receives the whole `Event`, so `name`, `description`, and `tags` already reach the model So today, with no code changes, `POST /events` with 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._SYSTEM` opens with *"generate a JSON lighting scheme for a holiday or observance event"*, and `_few_shot_examples()` pulls festive entries from `builtin.yaml`. Asked for a memorial, it will produce celebratory output — high brightness, fast effects. - [ ] Branch the system prompt for memorial events: restrained palette, low `global_brightness`, slow//gentle `effect_id` and `effect_speed`, long `transition_ms` - [ ] Use memorial-appropriate few-shot examples rather than the first three `event_keys` in the file ## Gap 2 — no non-AI fallback `AI_PROVIDER` is optional and capped by `AI_DAILY_LIMIT`. Events without AI are covered by tag templates in `schemes/builtin.yaml`, and there is no `memorial` tag — so with AI unconfigured this feature produces nothing appropriate. - [ ] Add a `memorial` tag template to `schemes/builtin.yaml`: dignified default (soft amber/white, slow breathe), works with no AI configured ## Gap 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: - [ ] Mark the event as annually recurring (flag or reserved tag) - [ ] Have the Jan 1 `yearly_job` materialise next year's occurrence for annual memorials, reusing the same `event_key` Worth 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 - [ ] A "Memorial" quick-create: person's name, date, optional notes, optional "remember annually", and a visible priority field (see below) - [ ] A button to request an AI person-specific palette, distinct from the generic default ## 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 `memorial` tag 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 - Any external death-notification feed or automatic detection. Memorials are created deliberately, by hand. - Changes to the §2.4 priority table itself. ## Acceptance - [ ] Creating a memorial for a person and date produces a dignified scheme with no AI configured - [ ] With AI configured, requesting a person-specific palette returns a memorial-appropriate candidate — subdued, not festive — that requires approval - [ ] An annual memorial reappears on its anniversary and reuses its approved scheme without an AI call - [ ] A memorial's priority is visible and editable at create time - [ ] Docs note the feature and the Dec-23-style collision behaviour
claude-bot added this to the v1.1.0 milestone 2026-08-31 02:48:34 +00:00
Author
Contributor

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 /events path 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 memorial tag 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 claimed 65=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 0 that npm run lint applies, 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 to lib/memorialKey.ts, where it belongs anyway, since scheme reuse is keyed on it.

Verified: 34 new backend tests, 11 new frontend, ruff/mypy clean, 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.

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 /events` path 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 `memorial` tag 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 claimed `65=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 0` that `npm run lint` applies, 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 to `lib/memorialKey.ts`, where it belongs anyway, since scheme reuse is keyed on it. Verified: 34 new backend tests, 11 new frontend, `ruff`/`mypy` clean, 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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#80
No description provided.