fix: default Auftragskatalog state to workspace membership, not absent

fetch_auftragskatalog_state() hardcoded absent as the default when no
device_merkmale row exists (COALESCE(dm.aktiv, FALSE)), regardless of
whether the Merkmal is part of the device's workspace. Since the
catalog is meant to be the same pool of Merkmale that workspaces are
composed from (see ADR-0010's clarification), that default was wrong:
flagging an already workspace-composed, actively-in-use Merkmal as
catalog-eligible would silently remove it from every device using
that workspace on their next check-in, without anyone ever deselecting
it.

Now joins workspace_merkmale for the device's assigned workspace and
defaults to present when the Merkmal is a member, absent otherwise;
an explicit device_merkmale row always overrides that default in
either direction. Verified against anode: catalog-eligible + already
workspace-composed Merkmal now defaults to present with no
device_merkmale row, explicit aktiv=false overrides to absent,
explicit aktiv=true overrides back to present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-04 10:06:45 +02:00
parent eb73f2b6b7
commit dc9bc40991

24
app.py
View File

@ -382,16 +382,23 @@ def fetch_assigned_blueprints(device_id: str):
def fetch_auftragskatalog_state(device_id: str): def fetch_auftragskatalog_state(device_id: str):
""" """
Voller Soll-Zustand (present/absent) aller katalogfähigen Merkmale für Voller Soll-Zustand (present/absent) aller katalogfähigen Merkmale für
das Backend des Device, unabhängig von der Workspace-Zuweisung das Backend des Device (siehe ADR-0010). Zustandslos aus der aktuellen
(siehe ADR-0010). Zustandslos aus der aktuellen Auswahl abgeleitet, Auswahl abgeleitet, keine Historie nötig.
keine Historie nötig.
Default ohne explizite Auftragszuweisung ist die Workspace-Zugehörigkeit:
der Auftragskatalog besteht aus denselben Merkmalen, aus denen auch
Workspaces zusammengesetzt werden, und ein Workspace ist fachlich nichts
anderes als eine Vorauswahl aus dem Katalog. Eine vorhandene
device_merkmale-Zeile überschreibt diesen Default in beide Richtungen
(auch ein workspace-komponiertes Merkmal lässt sich damit geräteweise
abwählen).
""" """
with get_database_connection() as conn: with get_database_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
SELECT bv.backend_id SELECT bv.workspace_id, bv.backend_id
FROM assignments a FROM assignments a
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
WHERE a.device_id = %s WHERE a.device_id = %s
@ -403,18 +410,21 @@ def fetch_auftragskatalog_state(device_id: str):
if assignment_row is None: if assignment_row is None:
return [] return []
(backend_id,) = assignment_row workspace_id, backend_id = assignment_row
cur.execute( cur.execute(
""" """
SELECT m.key, bp.ansible_role, COALESCE(dm.aktiv, FALSE) SELECT m.key, bp.ansible_role,
COALESCE(dm.aktiv, wm.merkmal_id IS NOT NULL)
FROM merkmale m FROM merkmale m
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
LEFT JOIN device_merkmale dm LEFT JOIN device_merkmale dm
ON dm.merkmal_id = m.id AND dm.device_id = %s ON dm.merkmal_id = m.id AND dm.device_id = %s
LEFT JOIN workspace_merkmale wm
ON wm.merkmal_id = m.id AND wm.workspace_id = %s
WHERE m.im_auftragskatalog WHERE m.im_auftragskatalog
""", """,
(backend_id, device_id), (backend_id, device_id, workspace_id),
) )
return [ return [
{ {