{"record":{"id":"c4e9a954b1343c13","repo":"Hmbown/CodeWhale","slug":"unable-to-compute-next-weekly-run","errorCode":null,"errorMessage":"Unable to compute next WEEKLY run","messagePattern":"Unable to compute next WEEKLY run","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":522,"sourceCode":"                byday,\n                byhour,\n                byminute,\n            } => {\n                for day_offset in 0..15 {\n                    let date = local_after.date_naive() + Duration::days(i64::from(day_offset));\n                    if !byday.contains(&date.weekday()) {\n                        continue;\n                    }\n                    let Some(candidate_naive) = date.and_hms_opt(*byhour, *byminute, 0) else {\n                        continue;\n                    };\n                    if let Some(candidate) = resolve_local_datetime(timezone, candidate_naive)\n                        && candidate.with_timezone(&Utc) > after\n                    {\n                        return Ok(candidate.with_timezone(&Utc));\n                    }\n                }\n                bail!(\"Unable to compute next WEEKLY run\");\n            }\n            Self::Cron { expr } => {\n                let cron = ParsedCronExpr::parse(expr)?;\n                let mut candidate_naive = local_after\n                    .naive_local()\n                    .with_second(0)\n                    .and_then(|dt| dt.with_nanosecond(0))\n                    .ok_or_else(|| anyhow::anyhow!(\"Unable to round CRON search start\"))?\n                    .checked_add_signed(Duration::minutes(1))\n                    .ok_or_else(|| anyhow::anyhow!(\"CRON schedule exceeded its range\"))?;\n\n                for _ in 0..MAX_CRON_SEARCH_MINUTES {\n                    if cron.matches(candidate_naive)\n                        && let Some(candidate) = resolve_local_datetime(timezone, candidate_naive)\n                    {\n                        let candidate = candidate.with_timezone(&Utc);\n                        if candidate > after {\n                            return Ok(candidate);","sourceCodeStart":504,"sourceCodeEnd":540,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/automation_manager.rs#L504-L540","documentation":"Thrown by the WEEKLY branch of next_after_in_timezone after scanning 15 consecutive days (day_offset 0..15) without finding a BYDAY/BYHOUR/BYMINUTE wall time that resolves to a real local instant strictly after 'after'. Any non-empty BYDAY recurs within 7 days, so through parse_rrule (which enforces non-empty WEEKLY BYDAY) this is a defensive exhaustion guard; it becomes reachable only when every matching wall time in the window fails to resolve in local time (DST spring-forward gap) or the schedule was constructed directly with an empty byday vec.","triggerScenarios":"Constructing AutomationSchedule::Weekly directly with byday: vec![] instead of going through parse_rrule; or a timezone where resolve_local_datetime (from_local_datetime(...).earliest()) returns None for the BYHOUR/BYMINUTE on every listed weekday inside the 15-day window (FREQ=WEEKLY;BYDAY=SU;BYHOUR=2;BYMINUTE=30 only skirts this on the single spring-forward Sunday, since the following Sunday resolves).","commonSituations":"Programmatic construction of the schedule enum rather than parse_rrule; unit tests building Weekly literals with no days; sandboxed environments with exotic timezone data.","solutions":["If constructing Weekly directly, reject an empty byday before computing the next run","Route schedule creation through parse_rrule so WEEKLY BYDAY non-emptiness is enforced","Shift BYHOUR/BYMINUTE away from the local DST gap hour if the error appears seasonally","If reached via parse_rrule, report it as a bug with the timezone and rrule"],"exampleFix":"// before (direct construction with no days)\nlet schedule = AutomationSchedule::Weekly { byday: vec![], byhour: 9, byminute: 0 };\n\n// after (validate before use)\nif byday.is_empty() {\n    anyhow::bail!(\"WEEKLY schedule needs at least one BYDAY day\");\n}","handlingStrategy":"validation","validationCode":"fn weekly_schedule_reachable(rrule: &str) -> Result<bool, anyhow::Error> {\n    if let AutomationSchedule::Weekly { byday, byhour, byminute } =\n        AutomationSchedule::parse_rrule(rrule)?\n    {\n        if byday.is_empty() {\n            return Ok(false);\n        }\n        let any_monday = chrono::NaiveDate::from_ymd_opt(2025, 1, 6).unwrap()\n            .and_hms_opt(byhour, byminute, 0).unwrap();\n        if chrono::Local.from_local_datetime(&any_monday).earliest().is_none() {\n            return Ok(false); // wall time falls in a DST gap\n        }\n    }\n    Ok(true)\n}","typeGuard":null,"tryCatchPattern":"match manager.update_automation(id, req) {\n    Ok(record) => { /* ... */ }\n    Err(e) if e.to_string().contains(\"next WEEKLY run\") => {\n        tracing::warn!(%e, \"unreachable WEEKLY automation {id}; check BYDAY/time\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Create schedules through parse_rrule rather than enum literals so BYDAY non-emptiness is enforced","Reject empty BYDAY at the UI or tool boundary","Keep automation wall times away from 02:00-03:00 local to dodge DST gaps","Capture the timezone in reports when this fires so maintainers can reproduce"],"tags":["rust","rrule","weekly","schedule","dst","automation"],"backgroundTag":"recurrence-next-occurrence-not-found","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}