# 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: ```python { "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` ```python 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): ```python 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})?', '', 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}', '', s) # Verwijder IP-adressen s = re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '', s) # Verwijder paden (Windows en Unix) s = re.sub(r'[A-Z]:\\[\w\\.-]+', '', s) s = re.sub(r'/[\w/.-]+', '', s) # Verwijder grote getallen (bijv. bytes, sizes) s = re.sub(r'\b\d{4,}\b', '', 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`: ```python # 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` ```python 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): ```python 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: ```python # 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 correctie** → `pattern.times_confirmed += 1` - **Operator maakt alsnog een ticket aan** → `pattern.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.