feat: Laientauglichkeit — Auto-Modus, Klartext-Ausgabe, einfachere Doku

Ziel: ein einziger täglicher Befehl genügt, jede Meldung ist auch ohne
IT-Wissen verständlich.

Auto-Modus (ein Cron-Job genügt für alles):
- data/state.json merkt sich, wann die Wochen-Prüfungen zuletzt liefen
  (baseline.py: load_state/save_state/is_check_due/mark_check_run)
- `scan` führt fällige Zusatzprüfungen (Tarnung/externe Links/Dateien)
  automatisch mit aus; Gesamt-Ampel = schlechtestes Teilergebnis;
  EIN kombinierter Report + EIN Alert
- neuer Config-Abschnitt periodic_checks (Default: wöchentlich, an)

Klartext (scanner/plain.py):
- Ampel 🟢/🟡/🔴 + Sätze ohne Fachbegriffe, aus den strukturierten Befunden
- Report: Klartext oben, "Technische Details (für Ihren Dienstleister)" unten
- Terminal-Ausgabe und E-Mail (alerter.py) ebenso umgestellt

Komfort:
- approve --all/--rebuild legt die Datei-Überwachung automatisch mit an
  (Opt-out: --skip-assets); approve-assets bleibt für den Sonderfall
- status zeigt Ampel zuoberst + wann die Wochen-Prüfungen zuletzt liefen
- Onboarding-Text in einfacher Sprache, wenn noch kein Vergleichsstand existiert

Doku: README + BEDIENUNGSANLEITUNG mit "In 3 Schritten"-Einstieg.

Tests: test_state.py, test_plain.py (Fachbegriff-Assertion), Auto-Asset-
Baseline-Test. 165 Tests grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-12 11:14:05 +02:00
commit bed20db8bd
11 changed files with 856 additions and 202 deletions

View file

@ -233,6 +233,50 @@ class BaselineManager:
)
logger.info("Asset hashes saved: %d entries", len(hashes))
# ------------------------------------------------------------------
# Run-state (drives auto-scheduling of the weekly checks)
# ------------------------------------------------------------------
@property
def _state_path(self) -> Path:
return self.data_dir / "state.json"
def load_state(self) -> dict:
"""Load the run-state file. Returns {'last_runs': {}} if none exists."""
if not self._state_path.exists():
return {"last_runs": {}}
try:
data = json.loads(self._state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, ValueError):
return {"last_runs": {}}
data.setdefault("last_runs", {})
return data
def save_state(self, state: dict) -> None:
"""Persist the run-state file."""
self.data_dir.mkdir(parents=True, exist_ok=True)
self._state_path.write_text(
json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8"
)
def is_check_due(self, name: str, interval_days: int) -> bool:
"""True if a periodic check has never run or last ran >= interval_days ago."""
last = self.load_state().get("last_runs", {}).get(name)
if not last:
return True
try:
last_dt = datetime.fromisoformat(last)
except ValueError:
return True
age = datetime.now(timezone.utc) - last_dt
return age.total_seconds() >= interval_days * 86400
def mark_check_run(self, name: str) -> None:
"""Record that a periodic check just ran (now, UTC)."""
state = self.load_state()
state["last_runs"][name] = datetime.now(timezone.utc).isoformat()
self.save_state(state)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------