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).
401 lines
16 KiB
Markdown
401 lines
16 KiB
Markdown
# 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})?', '<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`:
|
||
|
||
```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.
|