backupchecks/TODO-smart-overrides.md
Ivo Oskamp f21d6f4fca Release v0.3.0
Features:
- Smart Overrides Phase 1: create overrides directly from Run Checks via
  the "Apply override for future runs?" follow-up dialog after Mark as
  Success (scope + duration choices, audit-logged).
- Cove workstation offline handling: skip schedule-based missed-runs for
  Cove workstations (always on) and add an optional colorbar-based
  offline-detection toggle in Settings -> Integrations -> Cove
  (cove_offline_detection_enabled, cove_workstation_warning_days,
  cove_workstation_error_days). Synthetic offline runs use a stable
  external_id so they escalate in place and clear once activity resumes.
- Settings -> Maintenance: Generate test run card for exercising the
  Smart Override flow.
- Restored Mark as Success button in the Run Checks modal footer.

Changes:
- Run Checks Cove same-day suppression: hide repeat Cove runs after the
  first complete success run on the same local day.
- Inbox excludes mail messages linked to archived jobs.
- Run Checks / Search overview now applies Customer.active filter.
- In-app documentation refreshed across getting-started, users,
  mail-import, integrations (Cove), settings, backup-review,
  customers-jobs and autotask sections.

Tooling:
- Adopted the shared docker-build-and-push script. Modes are now t / r;
  release version is read from docs/changelog.md; the script no longer
  performs git operations. Removed obsolete version.txt and .last-branch.

Renames:
- docs/technical-notes-codex.md -> docs/TECHNICAL.md
- docs/changelog-claude.md -> docs/changelog-develop.md

Migrations:
- migrate_cove_offline_detection (3 columns on system_settings).
2026-05-01 11:04:35 +02:00

16 KiB
Raw Permalink Blame History

Smart Overrides — Technisch Ontwerp

Backupchecks leert van operator-acties om herhaalwerk te verminderen. Gefaseerde aanpak: directe UI-verbetering → patroonherkenning → cross-job kennisbank.


Fase 1: "Wil je dit ook in de toekomst?"

Probleem

De huidige "Mark as Success" knop in Run Checks maakt een override aan met een tijdvenster van ±1 minuut rond de specifieke run. Dezelfde fout morgen? Opnieuw handmatig markeren.

Oplossing

Na "Mark as Success" toont de UI een vervolgdialoog waarin de operator de scope en duur kan bepalen. De backend accepteert deze extra parameters en maakt een bredere override aan.

UI-wijziging: vervolgdialoog

Na een succesvolle mark-success-override API-call verschijnt een modal (of inline panel in de bestaande modal) met:

Scope-keuze (radio buttons):

  • "Alleen deze run" ← huidige gedrag, default
  • "Deze job, zelfde foutmelding" → object-level override op job_id + match_error_contains
  • "Alle jobs met deze software/type en zelfde foutmelding" → global override op backup_software + backup_type + match_error_contains

Duur-keuze (radio buttons):

  • "Eenmalig" ← huidige gedrag, ±1 minuut window
  • "1 week"
  • "1 maand"
  • "Permanent (tot handmatig uitgeschakeld)"

Optioneel commentaar (textarea, vooringevuld met de error-tekst als referentie)

API-wijziging

POST /api/run-checks/mark-success-override krijgt optionele extra velden:

{
    "run_id": 123,
    "scope": "job" | "global" | "run",       # default: "run" (huidig gedrag)
    "duration": "once" | "1w" | "1m" | "permanent",  # default: "once"
    "comment": "VSS snapshot timeout, known issue"
}

De backend-logica in api_run_checks_mark_success_override() verandert:

  • scope="run" → huidige gedrag (±1 min window)
  • scope="job"Override(level="object", job_id=job.id, match_error_contains=..., start_at=now, end_at=now+duration)
  • scope="global"Override(level="global", backup_software=..., backup_type=..., match_error_contains=..., start_at=now, end_at=now+duration)

Bij duration="permanent" wordt end_at=None gezet.

Error-tekst extractie

De vervolgdialoog moet de error-tekst tonen die als match_error_contains gebruikt wordt. Dit is al beschikbaar in de run-detail modal. De logica:

  1. Haal run_object_links op voor de run (bestaande query in _apply_overrides_to_run)
  2. Filter op objecten met niet-success status
  3. Neem de error_message van het eerste problematische object
  4. Fallback naar run.remark

Deze tekst wordt getoond in de dialoog zodat de operator kan zien wat er geleerd wordt, en eventueel kan aanpassen.

Database-wijzigingen

Geen. Het bestaande Override-model ondersteunt alle benodigde velden al (level, match_error_contains, match_error_mode, start_at, end_at, comment).

Nieuwe audit logging

Bij het aanmaken van een bredere override: log naar AuditLog met event_type="override_from_review" en details over scope, duur, en de oorspronkelijke run.


Fase 2: Patroonherkenning op reviews

Probleem

Fase 1 vereist nog steeds een bewuste keuze van de operator. Als dezelfde fout 5× achter elkaar wordt gereviewed zonder ticket of opmerking, had het systeem dat al eerder moeten signaleren.

Oplossing

Een achtergrondproces analyseert review-events en detecteert herhaalde patronen. Bij een drempel verschijnt een suggestie in de UI.

Nieuw model: OverrideSuggestion

class OverrideSuggestion(db.Model):
    __tablename__ = "override_suggestions"

    id = db.Column(db.Integer, primary_key=True)

    # Fingerprint van het patroon (hash van genormaliseerde velden)
    pattern_fingerprint = db.Column(db.String(64), nullable=False, index=True)

    # Leesbare omschrijving van het patroon
    pattern_description = db.Column(db.Text, nullable=False)

    # Scope van de suggestie
    suggested_level = db.Column(db.String(20), nullable=False)  # global | object
    suggested_backup_software = db.Column(db.String(255), nullable=True)
    suggested_backup_type = db.Column(db.String(255), nullable=True)
    suggested_job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"), nullable=True)
    suggested_match_status = db.Column(db.String(32), nullable=True)
    suggested_match_error = db.Column(db.String(255), nullable=True)

    # Bewijs
    match_count = db.Column(db.Integer, nullable=False, default=0)
    sample_run_ids = db.Column(db.Text, nullable=True)  # JSON list
    first_seen_at = db.Column(db.DateTime, nullable=False)
    last_seen_at = db.Column(db.DateTime, nullable=False)

    # Status
    status = db.Column(db.String(20), nullable=False, default="pending")
    # pending | accepted | dismissed | expired
    resolved_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    resolved_at = db.Column(db.DateTime, nullable=True)
    created_override_id = db.Column(db.Integer, db.ForeignKey("overrides.id"), nullable=True)

    created_at = db.Column(db.DateTime, nullable=False)
    updated_at = db.Column(db.DateTime, nullable=False)

Patroondetectie-logica

Trigger: Na elke mark-reviewed actie (of als periodieke task, bijv. elk uur).

Algoritme:

1. Query alle JobRuns die in de laatste 30 dagen reviewed zijn
   ZONDER dat er een ticket aan gelinkt is
   EN ZONDER dat er een override actief was

2. Groepeer op:
   - (backup_software, backup_type, status, genormaliseerde_error)    → global kandidaat
   - (job_id, status, genormaliseerde_error)                          → object kandidaat

3. Voor elke groep met count >= DREMPEL (configureerbaar, default 3):
   - Bereken fingerprint: sha256(level + scope-velden + genormaliseerde_error)
   - Check of er al een OverrideSuggestion bestaat met deze fingerprint
     → Ja: update match_count en last_seen_at
     → Nee: maak nieuwe suggestie aan

4. Suggesties ouder dan 90 dagen zonder nieuwe matches: status → expired

Error-normalisatie (voor groepering):

def normalize_error_for_grouping(error: str) -> str:
    """Verwijder variabele delen uit error-berichten zodat
    dezelfde fout met verschillende details als één patroon herkend wordt."""
    import re
    s = (error or "").strip()
    # Verwijder timestamps (diverse formaten)
    s = re.sub(r'\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}(:\d{2})?', '<TIMESTAMP>', s)
    # Verwijder GUIDs
    s = re.sub(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '<GUID>', s)
    # Verwijder IP-adressen
    s = re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '<IP>', s)
    # Verwijder paden (Windows en Unix)
    s = re.sub(r'[A-Z]:\\[\w\\.-]+', '<PATH>', s)
    s = re.sub(r'/[\w/.-]+', '<PATH>', s)
    # Verwijder grote getallen (bijv. bytes, sizes)
    s = re.sub(r'\b\d{4,}\b', '<NUM>', s)
    # Normaliseer whitespace
    s = re.sub(r'\s+', ' ', s).strip()
    return s

UI: suggesties tonen

Optie A — Dashboard badge: Een badge naast "Override Suggestions" in de sidebar of op het dashboard (zoals inbox_count). Operator klikt door naar een lijst.

Optie B — Inline in Run Checks: Wanneer een operator een run opent die past bij een bestaand patroon, toon een banner: "Dit patroon is al 5× handmatig gereviewed. Wil je een override aanmaken?" met knoppen "Ja, maak override" en "Negeer".

Aanbeveling: beide. De dashboard-badge geeft een overzicht, de inline-banner maakt het actionable op het moment dat het relevant is.

Suggesties-pagina (/override-suggestions):

  • Lijst van pending suggesties, gesorteerd op match_count (hoogste eerst)
  • Per suggestie: patroon-omschrijving, aantal matches, sample runs (klikbaar), voorgestelde override-config
  • Acties: "Accepteer" (maakt Override aan met voorgestelde config), "Negeer" (dismissed), "Aanpassen" (opent override-aanmaakformulier met vooringevulde velden)

Configuratie

Nieuwe velden op SystemSettings:

# Override Suggestions
suggestion_enabled = db.Column(db.Boolean, nullable=False, default=True)
suggestion_threshold = db.Column(db.Integer, nullable=False, default=3)
suggestion_lookback_days = db.Column(db.Integer, nullable=False, default=30)
suggestion_expiry_days = db.Column(db.Integer, nullable=False, default=90)

Fase 3: Error-fingerprint kennisbank

Probleem

Fase 2 werkt per installatie. Als klant A dezelfde Veeam VSS-fout heeft als klant B, moet de operator het bij beide apart afhandelen.

Oplossing

Bouw een kennisbank van genormaliseerde error-fingerprints met hun classificatie. Nieuwe runs worden automatisch gematcht tegen de kennisbank.

Nieuw model: ErrorPattern

class ErrorPattern(db.Model):
    __tablename__ = "error_patterns"

    id = db.Column(db.Integer, primary_key=True)

    # Genormaliseerde fingerprint (output van normalize_error_for_grouping + sha256)
    fingerprint = db.Column(db.String(64), unique=True, nullable=False, index=True)

    # Leesbaar voorbeeld van de originele error
    example_error = db.Column(db.Text, nullable=False)

    # Genormaliseerde versie (voor display)
    normalized_error = db.Column(db.Text, nullable=False)

    # Classificatie
    classification = db.Column(db.String(20), nullable=False, default="unknown")
    # benign | actionable | critical | unknown

    # Automatische actie
    auto_action = db.Column(db.String(20), nullable=False, default="none")
    # none | treat_as_success | needs_review | escalate

    # Scope-beperking (optioneel: alleen voor specifieke software/type)
    scope_backup_software = db.Column(db.String(255), nullable=True)
    scope_backup_type = db.Column(db.String(255), nullable=True)

    # Herkomst
    learned_from = db.Column(db.String(32), nullable=False, default="manual")
    # manual | override | suggestion
    source_override_id = db.Column(db.Integer, nullable=True)

    # Statistieken
    times_matched = db.Column(db.Integer, nullable=False, default=0)
    times_confirmed = db.Column(db.Integer, nullable=False, default=0)  # keer dat operator OK gaf
    times_rejected = db.Column(db.Integer, nullable=False, default=0)   # keer dat operator NIET OK gaf
    confidence = db.Column(db.Float, nullable=False, default=0.0)

    # Beheer
    active = db.Column(db.Boolean, nullable=False, default=True)
    created_by = db.Column(db.String(255), nullable=True)
    notes = db.Column(db.Text, nullable=True)

    created_at = db.Column(db.DateTime, nullable=False)
    updated_at = db.Column(db.DateTime, nullable=False)

Automatisch leren van overrides

Wanneer een Override wordt aangemaakt (via Fase 1-dialoog, Fase 2-suggestie, of handmatig):

def learn_from_override(override: Override):
    """Extraheer een ErrorPattern uit een nieuwe override."""
    if not override.match_error_contains:
        return  # Geen error-tekst om van te leren

    normalized = normalize_error_for_grouping(override.match_error_contains)
    fp = hashlib.sha256(normalized.encode()).hexdigest()[:16]

    existing = ErrorPattern.query.filter_by(fingerprint=fp).first()
    if existing:
        existing.times_confirmed += 1
        existing.confidence = min(1.0, existing.times_confirmed / (existing.times_confirmed + existing.times_rejected + 1))
        existing.updated_at = datetime.utcnow()
        return

    pattern = ErrorPattern(
        fingerprint=fp,
        example_error=override.match_error_contains,
        normalized_error=normalized,
        classification="benign",
        auto_action="treat_as_success" if override.treat_as_success else "none",
        scope_backup_software=override.backup_software,
        scope_backup_type=override.backup_type,
        learned_from="override",
        source_override_id=override.id,
        times_matched=0,
        times_confirmed=1,
        confidence=0.5,  # startwaarde bij eerste bevestiging
        active=True,
        created_by=override.created_by,
        created_at=datetime.utcnow(),
        updated_at=datetime.utcnow(),
    )
    db.session.add(pattern)

Integratie in de run-interpretatie

De fingerprint-check wordt een extra laag in _apply_overrides_to_run(), ná de bestaande override-evaluatie:

# Na de bestaande override checks (regel ~502-516 in routes_shared.py):

# Fase 3: Check error pattern kennisbank
if not override_applied:
    error_text = _get_run_error_text(run, run_object_rows)
    if error_text:
        normalized = normalize_error_for_grouping(error_text)
        fp = hashlib.sha256(normalized.encode()).hexdigest()[:16]
        pattern = ErrorPattern.query.filter_by(
            fingerprint=fp, active=True
        ).first()

        if pattern and pattern.confidence >= CONFIDENCE_THRESHOLD:
            # Check scope-beperking
            if pattern.scope_backup_software and pattern.scope_backup_software.lower() != (job.backup_software or "").lower():
                pattern = None
            if pattern and pattern.scope_backup_type and pattern.scope_backup_type.lower() != (job.backup_type or "").lower():
                pattern = None

        if pattern:
            pattern.times_matched += 1
            if pattern.auto_action == "treat_as_success":
                return "Success (auto)", True, "pattern", pattern.id, f"ErrorPattern id={pattern.id}"

CONFIDENCE_THRESHOLD (configureerbaar, default 0.7): voorkomt dat patronen met weinig bewijs automatisch worden toegepast. Een patroon begint op 0.5 en stijgt door bevestigingen.

Feedback-loop: confidence bijwerken

Wanneer een operator een run reviewed die door een ErrorPattern als success was gemarkeerd:

  • Reviewed zonder correctiepattern.times_confirmed += 1
  • Operator maakt alsnog een ticket aanpattern.times_rejected += 1
  • Herbereken: confidence = confirmed / (confirmed + rejected + 1)
  • Bij confidence < 0.3: automatisch active = False zetten

UI: kennisbank-pagina

/error-patterns (admin/operator):

  • Tabel met alle patronen, sorteerbaar op confidence, times_matched, classificatie
  • Per patroon: genormaliseerde error, voorbeeld, classificatie, auto_action, confidence-balk, match-statistieken
  • Inline edit voor classificatie en auto_action
  • Bulk-acties: activeren/deactiveren
  • Filter op software/type

In de run-detail modal: als een ErrorPattern is toegepast, toon een klein label "Auto-classified: benign (confidence 85%)" met link naar het patroon.


Implementatievolgorde en afhankelijkheden

Fase 1 (directe waarde, ~2-3 dagen werk)
├── Backend: scope + duration parameters toevoegen aan mark-success-override
├── Frontend: vervolgdialoog in run_checks.html modal
├── Audit logging voor bredere overrides
└── Geen nieuwe modellen of migraties nodig

Fase 2 (patroonherkenning, ~1-2 weken werk)
├── Migratie: override_suggestions tabel
├── Migratie: SystemSettings velden voor configuratie
├── Backend: patroondetectie-logica + normalize_error_for_grouping()
├── Backend: trigger na mark-reviewed OF periodieke task
├── Frontend: suggesties-pagina (/override-suggestions)
├── Frontend: dashboard badge + inline banner in Run Checks
└── Afhankelijk van: Fase 1 (optioneel, maar geeft betere data)

Fase 3 (kennisbank, ~2-3 weken werk)
├── Migratie: error_patterns tabel
├── Backend: learn_from_override() hook
├── Backend: fingerprint-check in _apply_overrides_to_run()
├── Backend: confidence feedback-loop bij review-acties
├── Frontend: /error-patterns beheerpagina
├── Frontend: "Auto-classified" label in run-detail modal
└── Afhankelijk van: Fase 2 (voor normalize_error_for_grouping())

Risico's en mitigatie

False positives (patroon matcht verkeerd) Mitigatie: confidence-drempel, scope-beperkingen, en de mogelijkheid om patronen te deactiveren. Fase 3-patronen met auto_action=treat_as_success worden in de UI duidelijk gemarkeerd als "Auto" zodat operators ze kunnen herkennen en corrigeren.

Error-normalisatie te agressief Mitigatie: begin conservatief (alleen timestamps, GUIDs, IPs verwijderen). Breid de normalisatie geleidelijk uit op basis van ervaring. De kennisbank-pagina toont zowel de genormaliseerde als de originele tekst zodat operators kunnen beoordelen of de normalisatie klopt.

Performance bij grote datasets Mitigatie: pattern_fingerprint en fingerprint zijn geïndexeerd. De patroondetectie-query in Fase 2 draait periodiek (niet real-time). De fingerprint-lookup in Fase 3 is een simpele indexed lookup per run.

Operator-vertrouwen Mitigatie: het systeem suggereert en labelt, maar de operator heeft altijd het laatste woord. "Success (auto)" is visueel onderscheidbaar van "Success" en "Success (override)". Elke automatische actie is traceerbaar naar het bronpatroon.