JQC WEB — phase44: scheduled inspection End Date ================================================ Repo: lt_janitorial_quality_control Deploy root: /home/jqc/janitorial_qc/ WEB ONLY. No iOS changes, no iPad rebuild. NEW FILE migrations/versions/phase44_scheduled_end_date.py revision = 'phase44_sched_end_date' down_revision = 'phase43_sched_recurrence' <- verified current HEAD OVERWRITE app/models/scheduled_inspection.py end_date column; is_within_end_date(); is_expired; expire_if_past_end_date(); fulfill() deactivates past the boundary app/utils/forms.py end_date DateField + validate() rules app/routes/scheduled_inspections.py _apply_recurrence() sets/clears end_date; _reject_if_past_end_date() guard; per-context next_due_date label; run_reminders() expiry sweep; audit detail app/api/scheduled.py 'end_date' in _scheduled_payload app/templates/scheduled_inspections/form.html End Date field + JS toggle app/templates/scheduled_inspections/list.html "Ends" column + "Ended" badge CLAUDE.md DEPLOY (migration step first, then code) cd /home/jqc/janitorial_qc git pull # 1. MIGRATION source venv/bin/activate flask db current # expect phase43_sched_recurrence flask db upgrade flask db current # expect phase44_sched_end_date # 2. CODE sudo systemctl restart janitorial_qc sudo systemctl status janitorial_qc --no-pager ROLLBACK flask db downgrade phase43_sched_recurrence # drops end_date, nothing else VERIFY 1. New Schedule -> frequency "One-time": End Date row is HIDDEN, date field reads "Start Date" 2. Switch frequency to Weekly: End Date row appears 3. Edit an existing schedule: date field reads "Next Due Date" 4. Validation: - end date before the due date -> rejected - end date on a one-time schedule (via curl/devtools) -> rejected - Mon/Wed/Fri, pick a Tuesday, end date that same Tuesday -> rejected, message names the Wednesday 5. List: "Ends" column shows the date, "No end" when blank, "—" for one-time 6. Existing schedules: unchanged, "No end", still Active, cadence identical 7. Boundary: set end date = next due date, complete the inspection -> schedule goes Inactive, badge reads "Ended" 8. Cron sweep: curl -X POST "https://jqc.ltservicesinc.com/scheduled-inspections/run?token=$DIGEST_SECRET" -> JSON now includes "expired": N -> a schedule past its end date that was never completed goes Inactive and stops generating overdue alerts NOTES - Migration follows the phase42/phase43 idiom in this repo (op.get_bind() + sa.text() + INFORMATION_SCHEMA). Flag if you want the stricter plain-string-only form instead; 62 existing migrations use this one. - api/scheduled.py now returns "end_date". Additive and safe: the iPad decodes explicit CodingKeys, so current builds ignore the new key.