Server auf Workspace-Versionen und OE-Workspace umgestellt
geraete_basis() ermittelt die Grundausstattung eines Geraets: Distro aus der Installation, Workspace von der naechsten OE mit Workspace (gepinnte Version oder neueste veroeffentlichte), sonst wie bisher aus der Installation. Blueprints, Auftragskatalog, Webapps und Profil-Aufloesung lesen die Version; Gruppen-Webapps ergaenzen. Gruppen tragen keinen Workspace mehr, Exklusivregel und Endpunkte entfallen. Admin-Bearbeitung wirkt bis Schritt E auf die Arbeitsversion. Workspace-Liste/-Detail liefern Distro, Version, Vorlage, Sichtbarkeit; Sichtbarkeit fuer Kunden nach Eigentum, global, Kundengruppe oder Organisation. Lokal gegen eine Kopie der Produktionsdaten geprueft: Soll-Zustand aller 21 Geraete und 12 Profile identisch zum alten Code, 19 Szenarien gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ac7485179
commit
4a7ae27dc7
1 file changed
+301
-320
@@ -219,6 +219,7 @@ class CreateWorkspaceRequest(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
organization_id: str | None = None
|
||||
backend_id: str | None = None
|
||||
|
||||
|
||||
class CloneWorkspaceRequest(BaseModel):
|
||||
@@ -327,10 +328,6 @@ class UpdateGruppeRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class SetGruppeWorkspaceRequest(BaseModel):
|
||||
workspace_id: str
|
||||
|
||||
|
||||
class SetHostnameKonventionRequest(BaseModel):
|
||||
praefix: str | None = None
|
||||
stellen: int = 4
|
||||
@@ -936,17 +933,17 @@ WEBAPP_XDG_CATEGORIES_DEFAULT = "Network;WebBrowser;"
|
||||
KIOSK_BROWSER_PRIORITAET = ["google-chrome", "chromium"]
|
||||
|
||||
|
||||
def _resolve_kiosk_browser(workspace_id: str) -> str | None:
|
||||
def _resolve_kiosk_browser(version_id) -> str | None:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.key
|
||||
FROM workspace_merkmale wm
|
||||
JOIN merkmale m ON m.id = wm.merkmal_id
|
||||
WHERE wm.workspace_id = %s AND m.key = ANY(%s)
|
||||
FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
WHERE vm.workspace_version_id = %s AND m.key = ANY(%s)
|
||||
""",
|
||||
(workspace_id, KIOSK_BROWSER_PRIORITAET),
|
||||
(version_id, KIOSK_BROWSER_PRIORITAET),
|
||||
)
|
||||
vorhandene = {row[0] for row in cur.fetchall()}
|
||||
|
||||
@@ -957,37 +954,37 @@ def _resolve_kiosk_browser(workspace_id: str) -> str | None:
|
||||
|
||||
|
||||
def fetch_workspace_webapps(device_id: str):
|
||||
"""Webapps eines Geräts: die der Workspace-Version (geraete_basis())
|
||||
plus die Ergänzungen aller Gruppen, in denen das Gerät ist."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.workspace_id
|
||||
FROM assignments a
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
|
||||
WHERE a.device_id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
assignment_row = cur.fetchone()
|
||||
basis = geraete_basis(cur, device_id)
|
||||
|
||||
if assignment_row is None:
|
||||
if basis is None:
|
||||
return []
|
||||
|
||||
(workspace_id,) = assignment_row
|
||||
version_id, _backend_id = basis
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT w.id, w.name, w.url, w.icon_filename, w.kiosk_modus, k.key
|
||||
SELECT w.id, w.name, w.url, w.icon_filename, w.kiosk_modus, k.key, w.sort_order
|
||||
FROM workspace_webapps w
|
||||
LEFT JOIN kategorien k ON k.id = w.kategorie_id
|
||||
WHERE w.workspace_id = %s
|
||||
ORDER BY w.sort_order, w.name
|
||||
WHERE w.workspace_version_id = %s
|
||||
UNION ALL
|
||||
SELECT gw.id, gw.name, gw.url, gw.icon_filename, gw.kiosk_modus, k.key, gw.sort_order
|
||||
FROM gruppen_webapps gw
|
||||
JOIN device_gruppen dg ON dg.gruppe_id = gw.gruppe_id
|
||||
LEFT JOIN kategorien k ON k.id = gw.kategorie_id
|
||||
WHERE dg.device_id = %s
|
||||
ORDER BY 7, 2
|
||||
""",
|
||||
(workspace_id,),
|
||||
(version_id, device_id),
|
||||
)
|
||||
zeilen = cur.fetchall()
|
||||
zeilen = [row[:6] for row in cur.fetchall()]
|
||||
|
||||
kiosk_browser = _resolve_kiosk_browser(workspace_id) if any(row[4] for row in zeilen) else None
|
||||
kiosk_browser = _resolve_kiosk_browser(version_id) if any(row[4] for row in zeilen) else None
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1011,39 +1008,52 @@ def fetch_workspace_webapps(device_id: str):
|
||||
]
|
||||
|
||||
|
||||
def fetch_assigned_blueprints(device_id: str):
|
||||
def version_blueprints(cur, version_id, backend_id) -> list[dict]:
|
||||
"""Merkmale einer Workspace-Version, die auf dieser Distro umgesetzt
|
||||
sind, mit ihrer Ansible-Rolle."""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.key, bp.ansible_role
|
||||
FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
WHERE vm.workspace_version_id = %s
|
||||
""",
|
||||
(backend_id, version_id),
|
||||
)
|
||||
return [{"merkmal": key, "ansible_role": ansible_role} for key, ansible_role in cur.fetchall()]
|
||||
|
||||
|
||||
def profil_blueprints(template_id: str) -> list[dict] | None:
|
||||
"""Blueprints, mit denen ein Gerät über diese Bereitstellungsvorlage
|
||||
(Installationsprofil) installiert wird: neueste veröffentlichte Version
|
||||
des Profil-Workspace auf der Profil-Distro."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.workspace_id, bv.backend_id
|
||||
FROM assignments a
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
|
||||
WHERE a.device_id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
"SELECT workspace_id, backend_id FROM bereitstellungsvorlagen WHERE id = %s",
|
||||
(template_id,),
|
||||
)
|
||||
assignment_row = cur.fetchone()
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
workspace_id, backend_id = row
|
||||
return version_blueprints(cur, veroeffentlichte_version_id(cur, workspace_id), backend_id)
|
||||
|
||||
if assignment_row is None:
|
||||
|
||||
def fetch_assigned_blueprints(device_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
basis = geraete_basis(cur, device_id)
|
||||
|
||||
if basis is None:
|
||||
return []
|
||||
|
||||
workspace_id, backend_id = assignment_row
|
||||
version_id, backend_id = basis
|
||||
return version_blueprints(cur, version_id, backend_id)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.key, bp.ansible_role
|
||||
FROM workspace_merkmale wm
|
||||
JOIN merkmale m ON m.id = wm.merkmal_id
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
WHERE wm.workspace_id = %s
|
||||
""",
|
||||
(backend_id, workspace_id),
|
||||
)
|
||||
return [
|
||||
{"merkmal": key, "ansible_role": ansible_role}
|
||||
for key, ansible_role in cur.fetchall()
|
||||
]
|
||||
|
||||
def _fernet() -> Fernet:
|
||||
if not ENCRYPTION_KEY:
|
||||
@@ -1220,31 +1230,96 @@ def resolve_katalog_overrides(device_id: str) -> dict:
|
||||
|
||||
return ergebnis
|
||||
|
||||
def resolve_effective_workspace_id(cur, device_id: str, vorlage_workspace_id: str) -> str:
|
||||
def veroeffentlichte_version_id(cur, workspace_id):
|
||||
"""Neueste veröffentlichte Version eines Workspace (None, falls es noch
|
||||
keine gibt) - das bekommen Geräte, deren OE "künftige Versionen
|
||||
automatisch übernehmen" hat, und alle Bestandsgeräte ohne OE-Workspace."""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM workspace_versionen
|
||||
WHERE workspace_id = %s AND status = 'veroeffentlicht'
|
||||
ORDER BY nummer DESC LIMIT 1
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def arbeitsversion_id(cur, workspace_id):
|
||||
"""
|
||||
Ermittelt den effektiv geltenden Workspace eines Geräts: normalerweise
|
||||
der bei der Installation zugewiesene (über die Bereitstellungsvorlage),
|
||||
außer eine Gerätegruppe des Geräts trägt einen eigenen Workspace - dann
|
||||
gewinnt dieser (siehe set_gruppe_workspace(), Design-Runde 10.09.2026).
|
||||
Das Backend/die Distro bleiben davon unberührt, die ändert sich nie
|
||||
nachträglich - nur welcher Merkmal-Grundbestand gilt. Die
|
||||
Exklusivitätsregel beim Zuweisen sorgt dafür, dass hier höchstens eine
|
||||
Zeile zurückkommt.
|
||||
Die Version, die die Admin-Bearbeitung eines Workspace verändert: der
|
||||
offene Entwurf, sonst die neueste veröffentlichte. Übergangsregel bis
|
||||
Schritt E (Entwurf/Veröffentlichen mit Wirkungsvorschau, siehe
|
||||
platform-docs/roadmap/workspace-creator-neu.md): solange es keinen
|
||||
Entwurf gibt, wirken Änderungen wie bisher sofort.
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.workspace_id
|
||||
FROM device_gruppen dg
|
||||
JOIN gruppen g ON g.id = dg.gruppe_id
|
||||
WHERE dg.device_id = %s AND g.workspace_id IS NOT NULL
|
||||
LIMIT 1
|
||||
SELECT id FROM workspace_versionen
|
||||
WHERE workspace_id = %s
|
||||
ORDER BY (status = 'entwurf') DESC, nummer DESC LIMIT 1
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def geraete_basis(cur, device_id: str):
|
||||
"""
|
||||
Grundausstattung eines Geräts (Workspace-Creator neu, Schritt C):
|
||||
(workspace_version_id, backend_id) oder None ohne Zuweisung.
|
||||
|
||||
Die Distro (backend_id) kommt immer aus der Installation und ändert sich
|
||||
nie. Der Workspace kommt von der nächstgelegenen OE mit Workspace (eigene
|
||||
OE, dann die Eltern bis zur Wurzel) - mit deren gepinnter Version, oder
|
||||
der neuesten veröffentlichten, wenn dort "automatisch aktualisieren"
|
||||
gesetzt ist. Trägt keine OE der Kette einen Workspace, gilt wie bisher
|
||||
der Workspace aus der Installation (neueste veröffentlichte Version).
|
||||
Gruppen ersetzen die Grundausstattung nicht mehr, sie ergänzen nur
|
||||
(resolve_katalog_overrides(), gruppen_webapps).
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.workspace_id, bv.backend_id, d.organization_id, d.oe_id
|
||||
FROM assignments a
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
|
||||
JOIN devices d ON d.id = a.device_id
|
||||
WHERE a.device_id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
gruppen_row = cur.fetchone()
|
||||
row = cur.fetchone()
|
||||
|
||||
return gruppen_row[0] if gruppen_row is not None else vorlage_workspace_id
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
installations_workspace_id, backend_id, organization_id, oe_id = row
|
||||
|
||||
if oe_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, parent_id, workspace_id, workspace_version_id, automatisch_aktualisieren
|
||||
FROM organisationseinheiten WHERE organization_id = %s
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
oes = {r[0]: r[1:] for r in cur.fetchall()}
|
||||
|
||||
aktuell = oe_id
|
||||
while aktuell is not None and aktuell in oes:
|
||||
parent_id, workspace_id, version_id, automatisch = oes[aktuell]
|
||||
if workspace_id is not None:
|
||||
if automatisch or version_id is None:
|
||||
version_id = veroeffentlichte_version_id(cur, workspace_id)
|
||||
return version_id, backend_id
|
||||
aktuell = parent_id
|
||||
|
||||
return veroeffentlichte_version_id(cur, installations_workspace_id), backend_id
|
||||
|
||||
def fetch_auftragskatalog_state(device_id: str):
|
||||
"""
|
||||
@@ -1253,9 +1328,8 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
Konfigurationsgruppen-Plan). Zustandslos aus der aktuellen Auswahl
|
||||
abgeleitet, keine Historie nötig.
|
||||
|
||||
Auflösungsreihenfolge: Workspace-Default (ggf. durch eine
|
||||
workspace-tragende Gerätegruppe ersetzt, siehe
|
||||
resolve_effective_workspace_id()) → OE-/Gruppen-Merkmal-Zwischenschicht
|
||||
Auflösungsreihenfolge: Grundausstattung (Workspace-Version, siehe
|
||||
geraete_basis()) → OE-/Gruppen-Merkmal-Zwischenschicht
|
||||
(resolve_katalog_overrides). Die frühere device_merkmale-Override-Ebene
|
||||
(gewann immer) wurde am 10.09.2026 abgelöst - Einzelgerät-Ausnahmen
|
||||
laufen jetzt über eine Ein-Geräte-Gruppe, also über genau dieselbe
|
||||
@@ -1264,34 +1338,24 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.workspace_id, bv.backend_id
|
||||
FROM assignments a
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
|
||||
WHERE a.device_id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
assignment_row = cur.fetchone()
|
||||
basis = geraete_basis(cur, device_id)
|
||||
|
||||
if assignment_row is None:
|
||||
if basis is None:
|
||||
return []
|
||||
|
||||
vorlage_workspace_id, backend_id = assignment_row
|
||||
workspace_id = resolve_effective_workspace_id(cur, device_id, vorlage_workspace_id)
|
||||
version_id, backend_id = basis
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, bp.ansible_role,
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
(vm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
FROM merkmale m
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
LEFT JOIN workspace_merkmale wm
|
||||
ON wm.merkmal_id = m.id AND wm.workspace_id = %s
|
||||
LEFT JOIN workspace_version_merkmale vm
|
||||
ON vm.merkmal_id = m.id AND vm.workspace_version_id = %s
|
||||
WHERE m.im_auftragskatalog
|
||||
""",
|
||||
(backend_id, workspace_id),
|
||||
(backend_id, version_id),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
@@ -1323,37 +1387,27 @@ def fetch_auftragskatalog_listing(device_id: str):
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.workspace_id, bv.backend_id
|
||||
FROM assignments a
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
|
||||
WHERE a.device_id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
assignment_row = cur.fetchone()
|
||||
basis = geraete_basis(cur, device_id)
|
||||
|
||||
if assignment_row is None:
|
||||
if basis is None:
|
||||
return None
|
||||
|
||||
vorlage_workspace_id, backend_id = assignment_row
|
||||
workspace_id = resolve_effective_workspace_id(cur, device_id, vorlage_workspace_id)
|
||||
version_id, backend_id = basis
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, m.name, m.description,
|
||||
k.key, k.name, k.sort_order,
|
||||
(wm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
(vm.merkmal_id IS NOT NULL) AS workspace_default
|
||||
FROM merkmale m
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
LEFT JOIN workspace_merkmale wm
|
||||
ON wm.merkmal_id = m.id AND wm.workspace_id = %s
|
||||
LEFT JOIN workspace_version_merkmale vm
|
||||
ON vm.merkmal_id = m.id AND vm.workspace_version_id = %s
|
||||
WHERE m.im_auftragskatalog
|
||||
ORDER BY k.sort_order NULLS LAST, m.name
|
||||
""",
|
||||
(backend_id, workspace_id),
|
||||
(backend_id, version_id),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
@@ -1813,23 +1867,14 @@ def fetch_gruppen(organization_id: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.id, g.name, g.workspace_id, w.name
|
||||
SELECT g.id, g.name
|
||||
FROM gruppen g
|
||||
LEFT JOIN workspaces w ON w.id = g.workspace_id
|
||||
WHERE g.organization_id = %s
|
||||
ORDER BY g.name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(gruppe_id),
|
||||
"name": name,
|
||||
"workspace_id": str(workspace_id) if workspace_id else None,
|
||||
"workspace_name": workspace_name,
|
||||
}
|
||||
for gruppe_id, name, workspace_id, workspace_name in cur.fetchall()
|
||||
]
|
||||
return [{"id": str(gruppe_id), "name": name} for gruppe_id, name in cur.fetchall()]
|
||||
|
||||
def create_gruppe(organization_id: str, name: str):
|
||||
gruppe_id = uuid4()
|
||||
@@ -1863,87 +1908,6 @@ def delete_gruppe(gruppe_id: str):
|
||||
cur.execute("DELETE FROM gruppen WHERE id = %s RETURNING id", (gruppe_id,))
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def set_gruppe_workspace(gruppe_id: str, workspace_id: str):
|
||||
"""
|
||||
Weist einer Gerätegruppe einen alternativen Workspace zu (Design-Runde
|
||||
10.09.2026, siehe project_phase2_app_store_merkmale-Memory) - ersetzt
|
||||
für Mitgliedsgeräte die komplette Workspace-Baseline (nicht nur
|
||||
einzelne Merkmale, dafür bleibt gruppen_merkmale zuständig). Ein
|
||||
Gerät kann gleichzeitig nur in EINER workspace-tragenden Gerätegruppe
|
||||
sein (bewusste Nutzer-Entscheidung, verhindert widersprüchliche
|
||||
Workspace-Wahl statt einer Prioritätsregel) - deshalb hier zusätzlich
|
||||
zur Organisationszugehörigkeit geprüft: kein Mitgliedsgerät der
|
||||
Gruppe darf schon in einer ANDEREN workspace-tragenden Gruppe sein.
|
||||
|
||||
Rückgabe: (True, None) bei Erfolg, sonst (False, Fehlercode).
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT organization_id FROM gruppen WHERE id = %s",
|
||||
(gruppe_id,),
|
||||
)
|
||||
gruppe_row = cur.fetchone()
|
||||
|
||||
if gruppe_row is None:
|
||||
return False, "gruppe_not_found"
|
||||
|
||||
gruppe_org_id = gruppe_row[0]
|
||||
|
||||
cur.execute(
|
||||
"SELECT organization_id FROM workspaces WHERE id = %s",
|
||||
(workspace_id,),
|
||||
)
|
||||
workspace_row = cur.fetchone()
|
||||
|
||||
if workspace_row is None:
|
||||
return False, "workspace_not_found"
|
||||
|
||||
# NULL organization_id = globaler Standard-Workspace (siehe
|
||||
# workspaces-Schema), den jede Organisation nutzen darf. Ist er
|
||||
# gesetzt, muss er zur eigenen Organisation gehören - ein
|
||||
# fremder, privater Workspace einer anderen Organisation darf
|
||||
# nicht ausgewählt werden können.
|
||||
workspace_org_id = workspace_row[0]
|
||||
if workspace_org_id is not None and workspace_org_id != gruppe_org_id:
|
||||
return False, "workspace_wrong_organization"
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT DISTINCT d.id, d.hostname, g2.name
|
||||
FROM device_gruppen dg
|
||||
JOIN devices d ON d.id = dg.device_id
|
||||
JOIN gruppen g2 ON g2.id = dg.gruppe_id
|
||||
WHERE dg.device_id IN (
|
||||
SELECT device_id FROM device_gruppen WHERE gruppe_id = %s
|
||||
)
|
||||
AND g2.id != %s
|
||||
AND g2.workspace_id IS NOT NULL
|
||||
""",
|
||||
(gruppe_id, gruppe_id),
|
||||
)
|
||||
konflikt_row = cur.fetchone()
|
||||
|
||||
if konflikt_row is not None:
|
||||
return False, "workspace_conflict"
|
||||
|
||||
cur.execute(
|
||||
"UPDATE gruppen SET workspace_id = %s WHERE id = %s",
|
||||
(workspace_id, gruppe_id),
|
||||
)
|
||||
|
||||
return True, None
|
||||
|
||||
def clear_gruppe_workspace(gruppe_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE gruppen SET workspace_id = NULL WHERE id = %s RETURNING id",
|
||||
(gruppe_id,),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def set_gruppe_auswahl(gruppe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
@@ -2034,17 +1998,16 @@ def fetch_gruppe_merkmale_listing(gruppe_id: str):
|
||||
def add_device_to_gruppe(device_id: str, gruppe_id: str):
|
||||
"""
|
||||
Fügt ein Gerät einer Gruppe hinzu (idempotent). Gibt False zurück, wenn
|
||||
Gerät und Gruppe nicht zur selben Organisation gehören, oder "conflict",
|
||||
wenn die Zielgruppe einen Workspace trägt und das Gerät bereits in einer
|
||||
ANDEREN workspace-tragenden Gruppe ist (siehe set_gruppe_workspace() für
|
||||
die Begründung der Exklusivitätsregel).
|
||||
Gerät und Gruppe nicht zur selben Organisation gehören. Seit 07.10.2026
|
||||
tragen Gruppen keinen eigenen Workspace mehr (nur noch Ergänzungen),
|
||||
die frühere Exklusivitätsregel entfällt damit.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT d.organization_id, g.organization_id, g.workspace_id
|
||||
SELECT d.organization_id, g.organization_id
|
||||
FROM devices d, gruppen g
|
||||
WHERE d.id = %s AND g.id = %s
|
||||
""",
|
||||
@@ -2055,21 +2018,6 @@ def add_device_to_gruppe(device_id: str, gruppe_id: str):
|
||||
if row is None or row[0] != row[1]:
|
||||
return False
|
||||
|
||||
ziel_workspace_id = row[2]
|
||||
|
||||
if ziel_workspace_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM device_gruppen dg
|
||||
JOIN gruppen g ON g.id = dg.gruppe_id
|
||||
WHERE dg.device_id = %s AND g.workspace_id IS NOT NULL AND g.id != %s
|
||||
""",
|
||||
(device_id, gruppe_id),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
return "conflict"
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_gruppen (device_id, gruppe_id)
|
||||
@@ -2900,9 +2848,15 @@ def fetch_workspaces():
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name,
|
||||
w.backend_id, b.name, b.version, w.ist_vorlage, w.sichtbarkeit,
|
||||
(SELECT max(nummer) FROM workspace_versionen v
|
||||
WHERE v.workspace_id = w.id AND v.status = 'veroeffentlicht'),
|
||||
EXISTS (SELECT 1 FROM workspace_versionen v
|
||||
WHERE v.workspace_id = w.id AND v.status = 'entwurf')
|
||||
FROM workspaces w
|
||||
LEFT JOIN organizations o ON o.id = w.organization_id
|
||||
LEFT JOIN backends b ON b.id = w.backend_id
|
||||
ORDER BY o.name NULLS FIRST, w.name
|
||||
"""
|
||||
)
|
||||
@@ -2914,30 +2868,49 @@ def fetch_workspaces():
|
||||
"description": description,
|
||||
"organization_id": str(organization_id) if organization_id else None,
|
||||
"organization_name": organization_name,
|
||||
"backend": (
|
||||
{"id": str(backend_id), "name": backend_name, "version": backend_version}
|
||||
if backend_id else None
|
||||
),
|
||||
"ist_vorlage": ist_vorlage,
|
||||
"sichtbarkeit": sichtbarkeit,
|
||||
"version": version,
|
||||
"hat_entwurf": hat_entwurf,
|
||||
}
|
||||
for (
|
||||
wid, key, name, description, organization_id, organization_name,
|
||||
backend_id, backend_name, backend_version, ist_vorlage, sichtbarkeit,
|
||||
version, hat_entwurf,
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def fetch_available_workspaces_for_organization(organization_id: str):
|
||||
"""
|
||||
Workspaces, die eine Organisation für ihre Gerätegruppen wählen darf:
|
||||
globale Standard-Workspaces (organization_id IS NULL, von uns
|
||||
gepflegt) plus die eigenen. Bewusst NICHT fetch_workspaces() - die
|
||||
liefert workspaces aller Organisationen (nur für die Admin-Techniker-
|
||||
Ansicht gedacht), das würde im Kundenportal fremde, private Workspace-
|
||||
Namen preisgeben.
|
||||
Workspaces, die eine Organisation sehen und verwenden darf: die eigenen
|
||||
plus die für sie freigegebenen Vorlagen - global, für ihre Kundengruppe
|
||||
oder ausdrücklich für sie (Workspace-Creator neu, 07.10.2026). Bewusst
|
||||
NICHT fetch_workspaces() - die liefert Workspaces aller Organisationen
|
||||
(nur für die Admin-Ansicht), das würde im Kundenportal fremde, private
|
||||
Workspace-Namen preisgeben.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, key, name, description, organization_id
|
||||
FROM workspaces
|
||||
WHERE organization_id IS NULL OR organization_id = %s
|
||||
ORDER BY (organization_id IS NULL) DESC, name
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id
|
||||
FROM workspaces w
|
||||
JOIN organizations o ON o.id = %s
|
||||
WHERE w.organization_id = o.id
|
||||
OR (w.ist_vorlage AND (
|
||||
w.sichtbarkeit = 'global'
|
||||
OR (w.sichtbarkeit = 'kundengruppe' AND EXISTS (
|
||||
SELECT 1 FROM workspace_freigabe_kundengruppen f
|
||||
WHERE f.workspace_id = w.id AND f.kundengruppe_id = o.kundengruppe_id))
|
||||
OR (w.sichtbarkeit = 'organisation' AND EXISTS (
|
||||
SELECT 1 FROM workspace_freigabe_organisationen f
|
||||
WHERE f.workspace_id = w.id AND f.organization_id = o.id))))
|
||||
ORDER BY (w.organization_id IS NULL) DESC, w.name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
@@ -2956,17 +2929,35 @@ def fetch_available_workspaces_for_organization(organization_id: str):
|
||||
for wid, key, name, description, ws_org_id in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_workspace(key: str, name: str, description: str | None, organization_id: str | None):
|
||||
def create_workspace(
|
||||
key: str, name: str, description: str | None, organization_id: str | None,
|
||||
backend_id: str | None = None,
|
||||
):
|
||||
"""Legt einen Workspace samt Version 1 an. Bis Schritt E (Entwurf/
|
||||
Veröffentlichen) ist Version 1 sofort veröffentlicht und wird direkt
|
||||
bearbeitet (siehe arbeitsversion_id())."""
|
||||
|
||||
workspace_id = uuid4()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspaces (id, organization_id, key, name, description)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
INSERT INTO workspaces (id, organization_id, key, name, description, backend_id, sichtbarkeit, ist_vorlage)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(workspace_id, organization_id, key, name, description),
|
||||
(
|
||||
workspace_id, organization_id, key, name, description, backend_id,
|
||||
"global" if organization_id is None else "organisation",
|
||||
organization_id is None,
|
||||
),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_versionen (id, workspace_id, nummer, status, veroeffentlicht_am)
|
||||
VALUES (%s, %s, 1, 'veroeffentlicht', now())
|
||||
""",
|
||||
(uuid4(), workspace_id),
|
||||
)
|
||||
|
||||
return {"id": str(workspace_id), "key": key, "name": name}
|
||||
@@ -3008,7 +2999,8 @@ def clone_workspace(source_workspace_id: str, organization_id: str):
|
||||
zaehler += 1
|
||||
|
||||
neuer_workspace = create_workspace(
|
||||
neuer_key, f"{quelle['name']} (eigene Kopie)", quelle["description"], organization_id
|
||||
neuer_key, f"{quelle['name']} (eigene Kopie)", quelle["description"], organization_id,
|
||||
quelle["backend"]["id"] if quelle["backend"] else None,
|
||||
)
|
||||
|
||||
for merkmal in quelle["merkmale"]:
|
||||
@@ -3021,9 +3013,11 @@ def fetch_workspace_detail(workspace_id: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name
|
||||
SELECT w.id, w.key, w.name, w.description, w.organization_id, o.name,
|
||||
w.backend_id, b.name, b.version, w.ist_vorlage, w.sichtbarkeit
|
||||
FROM workspaces w
|
||||
LEFT JOIN organizations o ON o.id = w.organization_id
|
||||
LEFT JOIN backends b ON b.id = w.backend_id
|
||||
WHERE w.id = %s
|
||||
""",
|
||||
(workspace_id,),
|
||||
@@ -3033,15 +3027,22 @@ def fetch_workspace_detail(workspace_id: str):
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
version_id = arbeitsversion_id(cur, workspace_id)
|
||||
cur.execute(
|
||||
"SELECT nummer, status FROM workspace_versionen WHERE id = %s",
|
||||
(version_id,),
|
||||
)
|
||||
version_row = cur.fetchone()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.key, m.name
|
||||
FROM workspace_merkmale wm
|
||||
JOIN merkmale m ON m.id = wm.merkmal_id
|
||||
WHERE wm.workspace_id = %s
|
||||
FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
WHERE vm.workspace_version_id = %s
|
||||
ORDER BY m.key
|
||||
""",
|
||||
(workspace_id,),
|
||||
(version_id,),
|
||||
)
|
||||
merkmale = [
|
||||
{"id": str(mid), "key": key, "name": name}
|
||||
@@ -3055,6 +3056,13 @@ def fetch_workspace_detail(workspace_id: str):
|
||||
"description": row[3],
|
||||
"organization_id": str(row[4]) if row[4] else None,
|
||||
"organization_name": row[5],
|
||||
"backend": {"id": str(row[6]), "name": row[7], "version": row[8]} if row[6] else None,
|
||||
"ist_vorlage": row[9],
|
||||
"sichtbarkeit": row[10],
|
||||
"version": (
|
||||
{"id": str(version_id), "nummer": version_row[0], "status": version_row[1]}
|
||||
if version_row else None
|
||||
),
|
||||
"merkmale": merkmale,
|
||||
}
|
||||
|
||||
@@ -3063,11 +3071,11 @@ def add_merkmal_to_workspace(workspace_id: str, merkmal_id: str):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_merkmale (workspace_id, merkmal_id)
|
||||
INSERT INTO workspace_version_merkmale (workspace_version_id, merkmal_id)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(workspace_id, merkmal_id),
|
||||
(arbeitsversion_id(cur, workspace_id), merkmal_id),
|
||||
)
|
||||
|
||||
def _write_webapp_icon(webapp_id, icon_bytes: bytes, content_type: str) -> str:
|
||||
@@ -3147,10 +3155,10 @@ def fetch_webapps_for_workspace(workspace_id: str):
|
||||
w.kategorie_id, k.name, w.kiosk_modus
|
||||
FROM workspace_webapps w
|
||||
LEFT JOIN kategorien k ON k.id = w.kategorie_id
|
||||
WHERE w.workspace_id = %s
|
||||
WHERE w.workspace_version_id = %s
|
||||
ORDER BY w.sort_order, w.name
|
||||
""",
|
||||
(workspace_id,),
|
||||
(arbeitsversion_id(cur, workspace_id),),
|
||||
)
|
||||
return [
|
||||
{
|
||||
@@ -3181,10 +3189,14 @@ def create_webapp(
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_webapps
|
||||
(id, workspace_id, name, url, icon_filename, icon_content_type, sort_order, kategorie_id, kiosk_modus)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
(id, workspace_id, workspace_version_id, name, url, icon_filename, icon_content_type,
|
||||
sort_order, kategorie_id, kiosk_modus)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(webapp_id, workspace_id, name, url, icon_filename, icon_content_type, sort_order, kategorie_id, kiosk_modus),
|
||||
(
|
||||
webapp_id, workspace_id, arbeitsversion_id(cur, workspace_id), name, url,
|
||||
icon_filename, icon_content_type, sort_order, kategorie_id, kiosk_modus,
|
||||
),
|
||||
)
|
||||
|
||||
return {"id": str(webapp_id), "name": name, "url": url, "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None}
|
||||
@@ -3231,7 +3243,23 @@ def delete_webapp(webapp_id: str):
|
||||
row = cur.fetchone()
|
||||
cur.execute("DELETE FROM workspace_webapps WHERE id = %s", (webapp_id,))
|
||||
|
||||
if row and row[0]:
|
||||
# Seit den Workspace-Versionen (07.10.2026) teilen sich Kopien
|
||||
# derselben Webapp in mehreren Versionen eine Icon-Datei - nur
|
||||
# löschen, wenn sie niemand mehr verwendet.
|
||||
icon_noch_verwendet = False
|
||||
if row and row[0]:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM workspace_webapps WHERE icon_filename = %s
|
||||
UNION ALL
|
||||
SELECT 1 FROM gruppen_webapps WHERE icon_filename = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(row[0], row[0]),
|
||||
)
|
||||
icon_noch_verwendet = cur.fetchone() is not None
|
||||
|
||||
if row and row[0] and not icon_noch_verwendet:
|
||||
icon_path = WEBAPP_ICONS_DIR / row[0]
|
||||
if icon_path.is_file():
|
||||
icon_path.unlink()
|
||||
@@ -3280,11 +3308,11 @@ def fetch_available_templates_for_workspace(workspace_id: str):
|
||||
FROM webapp_templates t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM workspace_webapps w
|
||||
WHERE w.workspace_id = %s AND w.template_id = t.id
|
||||
WHERE w.workspace_version_id = %s AND w.template_id = t.id
|
||||
)
|
||||
ORDER BY t.name
|
||||
""",
|
||||
(workspace_id,),
|
||||
(arbeitsversion_id(cur, workspace_id),),
|
||||
)
|
||||
return [
|
||||
{
|
||||
@@ -3371,10 +3399,11 @@ def activate_webapp_template(workspace_id: str, template_id: str):
|
||||
return None
|
||||
|
||||
name, url, icon_filename, icon_content_type, kategorie_id = template
|
||||
version_id = arbeitsversion_id(cur, workspace_id)
|
||||
|
||||
cur.execute(
|
||||
"SELECT 1 FROM workspace_webapps WHERE workspace_id = %s AND template_id = %s",
|
||||
(workspace_id, template_id),
|
||||
"SELECT 1 FROM workspace_webapps WHERE workspace_version_id = %s AND template_id = %s",
|
||||
(version_id, template_id),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
return None
|
||||
@@ -3398,10 +3427,15 @@ def activate_webapp_template(workspace_id: str, template_id: str):
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_webapps (id, workspace_id, name, url, icon_filename, icon_content_type, template_id, kategorie_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
INSERT INTO workspace_webapps
|
||||
(id, workspace_id, workspace_version_id, name, url, icon_filename, icon_content_type,
|
||||
template_id, kategorie_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(webapp_id, workspace_id, name, url, copied_icon_filename, icon_content_type, template_id, kategorie_id),
|
||||
(
|
||||
webapp_id, workspace_id, version_id, name, url, copied_icon_filename,
|
||||
icon_content_type, template_id, kategorie_id,
|
||||
),
|
||||
)
|
||||
|
||||
return {"id": str(webapp_id), "name": name, "url": url, "icon_path": f"/webapp-icons/{copied_icon_filename}" if copied_icon_filename else None}
|
||||
@@ -3411,8 +3445,8 @@ def remove_merkmal_from_workspace(workspace_id: str, merkmal_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM workspace_merkmale WHERE workspace_id = %s AND merkmal_id = %s",
|
||||
(workspace_id, merkmal_id),
|
||||
"DELETE FROM workspace_version_merkmale WHERE workspace_version_id = %s AND merkmal_id = %s",
|
||||
(arbeitsversion_id(cur, workspace_id), merkmal_id),
|
||||
)
|
||||
|
||||
def fetch_backends():
|
||||
@@ -4092,20 +4126,7 @@ def resolve_template(template_id: str, payload: ResolveRequest):
|
||||
disk_encryption, partitioning, secure_boot_required,
|
||||
) = template_row
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.key, bp.ansible_role
|
||||
FROM workspace_merkmale wm
|
||||
JOIN merkmale m ON m.id = wm.merkmal_id
|
||||
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
WHERE wm.workspace_id = %s
|
||||
""",
|
||||
(backend_id, workspace_id),
|
||||
)
|
||||
blueprints = [
|
||||
{"merkmal": key, "ansible_role": ansible_role}
|
||||
for key, ansible_role in cur.fetchall()
|
||||
]
|
||||
blueprints = version_blueprints(cur, veroeffentlichte_version_id(cur, workspace_id), backend_id)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
@@ -4171,8 +4192,13 @@ def get_webapp_icon(filename: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT icon_content_type FROM workspace_webapps WHERE icon_filename = %s LIMIT 1",
|
||||
(safe_name,),
|
||||
"""
|
||||
SELECT icon_content_type FROM workspace_webapps WHERE icon_filename = %s
|
||||
UNION ALL
|
||||
SELECT icon_content_type FROM gruppen_webapps WHERE icon_filename = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(safe_name, safe_name),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
media_type = row[0] if row and row[0] else "application/octet-stream"
|
||||
@@ -4837,7 +4863,9 @@ def create_workspace_endpoint(payload: CreateWorkspaceRequest, authorization: st
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"workspace": create_workspace(payload.key, payload.name, payload.description, payload.organization_id),
|
||||
"workspace": create_workspace(
|
||||
payload.key, payload.name, payload.description, payload.organization_id, payload.backend_id
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -5238,7 +5266,7 @@ def get_device_auftragskatalog(
|
||||
|
||||
# Direkte Geräte-Einzel-Auswahl (select/deselect) wurde am 10.09.2026
|
||||
# abgelöst - device_merkmale ist weg, Einzelgerät-Ausnahmen laufen jetzt
|
||||
# über eine Ein-Geräte-Gruppe (siehe set_gruppe_workspace() und
|
||||
# über eine Ein-Geräte-Gruppe (siehe
|
||||
# gruppen_merkmale-Endpunkte oben). get_device_auftragskatalog() bleibt als
|
||||
# reine Ist-Zustand-Anzeige bestehen.
|
||||
|
||||
@@ -5459,45 +5487,6 @@ def list_available_workspaces(organization_id: str, authorization: str | None =
|
||||
return {"success": True, "workspaces": fetch_available_workspaces_for_organization(organization_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/gruppen/{gruppe_id}/workspace")
|
||||
def set_gruppe_workspace_endpoint(
|
||||
gruppe_id: str,
|
||||
payload: SetGruppeWorkspaceRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
erfolg, fehlercode = set_gruppe_workspace(gruppe_id, payload.workspace_id)
|
||||
|
||||
if not erfolg:
|
||||
nachrichten = {
|
||||
"gruppe_not_found": "Die Gruppe wurde nicht gefunden.",
|
||||
"workspace_not_found": "Der Workspace wurde nicht gefunden.",
|
||||
"workspace_wrong_organization": "Dieser Workspace gehört einer anderen Organisation.",
|
||||
"workspace_conflict": "Mindestens ein Mitgliedsgerät ist bereits in einer anderen "
|
||||
"workspace-tragenden Gerätegruppe.",
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": fehlercode,
|
||||
"message": nachrichten.get(fehlercode, "Workspace konnte nicht zugewiesen werden."),
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/api/v1/gruppen/{gruppe_id}/workspace")
|
||||
def clear_gruppe_workspace_endpoint(gruppe_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not clear_gruppe_workspace(gruppe_id):
|
||||
return {"success": False, "error": "gruppe_not_found", "message": "Die Gruppe wurde nicht gefunden."}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/gruppen/{gruppe_id}/merkmale")
|
||||
def get_gruppe_merkmale(gruppe_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
@@ -5521,14 +5510,6 @@ def add_device_gruppe_endpoint(device_id: str, gruppe_id: str, authorization: st
|
||||
|
||||
ergebnis = add_device_to_gruppe(device_id, gruppe_id)
|
||||
|
||||
if ergebnis == "conflict":
|
||||
return {
|
||||
"success": False,
|
||||
"error": "workspace_conflict",
|
||||
"message": "Das Gerät ist bereits in einer anderen Gerätegruppe mit eigenem Workspace. "
|
||||
"Ein Gerät kann nur in einer workspace-tragenden Gerätegruppe gleichzeitig sein.",
|
||||
}
|
||||
|
||||
if not ergebnis:
|
||||
return {
|
||||
"success": False,
|
||||
|
||||
Reference in new issue
Block a user