Auto-commit local changes before build (2026-03-20 14:53:36)

This commit is contained in:
Ivo Oskamp 2026-03-20 14:53:36 +01:00
parent 75e0678410
commit c5a6759112
5 changed files with 67 additions and 11 deletions

View File

@ -1 +1 @@
v20260320-02-cove-historical-runs-and-detail 25

View File

@ -659,7 +659,9 @@ def _infer_schedule_map_from_runs(job_id: int):
- Synthetic missed rows never influence schedule inference. - Synthetic missed rows never influence schedule inference.
- To reduce noise, a weekday/time bucket must occur at least MIN_OCCURRENCES times. - To reduce noise, a weekday/time bucket must occur at least MIN_OCCURRENCES times.
""" """
MIN_OCCURRENCES = 3 # Higher threshold reduces false positives from short-lived patterns
# (e.g. a time-of-day shift that briefly leaves two active slots).
MIN_OCCURRENCES = 5
schedule = {i: [] for i in range(7)} # 0=Mon .. 6=Sun schedule = {i: [] for i in range(7)} # 0=Mon .. 6=Sun
# Certain job types are informational and should never participate in schedule # Certain job types are informational and should never participate in schedule
@ -691,6 +693,10 @@ def _infer_schedule_map_from_runs(job_id: int):
try: try:
# Only infer schedules from real runs that came from mail reports. # Only infer schedules from real runs that came from mail reports.
# Synthetic "Missed" rows must never influence schedule inference. # Synthetic "Missed" rows must never influence schedule inference.
# Limit to the last 90 days so that schedule changes (different day,
# time, or frequency) take effect quickly and do not leave stale slots
# generating false missed runs.
cutoff_utc = datetime.utcnow() - timedelta(days=90)
runs = ( runs = (
JobRun.query JobRun.query
.filter( .filter(
@ -698,6 +704,7 @@ def _infer_schedule_map_from_runs(job_id: int):
JobRun.run_at.isnot(None), JobRun.run_at.isnot(None),
JobRun.missed.is_(False), JobRun.missed.is_(False),
JobRun.mail_message_id.isnot(None), JobRun.mail_message_id.isnot(None),
JobRun.run_at >= cutoff_utc,
) )
.order_by(JobRun.run_at.desc()) .order_by(JobRun.run_at.desc())
.limit(500) .limit(500)
@ -716,6 +723,7 @@ def _infer_schedule_map_from_runs(job_id: int):
tz = None tz = None
counts = {i: {} for i in range(7)} # weekday -> { "HH:MM": count } counts = {i: {} for i in range(7)} # weekday -> { "HH:MM": count }
run_dts = [] # Collected for cadence guard below
for r in runs: for r in runs:
if not r.run_at: if not r.run_at:
continue continue
@ -730,12 +738,32 @@ def _infer_schedule_map_from_runs(job_id: int):
except Exception: except Exception:
pass pass
run_dts.append(dt)
wd = dt.weekday() wd = dt.weekday()
minute_bucket = (dt.minute // 15) * 15 minute_bucket = (dt.minute // 15) * 15
hh = dt.hour hh = dt.hour
tstr = f"{hh:02d}:{minute_bucket:02d}" tstr = f"{hh:02d}:{minute_bucket:02d}"
counts[wd][tstr] = int(counts[wd].get(tstr, 0)) + 1 counts[wd][tstr] = int(counts[wd].get(tstr, 0)) + 1
# Cadence guard: if the median gap between runs is >= 20 days the job has a
# monthly (or lower) cadence. Return an empty weekly schedule so that
# _infer_monthly_schedule_from_runs() handles it instead.
if len(run_dts) >= 2:
sorted_dts = sorted(run_dts)
gaps = []
for i in range(1, len(sorted_dts)):
try:
delta_days = (sorted_dts[i] - sorted_dts[i - 1]).total_seconds() / 86400.0
if delta_days > 0:
gaps.append(delta_days)
except Exception:
continue
if gaps:
gaps_sorted = sorted(gaps)
median_gap = gaps_sorted[len(gaps_sorted) // 2]
if median_gap >= 20.0:
return schedule # empty — defer to monthly inference
for wd in range(7): for wd in range(7):
# Keep only buckets that occur frequently enough. # Keep only buckets that occur frequently enough.
keep = [t for t, c in counts[wd].items() if int(c) >= MIN_OCCURRENCES] keep = [t for t, c in counts[wd].items() if int(c) >= MIN_OCCURRENCES]
@ -762,6 +790,9 @@ def _infer_monthly_schedule_from_runs(job_id: int):
try: try:
# Same "real run" rule as weekly inference. # Same "real run" rule as weekly inference.
# 180 days gives ~6 occurrences for a monthly job (enough for
# MIN_OCCURRENCES=3) while still discarding stale schedule data.
cutoff_utc = datetime.utcnow() - timedelta(days=180)
runs = ( runs = (
JobRun.query JobRun.query
.filter( .filter(
@ -769,6 +800,7 @@ def _infer_monthly_schedule_from_runs(job_id: int):
JobRun.run_at.isnot(None), JobRun.run_at.isnot(None),
JobRun.missed.is_(False), JobRun.missed.is_(False),
JobRun.mail_message_id.isnot(None), JobRun.mail_message_id.isnot(None),
JobRun.run_at >= cutoff_utc,
) )
.order_by(JobRun.run_at.asc()) .order_by(JobRun.run_at.asc())
.limit(500) .limit(500)

View File

@ -645,8 +645,8 @@ function renderObjects(objects) {
function objectSeverityRank(o) { function objectSeverityRank(o) {
var st = String((o && o.status) || "").trim().toLowerCase(); var st = String((o && o.status) || "").trim().toLowerCase();
var err = String((o && o.error_message) || "").trim(); var err = String((o && o.error_message) || "").trim();
if (st === "error" || st === "failed" || st === "failure" || err) return 0; if (st === "error" || st === "failed" || st === "failure") return 0;
if (st === "warning") return 1; if (st === "warning" || err) return 1;
return 2; return 2;
} }

View File

@ -208,17 +208,21 @@
overflow: auto; overflow: auto;
} }
#runChecksModal #rcm_body_iframe {
flex: 1 1 auto;
min-height: 0;
height: auto;
}
#runChecksModal .rcm-mail-panel { #runChecksModal .rcm-mail-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
} }
#runChecksModal #rcm_mail_iframe_body {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
#runChecksModal #rcm_body_iframe {
height: 100%;
display: block;
}
#runChecksModal .rcm-objects-scroll { #runChecksModal .rcm-objects-scroll {
max-height: 25vh; max-height: 25vh;
overflow: auto; overflow: auto;
@ -493,8 +497,8 @@ function statusClass(status) {
function objectSeverityRank(o) { function objectSeverityRank(o) {
var st = String((o && o.status) || '').trim().toLowerCase(); var st = String((o && o.status) || '').trim().toLowerCase();
var err = String((o && o.error_message) || '').trim(); var err = String((o && o.error_message) || '').trim();
if (st === 'error' || st === 'failed' || st === 'failure' || err) return 0; if (st === 'error' || st === 'failed' || st === 'failure') return 0;
if (st === 'warning') return 1; if (st === 'warning' || err) return 1;
return 2; return 2;
} }

View File

@ -2,6 +2,26 @@
This file documents all changes made to this project via Claude Code. This file documents all changes made to this project via Claude Code.
## [2026-03-20] (6)
### Fixed
- Run Checks and Job Detail modals — objects list sorting:
- `objectSeverityRank`: Warning items with an `error_message` (e.g. "Processing mailbox: MT completed with warning: Cannot process") were incorrectly ranked as Critical (rank 0) due to `|| err` on the rank-0 check; they are now correctly ranked as Warning (rank 1); only status `error`/`failed`/`failure` triggers rank 0
- Success objects that do have an `error_message` are still promoted to Warning rank (rank 1) to keep them visible
- Run Checks modal — mail iframe no longer collapses to near-zero height:
- `#rcm_mail_iframe_body` was missing flex rules so the `flex: 1 1 auto` on `#rcm_body_iframe` had no effect (the iframe is not a direct flex child of `.rcm-mail-panel`)
- Fixed: `#rcm_mail_iframe_body` now gets `flex: 1 1 auto; min-height: 0; overflow: hidden` so it fills the available panel space; `#rcm_body_iframe` gets `height: 100%; display: block`
## [2026-03-20] (5)
### Fixed
- Missed run detection: false positives caused by stale schedule data:
- **Time-of-day changes**: old time slot stayed active until 500 historical runs were "used up"; now weekly inference only looks at the last 90 days, so a changed run time no longer generates missed runs on the old slot after 90 days
- **Frequency changes** (e.g. daily → weekly): same 90-day window ensures old patterns stop influencing inference within 3 months
- **Monthly jobs falsely detected as weekly**: after ~21 months a monthly job at a fixed time accumulated 3+ hits per weekday, triggering daily missed runs; fixed by a cadence guard — if the median gap between runs ≥ 20 days, weekly inference is skipped and monthly inference handles the job instead
- **Monthly inference**: limited to the last 180 days so schedule changes are forgotten within 6 months while still providing enough data (≥ 3 occurrences) for detection
- `MIN_OCCURRENCES` for weekly inference raised from 3 → 5 to reduce false positives from transitional patterns (two overlapping slots during a time shift)
## [2026-03-20] (4) ## [2026-03-20] (4)
### Added ### Added