feat: Herkunfts-ID + OE-Join für Admin-Geräteliste

resolve_katalog_overrides() liefert jetzt zusätzlich quelle_id (konkrete
oe_id/gruppe_id, die eine Auftragskatalog-Einstellung geliefert hat) statt
nur den Herkunfts-Typ - Grundlage für anklickbare Herkunfts-Links im
Kundenplattform-Auftragskatalog. fetch_auftragskatalog_listing() gibt
quelle_id mit aus, fetch_auftragskatalog_state() (Agent-Pfad) ignoriert
sie weiterhin.

fetch_all_devices() (Admin-Geräteliste) bekommt denselben OE-Join wie
fetch_devices_for_organization() letzte Session - Grundlage für OE-Spalte
in der Admin-Geräteliste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-12 21:27:51 +02:00
parent a0df9f3ef9
commit e5b7081955

55
app.py
View File

@ -637,10 +637,14 @@ def resolve_katalog_overrides(device_id: str) -> dict:
"""
Ermittelt für ein Gerät die OE-/Gruppen-Zwischenschicht des
Auftragskatalogs (siehe Konfigurationsgruppen-Plan): pro Merkmal-ID ein
(aktiv, quelle)-Tupel, falls die OE-Kette oder eine Gruppe eine explizite
Meinung dazu hat - fehlt der Merkmal-Key im Ergebnis, hat weder OE-Kette
noch eine Gruppe eine Meinung, der Aufrufer fällt dann auf den
Workspace-Default zurück.
(aktiv, quelle, quelle_id)-Tupel, falls die OE-Kette oder eine Gruppe eine
explizite Meinung dazu hat - fehlt der Merkmal-Key im Ergebnis, hat weder
OE-Kette noch eine Gruppe eine Meinung, der Aufrufer fällt dann auf den
Workspace-Default zurück. quelle_id ist die konkrete oe_id/gruppe_id, die
das Ergebnis geliefert hat (fürs Kundenportal, Sprung von der
Herkunfts-Anzeige zur verantwortlichen OE/Gruppe) - bei mehreren
zustimmenden Gruppen wird eine davon genannt, kein Anspruch auf
vollständige Attribution aller Beitragenden.
Reihenfolge: innerhalb der OE-Kette gewinnt die spezifischste OE
(klassische Vererbung, von der eigenen OE aus nach oben zur Wurzel
@ -692,30 +696,36 @@ def resolve_katalog_overrides(device_id: str) -> dict:
for oe_id_in_chain in oe_chain:
for merkmal_id, aktiv in rows_by_oe.get(oe_id_in_chain, {}).items():
oe_aktiv_by_merkmal.setdefault(merkmal_id, aktiv)
oe_aktiv_by_merkmal.setdefault(merkmal_id, (aktiv, oe_id_in_chain))
cur.execute(
"""
SELECT gm.merkmal_id, gm.aktiv
SELECT gm.merkmal_id, gm.gruppe_id, gm.aktiv
FROM device_gruppen dg
JOIN gruppen_merkmale gm ON gm.gruppe_id = dg.gruppe_id
WHERE dg.device_id = %s
""",
(device_id,),
)
gruppen_aktiv_by_merkmal = {}
for merkmal_id, aktiv in cur.fetchall():
gruppen_aktiv_by_merkmal.setdefault(merkmal_id, []).append(aktiv)
gruppen_stimmen_by_merkmal = {}
for merkmal_id, gruppe_id, aktiv in cur.fetchall():
gruppen_stimmen_by_merkmal.setdefault(merkmal_id, []).append((gruppe_id, aktiv))
ergebnis = {}
for merkmal_id in set(oe_aktiv_by_merkmal) | set(gruppen_aktiv_by_merkmal):
oe_aktiv = oe_aktiv_by_merkmal.get(merkmal_id)
gruppen_stimmen = gruppen_aktiv_by_merkmal.get(merkmal_id, [])
for merkmal_id in set(oe_aktiv_by_merkmal) | set(gruppen_stimmen_by_merkmal):
oe_aktiv, oe_id_gewinner = oe_aktiv_by_merkmal.get(merkmal_id, (None, None))
gruppen_stimmen = gruppen_stimmen_by_merkmal.get(merkmal_id, [])
aktive_gruppen = [gruppe_id for gruppe_id, aktiv in gruppen_stimmen if aktiv]
inaktive_gruppen = [gruppe_id for gruppe_id, aktiv in gruppen_stimmen if not aktiv]
if oe_aktiv is True or True in gruppen_stimmen:
ergebnis[merkmal_id] = (True, "oe" if oe_aktiv is True else "gruppe")
if oe_aktiv is True or aktive_gruppen:
quelle = "oe" if oe_aktiv is True else "gruppe"
quelle_id = oe_id_gewinner if oe_aktiv is True else aktive_gruppen[0]
ergebnis[merkmal_id] = (True, quelle, quelle_id)
else:
ergebnis[merkmal_id] = (False, "oe" if oe_aktiv is False else "gruppe")
quelle = "oe" if oe_aktiv is False else "gruppe"
quelle_id = oe_id_gewinner if oe_aktiv is False else inaktive_gruppen[0]
ergebnis[merkmal_id] = (False, quelle, quelle_id)
return ergebnis
@ -774,7 +784,7 @@ def fetch_auftragskatalog_state(device_id: str):
if device_override is not None:
aktiv = device_override
elif merkmal_id in overrides:
aktiv, _quelle = overrides[merkmal_id]
aktiv, _quelle, _quelle_id = overrides[merkmal_id]
else:
aktiv = workspace_default
@ -842,11 +852,11 @@ def fetch_auftragskatalog_listing(device_id: str):
device_override, workspace_default, optionen,
) in rows:
if device_override is not None:
aktiv, quelle = device_override, "geraet"
aktiv, quelle, quelle_id = device_override, "geraet", None
elif merkmal_id in overrides:
aktiv, quelle = overrides[merkmal_id]
aktiv, quelle, quelle_id = overrides[merkmal_id]
else:
aktiv, quelle = workspace_default, "workspace"
aktiv, quelle, quelle_id = workspace_default, "workspace", None
ergebnis.append({
"merkmal": key,
@ -858,6 +868,7 @@ def fetch_auftragskatalog_listing(device_id: str):
),
"state": "present" if aktiv else "absent",
"quelle": quelle,
"quelle_id": str(quelle_id) if quelle_id is not None else None,
"optionen": optionen,
})
@ -1381,9 +1392,10 @@ def fetch_all_devices():
cur.execute(
"""
SELECT d.id, d.hostname, d.device_fingerprint, d.agent_last_checkin,
d.organization_id, o.name, d.deprovisioned_at
d.organization_id, o.name, d.deprovisioned_at, d.oe_id, oe.name
FROM devices d
JOIN organizations o ON o.id = d.organization_id
LEFT JOIN organisationseinheiten oe ON oe.id = d.oe_id
WHERE d.archived_at IS NULL
ORDER BY o.name, d.hostname NULLS LAST, d.created_at
"""
@ -1401,10 +1413,13 @@ def fetch_all_devices():
"deprovisioned_at": (
deprovisioned_at.isoformat() if deprovisioned_at is not None else None
),
"oe_id": str(oe_id) if oe_id is not None else None,
"oe_name": oe_name,
}
for (
device_id, hostname, fingerprint, last_checkin,
organization_id, organization_name, deprovisioned_at,
oe_id, oe_name,
) in cur.fetchall()
]