task_templates.lead_days (default 0): scheduler now spawns the task
when (next_due - lead_days) <= today instead of next_due <= today, but
due_date on the created task is still the real next_due — so overdue
and priority handling are unaffected, the task just becomes visible
earlier for prep/booking. Small jobs (AC filter clean) can stay at 0;
bigger ones needing advance notice (septic tank empty-out, contractor
booking) can set 2-3 days or 30 days as needed.
- db.js: lead_days INT NOT NULL DEFAULT 0 (inline for fresh installs,
ALTER for existing).
- scheduler.js: WHERE (next_due - lead_days) <= $1 — next_due itself
and the fixed/floating rollover logic are untouched by this.
- templates.js: accept + validate lead_days (>= 0) on create/update.
- Recurring.tsx: lead time field in the template form with contextual
hint text, plus a small 'generates Nd early' note on list rows that
have one set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was no way to remove a template once created (e.g. test/scratch
recurring tasks) - only edit/pause via active. DELETE /api/templates/:id
detaches any tasks already spawned from it (template_id cleared) rather
than blocking on the FK or cascading, so existing task history survives
as ordinary one-off tasks while the recurring schedule itself is removed.
Delete button in the edit modal footer, only shown when editing an
existing template - not on new/duplicate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
duplicate() blanked next_due along with location/asset, but title/
description/notes/etc. all stayed filled in — so a duplicated
template looked fully populated while due date was actually empty,
and the combined 'Title and first due date are required' error didn't
say which field was missing. Adding another room to an existing
schedule normally wants the same due date as the rest of the group
anyway, so carry it over from the source template instead of clearing
it; only location/asset genuinely need re-picking. Error messages now
name the specific missing field.
For recurring jobs that repeat identically across many locations (e.g.
clean AC filter in every room), one task_templates row per location is
still required — schedule/completion tracking is per-location, which
matters especially for floating mode — but creating and organising
that many rows by hand didn't scale.
- New template_groups table (id, unique name) + task_templates.group_id.
POST /api/template-groups upserts by name so picking an existing
group from the UI never creates a duplicate folder.
- Recurring template form: optional group name (datalist-autocompleted
from existing groups) plus a "create for multiple locations" toggle
that reveals a checkbox list of locations; saving loops one
createTemplate() call per selected location, all sharing the
resolved group_id.
- Duplicate button on each template row pre-fills the New Template
form from that row (location/asset/next-due cleared for repicking),
landing in the same group automatically — the lightweight path for
adding one more room later without the bulk picker.
- Recurring.tsx list now groups rows under a collapsible folder header
per group_name; ungrouped templates render as flat rows as before.
Fixed mode keeps the existing behavior: next_due always advances from
the previous scheduled date, regardless of when a task is actually
completed. Floating mode instead computes the next occurrence from
the completion date of the current task, so early/late completion
shifts the whole schedule.
- task_templates gains schedule_mode ('fixed'|'floating', default
'fixed') and pending_task_id, tracking the open task spawned from a
floating template so the hourly sweep doesn't respawn duplicates
before it's resolved.
- scheduler.js: floating templates set pending_task_id instead of
advancing next_due at spawn time.
- tasks.js /resolve: floating templates recompute next_due from
completed_at once the task is marked fixed (not temporary_fix,
since that isn't really done yet); pending_task_id is always
cleared on fixed resolution so a stale flag can't wedge the
template if the mode changed mid-cycle.
- addInterval/isoDate pulled out of scheduler.js into a shared
lib/recurrence.js used by both the scheduler and the resolve route.
- Recurring.tsx: schedule mode toggle in the template form, and the
list shows "awaiting completion" instead of a stale next-due date
for floating templates with an open occurrence.