From 56415eae594094dd45d95ce1a3316dbf71a91600 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Tue, 13 Jan 2026 14:12:59 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-13 14:12:58) --- .last-branch | 2 +- .../src/backend/app/main/routes_overrides.py | 32 ++++++++++++- .../src/backend/app/main/routes_run_checks.py | 2 + .../src/backend/app/main/routes_shared.py | 47 ++++++++++++++++--- .../src/backend/app/migrations.py | 21 ++++++++- .../backupchecks/src/backend/app/models.py | 2 + .../src/templates/main/overrides.html | 14 +++++- docs/changelog.md | 8 ++++ 8 files changed, 118 insertions(+), 10 deletions(-) diff --git a/.last-branch b/.last-branch index ac23253..8b870e4 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260113-05-reporter-menu-restrict +v20260113-06-overrides-error-match-modes diff --git a/containers/backupchecks/src/backend/app/main/routes_overrides.py b/containers/backupchecks/src/backend/app/main/routes_overrides.py index e783d14..ab488d4 100644 --- a/containers/backupchecks/src/backend/app/main/routes_overrides.py +++ b/containers/backupchecks/src/backend/app/main/routes_overrides.py @@ -75,7 +75,16 @@ def overrides(): if ov.match_status: crit.append(f"status == {ov.match_status}") if ov.match_error_contains: - crit.append(f"error contains '{ov.match_error_contains}'") + mode = (getattr(ov, "match_error_mode", None) or "contains").strip().lower() + if mode == "exact": + label = "error exact" + elif mode == "starts_with": + label = "error starts with" + elif mode == "ends_with": + label = "error ends with" + else: + label = "error contains" + crit.append(f"{label} '{ov.match_error_contains}'") if crit: scope = scope + " [" + ", ".join(crit) + "]" @@ -95,6 +104,13 @@ def overrides(): "comment": ov.comment or "", "match_status": ov.match_status or "", "match_error_contains": ov.match_error_contains or "", + "match_error_mode": getattr(ov, "match_error_mode", None) or "", + "backup_software": ov.backup_software or "", + "backup_type": ov.backup_type or "", + "job_id": ov.job_id or "", + "object_name": ov.object_name or "", + "start_at_raw": (ov.start_at.strftime("%Y-%m-%dT%H:%M") if ov.start_at else ""), + "end_at_raw": (ov.end_at.strftime("%Y-%m-%dT%H:%M") if ov.end_at else ""), } ) @@ -126,6 +142,12 @@ def overrides_create(): match_status = (request.form.get("match_status") or "").strip() or None match_error_contains = (request.form.get("match_error_contains") or "").strip() or None + match_error_mode = (request.form.get("match_error_mode") or "").strip().lower() or None + if match_error_contains: + if match_error_mode not in ("contains", "exact", "starts_with", "ends_with"): + match_error_mode = "contains" + else: + match_error_mode = None start_at_str = request.form.get("start_at") or "" end_at_str = request.form.get("end_at") or "" @@ -159,6 +181,7 @@ def overrides_create(): object_name=object_name if level == "object" else None, match_status=match_status, match_error_contains=match_error_contains, + match_error_mode=match_error_mode, treat_as_success=treat_as_success, active=True, comment=comment, @@ -218,6 +241,12 @@ def overrides_update(override_id: int): match_status = (request.form.get("match_status") or "").strip() or None match_error_contains = (request.form.get("match_error_contains") or "").strip() or None + match_error_mode = (request.form.get("match_error_mode") or "").strip().lower() or None + if match_error_contains: + if match_error_mode not in ("contains", "exact", "starts_with", "ends_with"): + match_error_mode = "contains" + else: + match_error_mode = None start_at_str = request.form.get("start_at") or "" end_at_str = request.form.get("end_at") or "" @@ -252,6 +281,7 @@ def overrides_update(override_id: int): ov.object_name = object_name if level == "object" else None ov.match_status = match_status ov.match_error_contains = match_error_contains + ov.match_error_mode = match_error_mode ov.treat_as_success = treat_as_success ov.comment = comment ov.start_at = start_at diff --git a/containers/backupchecks/src/backend/app/main/routes_run_checks.py b/containers/backupchecks/src/backend/app/main/routes_run_checks.py index e369186..b073af2 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -968,6 +968,7 @@ def api_run_checks_mark_success_override(): object_name=obj_name, match_status=(obj_status or None), match_error_contains=(err[:255] if err else None), + match_error_mode=("contains" if err else None), treat_as_success=True, active=True, comment=comment, @@ -999,6 +1000,7 @@ def api_run_checks_mark_success_override(): backup_type=job.backup_type or None, match_status=(getattr(run, "status", None) or None), match_error_contains=(match_error_contains[:255] if match_error_contains else None), + match_error_mode=("contains" if match_error_contains else None), treat_as_success=True, active=True, comment=comment, diff --git a/containers/backupchecks/src/backend/app/main/routes_shared.py b/containers/backupchecks/src/backend/app/main/routes_shared.py index 5789981..4975c4b 100644 --- a/containers/backupchecks/src/backend/app/main/routes_shared.py +++ b/containers/backupchecks/src/backend/app/main/routes_shared.py @@ -293,7 +293,8 @@ def _apply_overrides_to_run(job: Job, run: JobRun): try: mec = (getattr(ov, "match_error_contains", None) or "").strip() if mec: - parts.append(f"contains={mec}") + mem = (getattr(ov, "match_error_mode", None) or "contains").strip() + parts.append(f"error_{mem}={mec}") except Exception: pass try: @@ -342,6 +343,40 @@ def _apply_overrides_to_run(job: Job, run: JobRun): return False return needle.lower() in haystack.lower() + def _matches_error_text(haystack: str | None, needle: str | None, mode: str | None) -> bool: + """Match error text using a configured mode. + + Modes: + - contains (default) + - exact + - starts_with + - ends_with + + Matching is case-insensitive and trims surrounding whitespace. + """ + if not needle: + return True + if not haystack: + return False + + hs = (haystack or "").strip() + nd = (needle or "").strip() + if not hs: + return False + + hs_l = hs.lower() + nd_l = nd.lower() + m = (mode or "contains").strip().lower() + + if m == "exact": + return hs_l == nd_l + if m in ("starts_with", "startswith", "start"): + return hs_l.startswith(nd_l) + if m in ("ends_with", "endswith", "end"): + return hs_l.endswith(nd_l) + # Default/fallback + return nd_l in hs_l + def _matches_status(candidate: str | None, expected: str | None) -> bool: if not expected: return True @@ -410,12 +445,12 @@ def _apply_overrides_to_run(job: Job, run: JobRun): # Global overrides should match both the run-level remark and any object-level error messages. if ov.match_error_contains: - if _contains(run.remark, ov.match_error_contains): + if _matches_error_text(run.remark, ov.match_error_contains, getattr(ov, "match_error_mode", None)): return True # Check persisted run-object error messages. for row in run_object_rows or []: - if _contains(row.get("error_message"), ov.match_error_contains): + if _matches_error_text(row.get("error_message"), ov.match_error_contains, getattr(ov, "match_error_mode", None)): return True objs = [] @@ -424,7 +459,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun): except Exception: objs = [] for obj in objs or []: - if _contains(getattr(obj, "error_message", None), ov.match_error_contains): + if _matches_error_text(getattr(obj, "error_message", None), ov.match_error_contains, getattr(ov, "match_error_mode", None)): return True return False @@ -438,7 +473,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun): continue if not _matches_status(row.get("status"), ov.match_status): continue - if not _contains(row.get("error_message"), ov.match_error_contains): + if not _matches_error_text(row.get("error_message"), ov.match_error_contains, getattr(ov, "match_error_mode", None)): continue return True @@ -453,7 +488,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun): continue if not _matches_status(getattr(obj, "status", None), ov.match_status): continue - if not _contains(getattr(obj, "error_message", None), ov.match_error_contains): + if not _matches_error_text(getattr(obj, "error_message", None), ov.match_error_contains, getattr(ov, "match_error_mode", None)): continue return True diff --git a/containers/backupchecks/src/backend/app/migrations.py b/containers/backupchecks/src/backend/app/migrations.py index 3387793..334be39 100644 --- a/containers/backupchecks/src/backend/app/migrations.py +++ b/containers/backupchecks/src/backend/app/migrations.py @@ -378,7 +378,7 @@ def migrate_remarks_active_from_date() -> None: def migrate_overrides_match_columns() -> None: - """Add match_status and match_error_contains columns to overrides table if missing.""" + """Add match_status / match_error columns to overrides table if missing.""" engine = db.get_engine() inspector = inspect(engine) try: @@ -397,6 +397,25 @@ def migrate_overrides_match_columns() -> None: print("[migrations] Adding overrides.match_error_contains column...") conn.execute(text('ALTER TABLE "overrides" ADD COLUMN match_error_contains VARCHAR(255)')) + if "match_error_mode" not in existing_columns: + print("[migrations] Adding overrides.match_error_mode column...") + conn.execute(text('ALTER TABLE "overrides" ADD COLUMN match_error_mode VARCHAR(20)')) + + # Backfill mode for existing overrides that already have a match string. + try: + conn.execute( + text( + """ + UPDATE "overrides" + SET match_error_mode = 'contains' + WHERE (match_error_mode IS NULL OR match_error_mode = '') + AND (match_error_contains IS NOT NULL AND match_error_contains <> ''); + """ + ) + ) + except Exception: + pass + print("[migrations] migrate_overrides_match_columns completed.") diff --git a/containers/backupchecks/src/backend/app/models.py b/containers/backupchecks/src/backend/app/models.py index bdcfa50..3d23da6 100644 --- a/containers/backupchecks/src/backend/app/models.py +++ b/containers/backupchecks/src/backend/app/models.py @@ -156,6 +156,8 @@ class Override(db.Model): # Matching criteria on object status / error message match_status = db.Column(db.String(32), nullable=True) match_error_contains = db.Column(db.String(255), nullable=True) + # Matching mode for error text: contains (default), exact, starts_with, ends_with + match_error_mode = db.Column(db.String(20), nullable=True) # Behaviour flags treat_as_success = db.Column(db.Boolean, nullable=False, default=True) diff --git a/containers/backupchecks/src/templates/main/overrides.html b/containers/backupchecks/src/templates/main/overrides.html index 0af9056..ff33b77 100644 --- a/containers/backupchecks/src/templates/main/overrides.html +++ b/containers/backupchecks/src/templates/main/overrides.html @@ -62,7 +62,16 @@
- + + +
+
+
@@ -142,6 +151,7 @@ data-ov-object-name="{{ ov.object_name or '' }}" data-ov-match-status="{{ ov.match_status or '' }}" data-ov-match-error-contains="{{ ov.match_error_contains or '' }}" + data-ov-match-error-mode="{{ ov.match_error_mode or 'contains' }}" data-ov-treat-as-success="{{ 1 if ov.treat_as_success else 0 }}" data-ov-comment="{{ ov.comment or '' }}" data-ov-start-at="{{ ov.start_at_raw or '' }}" @@ -190,6 +200,7 @@ const jobField = document.getElementById('ov_job_id'); const objectNameField = document.getElementById('ov_object_name'); const matchStatusField = document.getElementById('ov_match_status'); + const matchErrorModeField = document.getElementById('ov_match_error_mode'); const matchErrorContainsField = document.getElementById('ov_match_error_contains'); const treatAsSuccessField = document.getElementById('ov_treat_success'); const commentField = document.getElementById('ov_comment'); @@ -228,6 +239,7 @@ setValue(jobField, btn.dataset.ovJobId || ''); setValue(objectNameField, btn.dataset.ovObjectName || ''); setValue(matchStatusField, btn.dataset.ovMatchStatus || ''); + setValue(matchErrorModeField, btn.dataset.ovMatchErrorMode || 'contains'); setValue(matchErrorContainsField, btn.dataset.ovMatchErrorContains || ''); if (treatAsSuccessField) treatAsSuccessField.checked = (btn.dataset.ovTreatAsSuccess === '1'); setValue(commentField, btn.dataset.ovComment || ''); diff --git a/docs/changelog.md b/docs/changelog.md index 7512e1a..59b241d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -35,6 +35,14 @@ - Updated menu rendering to hide all unauthorized menu items for Reporter users. - Adjusted route access to ensure Feedback pages are accessible for the Reporter role. +--- + +## v20260113-06-overrides-error-match-modes +- Added configurable error text matching modes for overrides: contains, exact, starts with, ends with +- Updated override evaluation logic to apply the selected match mode across run remarks and object error messages +- Extended overrides UI with a match type selector and improved edit support for existing overrides +- Added database migration to create and backfill overrides.match_error_mode for existing records + *** ## v0.1.20 -- 2.45.2