Sync remaining local tracked changes

This commit is contained in:
Ivo Oskamp 2026-04-13 15:43:47 +02:00
parent d095a23944
commit 3cb608cb6b
4 changed files with 157 additions and 3 deletions

View File

@ -1 +1 @@
25
v20260402-01

View File

@ -9,7 +9,7 @@ from datetime import date, datetime, time, timedelta, timezone
from flask import flash, jsonify, redirect, render_template, request, url_for
from urllib.parse import urlencode, urljoin
from flask_login import current_user, login_required
from sqlalchemy import and_, or_, func, text
from sqlalchemy import and_, bindparam, or_, func, text
from .routes_shared import (
_apply_overrides_to_run,
@ -167,6 +167,118 @@ def _is_hidden_3cx_non_backup(backup_software: str | None, backup_type: str | No
return bs == "3cx" and bt in {"update", "ssl certificate"}
def _chunked(values: list[int], size: int = 500) -> list[list[int]]:
if not values:
return []
return [values[i:i + size] for i in range(0, len(values), size)]
def _get_cove_complete_success_run_ids(run_ids: list[int]) -> set[int]:
"""Return Cove run ids that have at least one object and all object statuses are Success."""
if not run_ids:
return set()
complete_success_ids: set[int] = set()
for chunk in _chunked(run_ids, size=500):
rows = db.session.execute(
text(
"""
SELECT
rol.run_id AS run_id,
COUNT(*) AS obj_count,
SUM(CASE WHEN LOWER(COALESCE(rol.status, '')) = 'success' THEN 1 ELSE 0 END) AS success_count
FROM run_object_links rol
WHERE rol.run_id IN :run_ids
GROUP BY rol.run_id
"""
).bindparams(bindparam("run_ids", expanding=True)),
{"run_ids": chunk},
).mappings().all()
for rr in rows:
run_id = int(rr.get("run_id") or 0)
obj_count = int(rr.get("obj_count") or 0)
success_count = int(rr.get("success_count") or 0)
if run_id > 0 and obj_count > 0 and success_count == obj_count:
complete_success_ids.add(run_id)
return complete_success_ids
def _collect_suppressed_cove_run_ids(
*,
job_ids: list[int] | None = None,
include_reviewed: bool = False,
) -> set[int]:
"""Suppress Cove runs that occur later on a day after the first complete success run."""
q = (
db.session.query(
JobRun.id.label("run_id"),
JobRun.job_id.label("job_id"),
func.coalesce(JobRun.run_at, JobRun.created_at).label("run_ts"),
JobRun.status.label("status"),
)
.filter(JobRun.source_type == "cove_api")
)
if job_ids:
q = q.filter(JobRun.job_id.in_(job_ids))
if not include_reviewed:
q = q.filter(JobRun.reviewed_at.is_(None))
run_rows = q.order_by(
JobRun.job_id.asc(),
func.coalesce(JobRun.run_at, JobRun.created_at).asc(),
JobRun.id.asc(),
).all()
if not run_rows:
return set()
success_candidate_ids = [
int(r.run_id)
for r in run_rows
if ((r.status or "").strip().lower() == "success")
]
complete_success_ids = _get_cove_complete_success_run_ids(success_candidate_ids)
if not complete_success_ids:
return set()
cutoff_by_job_day: dict[tuple[int, date], datetime] = {}
for r in run_rows:
run_id = int(r.run_id or 0)
run_ts = getattr(r, "run_ts", None)
if run_id <= 0 or run_ts is None or run_id not in complete_success_ids:
continue
local_day = _to_amsterdam_date(run_ts)
if local_day is None:
continue
key = (int(r.job_id), local_day)
prev_cutoff = cutoff_by_job_day.get(key)
if prev_cutoff is None or run_ts < prev_cutoff:
cutoff_by_job_day[key] = run_ts
if not cutoff_by_job_day:
return set()
suppressed: set[int] = set()
for r in run_rows:
run_id = int(r.run_id or 0)
run_ts = getattr(r, "run_ts", None)
if run_id <= 0 or run_ts is None:
continue
local_day = _to_amsterdam_date(run_ts)
if local_day is None:
continue
cutoff = cutoff_by_job_day.get((int(r.job_id), local_day))
if cutoff is None:
continue
if run_ts > cutoff:
suppressed.add(run_id)
return suppressed
def _ensure_internal_ticket_for_autotask(
*,
ticket_number: str,
@ -1337,6 +1449,13 @@ def run_checks_page():
| (func.coalesce(Job.job_name, "").ilike(pat, escape="\\"))
)
# Restrict Cove suppression calculation to currently relevant jobs.
candidate_job_ids = [int(x) for (x,) in base.with_entities(Job.id).limit(4000).all()]
suppressed_cove_run_ids = _collect_suppressed_cove_run_ids(
job_ids=candidate_job_ids,
include_reviewed=include_reviewed,
)
# Runs to show in the overview: unreviewed (or all if admin toggle enabled)
run_filter = []
if not include_reviewed:
@ -1388,6 +1507,8 @@ def run_checks_page():
)
if run_filter:
agg = agg.filter(*run_filter)
if suppressed_cove_run_ids:
agg = agg.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
agg = agg.subquery()
@ -1439,6 +1560,8 @@ def run_checks_page():
)
if run_filter:
s_q = s_q.filter(*run_filter)
if suppressed_cove_run_ids:
s_q = s_q.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
s_q = s_q.group_by(JobRun.job_id, JobRun.status, JobRun.missed, JobRun.override_applied)
for jid, status, missed, override_applied, cnt in s_q.all():
@ -1698,6 +1821,13 @@ def run_checks_details():
if not include_reviewed:
q = q.filter(JobRun.reviewed_at.is_(None))
suppressed_cove_run_ids = _collect_suppressed_cove_run_ids(
job_ids=[int(job.id)],
include_reviewed=include_reviewed,
)
if suppressed_cove_run_ids:
q = q.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
runs = q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all()
# Prefetch internal ticket resolution info for Autotask-linked runs (Phase 2 UI).

View File

@ -2,6 +2,17 @@
This file documents all changes made to this project via Claude Code.
## [2026-04-13]
### Fixed
- Run Checks now suppresses repeated Cove runs within the same local day once a **complete success run** has occurred:
- A complete success run is defined as `JobRun.status = Success` with at least one persisted run object and all object statuses equal to `Success`.
- For each Cove job/day, the first complete success run is treated as the cutoff; newer runs on that same day are hidden from Run Checks (both overview aggregation and modal details), regardless of whether they are `Success`, `Warning`, or `Failed/Error`.
- Sorting remains unchanged (`newest -> oldest`).
### Validation
- Test build executed with `./build-and-push.sh t` on 2026-04-13 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:520778f4b72643c1cd1815fa424317ee2dce182ccfcbea687f4ac711b3d00fb0`).
## [2026-04-02]
### Added

View File

@ -331,6 +331,11 @@ Cove run rows in the job detail history table are clickable even without a mail
- `routes_run_checks.py` returns `cove_summary` in the run payload for `source_type="cove_api"` runs
- Includes: account_name, computer_name, customer_name, readable datasource labels, last_run_at, status
- `run_checks.html` shows the Cove summary panel and hides the mail section
- Duplicate-day suppression for Cove runs:
- Runs are grouped per job per local day (Europe/Amsterdam date derived from run timestamp).
- A run is considered a "complete success" when `JobRun.status == Success` and persisted run objects exist with all object statuses equal to `Success`.
- Once the first complete success exists on that day, all newer Cove runs for the same day are hidden in Run Checks (overview aggregation + details modal), regardless of status (`Success`, `Warning`, `Failed/Error`).
- Sort order in the modal remains unchanged (`newest -> oldest`).
### Migrations
- `migrate_cove_integration()` — adds 8 columns to `system_settings`, `cove_account_id` to `jobs`, `source_type` + `external_id` to `job_runs`, dedup index on `job_runs.external_id`
@ -730,6 +735,15 @@ File: `build-and-push.sh`
## Recent Changes
### 2026-04-13
- **Run Checks Cove daily suppression** (`main/routes_run_checks.py`):
- Added Cove-specific filtering to suppress repeated same-day runs after the first complete success run.
- Complete success criteria: run status `Success`, object set present, all object statuses `Success`.
- Applied consistently to both Run Checks overview aggregation and details modal query.
- Local-day grouping uses the existing Amsterdam date helper for run timestamps.
- **Validation**:
- Test build executed with `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:520778f4b72643c1cd1815fa424317ee2dce182ccfcbea687f4ac711b3d00fb0`.
### 2026-03-23
- **Synology ABB parser fix** (`parsers/synology.py`): ABB completion regex now also matches `has been completed` phrasing.
- **Job name parsing corrected for ABB mails**: messages like `backup task dc001 on DS220p has been completed` no longer fall back to generic Synology Active Backup parsing; `job_name` stays `dc001` instead of bracketed subject prefix values.
@ -819,4 +833,3 @@ File: `build-and-push.sh`
### 2026-02-10
- **Added screenshot support to Feedback system**: Multiple file upload, inline display, two-stage delete (soft delete for audit trail, permanent delete for cleanup).
- **Completed transition to link-based ticket system**: All pages now use JOIN queries, no date-based logic. Added cross-browser copy ticket functionality with three-tier fallback mechanism to both Run Checks and Job Details pages.