Skip to content

feat: Ignite v3 Plan 2 — scheduling - #6

Merged
malinfossum merged 7 commits into
mainfrom
feat/ignite-v3-scheduling
Aug 13, 2026
Merged

feat: Ignite v3 Plan 2 — scheduling#6
malinfossum merged 7 commits into
mainfrom
feat/ignite-v3-scheduling

Conversation

@malinfossum

Copy link
Copy Markdown
Owner

Implements decision D8 and all of §6 of the v3 spec: any task can now carry a real due date and, optionally, a time of day.

Plan: docs/superpowers/plans/2026-08-13-ignite-v3-scheduling.md (stress-tested before execution; five findings folded in).

What changed

hasTime on the model Rides the existing BOOL_FIELDS 0/1 conversion. No DB version bump, no migration — absent fields read as false.
Untimed sorts last New sortByDueThenUntimed in utils/time.js: day ascending, then timed-before-untimed within each day, then clock time, then createdAt.
Rows respect it task.js picks formatTimeLabel when timed, formatOccurrenceLabel when not.
The dialog schedules Date and time at the top, repeat demoted beneath, plus a "Does not repeat" cadence — which is what finally lets a one-off task carry a date.
Menu copy Repeat…Schedule… on both surfaces.

A one-off task could not previously be given a date at all. buildRule() always returned a rule, so every save created a repeat. That is the part of §6 that is easy to miss — it appears there as a finding rather than a requirement.

Verification

221 tests / 15 files, Biome clean (57 files), build clean.

Browser-verified against ignite-dev, all eleven dialog invariants re-checked after the restructure rather than assumed:

  • Save gating (empty date, weekly with no chips, interval 0), interval clamping on blur not mid-type, the date remaining the single source for monthly day, chips remaining the source for weekly, backdrop-click closing only from the backdrop, Escape returning focus to .task__menu-btn — no focus reaching <body> on any path.
  • Title escaping holds: renaming a task to <img src=x onerror=…> renders as literal text in the heading, with no injected element and the payload never firing.
  • The RangeError path is closed — 99:99 and free text both disable Save rather than throwing out of the handler and leaving the background inert.
  • Ordering with real data: 09:00, 10:00, untimed last.
  • Time-of-day survives recurrence: completing a weekly occurrence at 14:45 advanced dueAt exactly 7 days to 14:45 local, with hasTime intact and completed still 0.
  • New time input: labelled, 46px tall, colours identical to the existing .repeat-input so no new contrast surface.

Not done, deliberately

  • No axe run. axe-core is not installed and a11y automation was previously discarded on purpose, so I did not add a package or inject a remote script for it. Targeted manual checks are above; a full axe pass on .repeat-panel is still owed.
  • pickNextTask treats an untimed task as already overdue (its stored midnight is behind now), so it is never the "Next" highlight. Pre-existing and invisible while every dated task was midnight. Plan 3 replaces the function, so fixing it now means deriving its semantics twice.
  • Internal repeat-* naming is unchanged — only user-visible copy says "Schedule".

New invariants are recorded in project memory.

Six tasks implementing D8 and spec section 6: a hasTime model field, an
untimed-sorts-last pure seam, row labels that respect it, and the
recurrence dialog becoming a schedule dialog with an optional repeat.

Stress-tested before committing; five findings folded in:

- sortByDueThenUntimed compares the calendar day FIRST. Section 6.2 scopes
  the untimed-last rule to a single day, but the sort also runs over the
  overdue bucket, which spans weeks — without it, an untimed task from
  three weeks ago sorts below this morning's 09:00.
- parseDate/parseTime close a RangeError path. buildDueAt runs as an
  argument to onSave, so a throw fires before closeRecurrenceEditor clears
  inert, wedging the app behind an open dialog.
- Open focus moves to the date field. The old selector took the first
  cadence radio whatever was checked, parking a weekly task on "Does not
  repeat" and skipping the two fields above it.
- A local [hidden] rule so the dialog does not depend on a design-system
  rule for correctness. Both containers set display, which beats the UA
  [hidden]; the reset only started covering this in 2.0.1 today.
- The createdAt tie-break is documented as safe only because the value is
  always a UTC ISO string.

Security and privacy passes were clean.
Day ascending first, then timed-before-untimed within each day, then
clock time, then createdAt. The day comparison matters because this sorts
the overdue bucket as well as today's, and overdue spans weeks — without
it an untimed task from three weeks ago lands below this morning's 09:00.

Also updates the existing "sorts today by dueAt ascending" fixture to set
hasTime on both dated tasks. Sorting by clock time is only meaningful for
tasks that carry one; without the flag those fixtures are untimed and
correctly sort by createdAt instead. The test's intent was always two
timed tasks — the fixture just could not say so before now.
@malinfossum
malinfossum merged commit 829cff1 into main Aug 13, 2026
1 check passed
@malinfossum
malinfossum deleted the feat/ignite-v3-scheduling branch August 13, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant