From 3948d98273d2940314df81a2d63a27af06a7bdfe Mon Sep 17 00:00:00 2001 From: Guido Date: Fri, 21 Aug 2026 14:33:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Diagnose-Skript=20f=C3=BCr=20fehlende?= =?UTF-8?q?=20Schlagw=C3=B6rter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Kategorie-Liste zeigt nur manuelle, einer bestehenden Kategorie zugeordnete Regeln. Bleibt sie leer, obwohl Regeln in der Datenbank stehen, sagt das Skript warum: Es schlüsselt den Bestand nach source, kind und is_transfer auf, zählt Regeln ohne Kategorie sowie solche, deren Kategorie nicht mehr existiert, und nennt die Zahl der tatsächlich sichtbaren. Co-Authored-By: Claude Opus 4.8 --- backend/scripts/pruefe_schlagwoerter.py | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100755 backend/scripts/pruefe_schlagwoerter.py diff --git a/backend/scripts/pruefe_schlagwoerter.py b/backend/scripts/pruefe_schlagwoerter.py new file mode 100755 index 0000000..e4686a7 --- /dev/null +++ b/backend/scripts/pruefe_schlagwoerter.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Zeigt, warum Schlagwörter in der Oberfläche fehlen könnten. + +Die Liste einer Kategorie zeigt nur Regeln mit ``source='manual'``, die einer +Kategorie zugeordnet und keine Transfer-Regel sind. Automatisch gelernte Regeln +tauchen dort bewusst nicht auf. Dieses Skript schlüsselt den Bestand danach auf. + +Aufruf: pruefe_schlagwoerter.py [pfad/zur/app.db] +""" +import sqlite3 +import sys +from collections import Counter + +DB = sys.argv[1] if len(sys.argv) > 1 else "/var/lib/certain-finance/app.db" +con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True) +con.row_factory = sqlite3.Row + + +def spalten(tabelle: str) -> set[str]: + return {r["name"] for r in con.execute(f"PRAGMA table_info({tabelle})")} + + +vorhanden = spalten("categorization_rules") +print(f"Datenbank: {DB}") +print(f"Spalten in categorization_rules: {', '.join(sorted(vorhanden))}\n") + +gesamt = con.execute("SELECT COUNT(*) FROM categorization_rules").fetchone()[0] +print(f"Regeln insgesamt: {gesamt}") + +for feld in ("source", "kind", "is_transfer", "match_field"): + if feld not in vorhanden: + print(f" {feld:<12} – Spalte fehlt") + continue + verteilung = Counter( + r[0] for r in con.execute(f"SELECT {feld} FROM categorization_rules") + ) + print(f" {feld:<12} {dict(verteilung)}") + +ohne_kat = con.execute( + "SELECT COUNT(*) FROM categorization_rules WHERE category_id IS NULL" +).fetchone()[0] +verwaist = con.execute( + "SELECT COUNT(*) FROM categorization_rules r WHERE r.category_id IS NOT NULL" + " AND NOT EXISTS (SELECT 1 FROM categories c WHERE c.id = r.category_id)" +).fetchone()[0] +print(f" ohne Kategorie: {ohne_kat}") +print(f" Kategorie zeigt ins Leere: {verwaist} <-- solche erscheinen nirgends") + +# Genau die Bedingung, die die Oberfläche für die Kategorie-Liste nutzt. +sichtbar = con.execute( + "SELECT COUNT(*) FROM categorization_rules r" + " JOIN categories c ON c.id = r.category_id" + " WHERE r.source = 'manual' AND r.is_transfer = 0" +).fetchone()[0] +print(f"\nIn der Kategorie-Liste sichtbar: {sichtbar} von {gesamt}") + +print("\nBeispiele (die ersten 12):") +print(f" {'Muster':<26}{'source':<10}{'kind':<10}{'transfer':<9}{'Kategorie'}") +for r in con.execute( + "SELECT r.pattern, r.source, r.kind, r.is_transfer, c.name AS kat" + " FROM categorization_rules r LEFT JOIN categories c ON c.id = r.category_id" + " ORDER BY r.id LIMIT 12" +): + print(f" {r['pattern'][:24]:<26}{str(r['source']):<10}{str(r['kind']):<10}" + f"{str(r['is_transfer']):<9}{r['kat'] or '— fehlt —'}") +con.close()