{"record":{"id":"fd7453d95a04dc6b","repo":"tinyhumansai/openhuman","slug":"unsupported-delay-unit-unit-use-s-m-h-d","errorCode":null,"errorMessage":"unsupported delay unit '{unit}', use s/m/h/d","messagePattern":"unsupported delay unit '(.+?)', use s/m/h/d","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/cron/ops.rs","lineNumber":147,"sourceCode":"/// Parse a human-friendly delay string (e.g. \"5m\", \"2h\", \"30s\") into a\n/// `chrono::Duration`. Defaults to minutes when no unit is given.\npub fn parse_human_delay(input: &str) -> Result<chrono::Duration> {\n    let input = input.trim();\n    if input.is_empty() {\n        anyhow::bail!(\"delay must not be empty\");\n    }\n    let split = input\n        .find(|c: char| !c.is_ascii_digit())\n        .unwrap_or(input.len());\n    let (num, unit) = input.split_at(split);\n    let amount: i64 = num.parse()?;\n    let unit = if unit.is_empty() { \"m\" } else { unit };\n    let duration = match unit {\n        \"s\" => chrono::Duration::seconds(amount),\n        \"m\" => chrono::Duration::minutes(amount),\n        \"h\" => chrono::Duration::hours(amount),\n        \"d\" => chrono::Duration::days(amount),\n        _ => anyhow::bail!(\"unsupported delay unit '{unit}', use s/m/h/d\"),\n    };\n    Ok(duration)\n}\n\npub async fn cron_list(config: &Config) -> Result<RpcOutcome<Vec<CronJob>>, String> {\n    if !config.cron.enabled {\n        return Err(\"cron is disabled by config (cron.enabled=false)\".to_string());\n    }\n    let jobs = cron::list_jobs(config).map_err(|e| e.to_string())?;\n    Ok(RpcOutcome::single_log(jobs, \"cron jobs listed\"))\n}\n\npub async fn cron_update(\n    config: &Config,\n    job_id: &str,\n    patch: CronJobPatch,\n) -> Result<RpcOutcome<CronJob>, String> {\n    if job_id.trim().is_empty() {","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/cron/ops.rs#L129-L165","documentation":"parse_human_delay accepts exactly the units s, m, h and d (seconds/minutes/hours/days), defaulting to minutes when no suffix is given. Any other suffix — ms, w, min, sec — bails with this message. Decimals also fail: the split takes the leading digit run, so '1.5h' leaves unit '.5h'.","triggerScenarios":"Passing 500ms (milliseconds are unsupported — whole-second granularity), 2w, 10min, 1.5h, 90sec, or wrong-case suffixes like 5M; any free-text delay field that assumes Go/ISO-8601 duration syntax.","commonSituations":"Assuming PT30M / 30min / 1h30m style durations are accepted; converting timeouts from millisecond-based tools without rescaling; UI free-text inputs for delays.","solutions":["Rewrite the value with a supported unit: 500ms → 1s, 2w → 14d, 10min → 10m","Compound values are not supported: 1h30m → 90m","A bare number means minutes — append s/h/d explicitly when you mean anything else"],"exampleFix":"# before\n--delay 500ms\n\n# after\n--delay 1s","handlingStrategy":"validation","validationCode":"fn valid_delay(s: &str) -> bool {\n    let t = s.trim();\n    let split = t.find(|c: char| !c.is_ascii_digit()).unwrap_or(t.len());\n    let (num, unit) = t.split_at(split);\n    !num.is_empty()\n        && matches!(if unit.is_empty() { \"m\" } else { unit }, \"s\" | \"m\" | \"h\" | \"d\")\n}","typeGuard":"const DELAY_RE = /^(\\d+)(s|m|h|d)?$/;\nfunction isValidDelay(input: string): boolean {\n  return DELAY_RE.test(input.trim());\n}","tryCatchPattern":null,"preventionTips":["Offer a units dropdown (s/m/h/d) in UIs instead of free text","Rescale millisecond values to whole seconds before sending","Remember: no compound or decimal durations — pre-compute a single integer plus unit"],"tags":["cron","delay","units","parsing"],"backgroundTag":"invalid-duration-format","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}