wiki/concepts/Navigation-API-Konzept.md
2026-01-25 03:01:35 +01:00

24 KiB

Navigation API Konzept

Übersicht

Dieses Dokument beschreibt das Konzept für die Navigation-API, welche dem UI alle notwendigen Informationen für das Rendering der Navigation liefert. Die API ist die Single Source of Truth für die Navigationsstruktur.

Grundprinzipien

1. Backend liefert, UI rendert

  • Das Gateway bestimmt, welche Navigationselemente ein User sehen darf
  • Das UI rendert nur das, was es vom Gateway erhält
  • Keine Permission-Logik im UI für Navigation

2. Trennung von Daten und Darstellung

  • Die API liefert keine Style-Elemente (Icons, CSS-Klassen, etc.)
  • Die API liefert UI-Komponenten-Codes, die das UI auf seine Komponenten mappt
  • Verschiedene UIs können dieselben Daten unterschiedlich darstellen

3. Nur sichtbare Elemente

  • Elemente ohne Zugriffsberechtigung werden nicht in den Baum aufgenommen
  • Kein hasAccess: false - wenn kein Zugriff, dann nicht im Response

4. Fehlertoleranz

  • Wenn ein Code im UI nicht gemappt werden kann → Fehlertext im Nav-Tree
  • Synchronisationsprobleme zwischen Gateway und UI werden sofort sichtbar

5. Personalisierbare Sortierung

  • Jedes Element hat eine order Nummer für die Sortierung
  • User können die Reihenfolge anpassen durch User-Settings
  • Gateway liefert bereits sortiert (Default + User-Override)

API Response Struktur

Endpoint

GET /api/navigation

Response Format

{
  "language": "de",
  "blocks": [
    {
      "type": "static",
      "id": "system",
      "title": "SYSTEM",
      "order": 10,
      "items": [...]
    },
    {
      "type": "static",
      "id": "workflows",
      "title": "WORKFLOWS",
      "order": 20,
      "items": [...]
    },
    {
      "type": "dynamic",
      "id": "features",
      "title": "FEATURES",
      "order": 100,
      "mandates": [...]
    }
  ]
}

Block-Typen

Static Block

Für System-Seiten, die nicht an Feature-Instanzen gebunden sind.

{
  "type": "static",
  "id": "system",
  "title": "SYSTEM",
  "order": 10,
  "items": [
    {
      "uiComponent": "page.system.home",
      "uiLabel": "Übersicht",
      "uiPath": "/",
      "order": 10,
      "objectKey": "ui.system.home"
    },
    {
      "uiComponent": "page.system.settings",
      "uiLabel": "Einstellungen",
      "uiPath": "/settings",
      "order": 20,
      "objectKey": "ui.system.settings"
    }
  ]
}

Felder pro Item:

Feld Beschreibung
uiComponent Eindeutiger Code für UI-Mapping (z.B. page.system.home)
uiLabel Anzeigetext (bereits in der gewählten Sprache)
uiPath URL-Pfad für Navigation
order Sortierreihenfolge (Default oder User-Override)
objectKey Vollqualifizierter RBAC-Objektname (für Debugging/Referenz)

Dynamic Block

Für Feature-Instanzen, gruppiert nach Mandanten.

{
  "type": "dynamic",
  "id": "features",
  "title": "FEATURES",
  "order": 100,
  "mandates": [
    {
      "id": "5ce04434-c4ce-4269-9861-19ff7ebc2a1d",
      "uiLabel": "SOHA Treuhand AG",
      "order": 10,
      "features": [
        {
          "uiComponent": "feature.trustee",
          "uiLabel": "Treuhand",
          "order": 10,
          "instances": [
            {
              "id": "2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0",
              "uiLabel": "ValueOn AG 2026",
              "order": 10,
              "views": [
                {
                  "uiComponent": "page.feature.trustee.dashboard",
                  "uiLabel": "Übersicht",
                  "uiPath": "/mandates/5ce04434.../trustee/2fc48f66.../dashboard",
                  "order": 10,
                  "objectKey": "ui.feature.trustee.dashboard"
                },
                {
                  "uiComponent": "page.feature.trustee.positions",
                  "uiLabel": "Positionen",
                  "uiPath": "/mandates/5ce04434.../trustee/2fc48f66.../positions",
                  "order": 20,
                  "objectKey": "ui.feature.trustee.positions"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Hinweis: Der dynamic Block ist nur vorhanden, wenn der User mindestens eine Feature-Instanz hat.

Sortierung auf allen Ebenen:

  • Mandanten: mandates[].order
  • Features: features[].order
  • Instanzen: instances[].order
  • Views: views[].order

User-Personalisierung (Order Override)

Konzept

User können die Reihenfolge der Navigation anpassen. Die Einstellungen werden in den User-Settings gespeichert.

User Settings Struktur

{
  "userId": "user-123",
  "settings": {
    "navOrder": {
      "page.system.home": 10,
      "page.system.prompts": 5,
      "page.admin.users": 100,
      "feature.trustee": 20,
      "feature.realestate": 10,
      "mandate.5ce04434-...": 5,
      "instance.2fc48f66-...": 1
    }
  }
}

Merge-Logik im Gateway

Effektive Order = User-Override ?? Default-Order ?? 50
  1. Gateway lädt Default-Order aus Konfiguration
  2. Gateway lädt User-Settings (falls vorhanden)
  3. Für jedes Element: User-Order überschreibt Default
  4. Sortierung erfolgt im Gateway, UI erhält bereits sortiert

Sortier-Priorität

Ebene Key für navOrder Beispiel
Block Block-ID "system", "workflows", "features"
Static Item uiComponent "page.system.home"
Mandate mandate.{id} "mandate.5ce04434-..."
Feature uiComponent "feature.trustee"
Instance instance.{id} "instance.2fc48f66-..."
View uiComponent "page.feature.trustee.dashboard"

API für Order-Update

PUT /api/user/settings/navOrder
Body: { "page.system.prompts": 5, "feature.trustee": 20 }

Code-Konvention

Statische Seiten

page.system.<page-name>
page.admin.<page-name>

Beispiele:

  • page.system.home
  • page.system.settings
  • page.system.prompts
  • page.admin.users
  • page.admin.mandates

Feature-Seiten

page.feature.<feature-code>.<view-name>

Beispiele:

  • page.feature.trustee.dashboard
  • page.feature.trustee.positions
  • page.feature.trustee.instance-roles
  • page.feature.realestate.projects

Feature-Codes (für Gruppierung)

feature.<feature-code>

Beispiele:

  • feature.trustee
  • feature.realestate
  • feature.chatworkflow

ObjectKey-Konvention (RBAC)

Die objectKey werden für Access Rules verwendet und sind vollqualifiziert:

UI Objekte

ui.system.<name>          → System-Seiten
ui.admin.<name>           → Admin-Seiten
ui.feature.<code>.<view>  → Feature-Views

DATA Objekte

data.system.<table>           → System-Tabellen
data.feature.<code>.<table>   → Feature-Tabellen

RESOURCE Objekte

resource.system.<action>              → System-Aktionen
resource.feature.<code>.<action>      → Feature-Aktionen

UI Mapping & Fehlerbehandlung

UI Code Registry

Das UI definiert ein Mapping von Codes zu Komponenten:

// frontend_nyla/src/config/pageRegistry.ts

export const PAGE_REGISTRY: Record<string, PageDefinition> = {
  // Static pages
  'page.system.home': {
    component: HomePage,
    icon: FaHome,
    layout: 'default',
  },
  'page.system.settings': {
    component: SettingsPage,
    icon: FaCog,
    layout: 'default',
  },
  'page.admin.users': {
    component: AdminUsersPage,
    icon: FaUsers,
    layout: 'admin',
  },
  
  // Feature pages
  'page.feature.trustee.dashboard': {
    component: TrusteeDashboardView,
    icon: FaChartBar,
    layout: 'feature',
  },
  'page.feature.trustee.positions': {
    component: TrusteePositionsView,
    icon: FaListAlt,
    layout: 'feature',
  },
  // ...
};

export const FEATURE_REGISTRY: Record<string, FeatureDefinition> = {
  'feature.trustee': {
    icon: FaBriefcase,
    color: '#e74c3c',
  },
  'feature.realestate': {
    icon: FaBuilding,
    color: '#3498db',
  },
};

Fehlerbehandlung

Wenn ein uiComponent Code nicht gemappt werden kann:

function renderNavItem(item: NavItem) {
  const pageDef = PAGE_REGISTRY[item.uiComponent];
  
  if (!pageDef) {
    // Fehler sichtbar machen!
    return (
      <NavError>
        ⚠️ Unbekannter Code: {item.uiComponent}
        <small>ObjectKey: {item.objectKey}</small>
      </NavError>
    );
  }
  
  return (
    <NavLink to={item.uiPath} icon={pageDef.icon}>
      {item.uiLabel}
    </NavLink>
  );
}

Wichtig: Fehler werden im Nav-Tree angezeigt, nicht versteckt! So werden Synchronisationsprobleme zwischen Gateway und UI sofort sichtbar.

Sortierung im UI

Das UI sortiert die bereits vom Gateway sortierten Elemente NICHT neu. Die Sortierung erfolgt ausschliesslich im Gateway basierend auf:

  1. Default-Order aus Konfiguration
  2. User-Override aus Settings (falls vorhanden)
// UI rendert in der Reihenfolge wie empfangen
function renderBlock(block: Block) {
  // items sind bereits sortiert
  return block.items.map(item => renderNavItem(item));
}

Implementierungsschritte

Phase 1: Gateway - Navigation Endpoint

  1. Neuer Endpoint /api/navigation erstellen
  2. Static Blocks aus NAVIGATION_SECTIONS generieren
  3. Dynamic Block aus Feature-Instanzen generieren
  4. Permission-Filter anwenden (nur sichtbare Elemente)
  5. uiComponent Codes gemäß Konvention setzen
  6. Default-Order für alle Elemente setzen

Phase 2: Gateway - Access Rules

  1. TEMPLATE_ROLES auf vollqualifizierte objectKey umstellen
  2. Permission-Check auf objectKey umstellen
  3. Migration bestehender Access Rules (falls nötig)

Phase 3: Gateway - User Settings

  1. User Settings Modell erweitern mit navOrder
  2. API Endpoint PUT /api/user/settings/navOrder erstellen
  3. Merge-Logik implementieren (User-Override → Default)
  4. Sortierung vor Response durchführen

Phase 4: Frontend

  1. PAGE_REGISTRY erstellen mit uiComponent → Component Mapping
  2. FEATURE_REGISTRY erstellen mit Feature-Code → Style Mapping
  3. useNavigation Hook auf neuen Endpoint umstellen
  4. MandateNavigation refactoren für neue Datenstruktur
  5. Fehlerhandling für unbekannte uiComponents implementieren

Phase 5: Frontend - Drag & Drop (Optional)

  1. Drag & Drop UI für Nav-Tree Sortierung
  2. API Call bei Order-Änderung
  3. Optimistic Update für schnelle UX

Phase 6: Cleanup

  1. FEATURE_REGISTRY (alte Version in mandate.ts) entfernen
  2. Hardcodierte Navigation im Frontend entfernen
  3. Tests für Synchronisation Gateway ↔ UI

Beispiel: Vollständige Response

{
  "language": "de",
  "blocks": [
    {
      "type": "static",
      "id": "system",
      "title": "SYSTEM",
      "order": 10,
      "items": [
        {
          "uiComponent": "page.system.home",
          "uiLabel": "Übersicht",
          "uiPath": "/",
          "order": 10,
          "objectKey": "ui.system.home"
        },
        {
          "uiComponent": "page.system.settings",
          "uiLabel": "Einstellungen",
          "uiPath": "/settings",
          "order": 20,
          "objectKey": "ui.system.settings"
        }
      ]
    },
    {
      "type": "static",
      "id": "workflows",
      "title": "WORKFLOWS",
      "order": 20,
      "items": [
        {
          "uiComponent": "page.system.playground",
          "uiLabel": "Chat Playground",
          "uiPath": "/workflows/playground",
          "order": 10,
          "objectKey": "ui.system.playground"
        },
        {
          "uiComponent": "page.system.chats",
          "uiLabel": "Chats",
          "uiPath": "/workflows/list",
          "order": 20,
          "objectKey": "ui.system.chats"
        }
      ]
    },
    {
      "type": "static",
      "id": "basedata",
      "title": "BASISDATEN",
      "order": 30,
      "items": [
        {
          "uiComponent": "page.system.prompts",
          "uiLabel": "Prompts",
          "uiPath": "/basedata/prompts",
          "order": 10,
          "objectKey": "ui.system.prompts"
        },
        {
          "uiComponent": "page.system.files",
          "uiLabel": "Dateien",
          "uiPath": "/basedata/files",
          "order": 20,
          "objectKey": "ui.system.files"
        }
      ]
    },
    {
      "type": "static",
      "id": "admin",
      "title": "ADMINISTRATION",
      "order": 200,
      "items": [
        {
          "uiComponent": "page.admin.users",
          "uiLabel": "Benutzer",
          "uiPath": "/admin/users",
          "order": 10,
          "objectKey": "ui.admin.users"
        },
        {
          "uiComponent": "page.admin.mandates",
          "uiLabel": "Mandanten",
          "uiPath": "/admin/mandates",
          "order": 20,
          "objectKey": "ui.admin.mandates"
        }
      ]
    },
    {
      "type": "dynamic",
      "id": "features",
      "title": "MEINE FEATURES",
      "order": 100,
      "mandates": [
        {
          "id": "5ce04434-c4ce-4269-9861-19ff7ebc2a1d",
          "uiLabel": "SOHA Treuhand AG",
          "order": 10,
          "features": [
            {
              "uiComponent": "feature.trustee",
              "uiLabel": "Treuhand",
              "order": 10,
              "instances": [
                {
                  "id": "2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0",
                  "uiLabel": "ValueOn AG 2026",
                  "order": 10,
                  "views": [
                    {
                      "uiComponent": "page.feature.trustee.dashboard",
                      "uiLabel": "Übersicht",
                      "uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/dashboard",
                      "order": 10,
                      "objectKey": "ui.feature.trustee.dashboard"
                    },
                    {
                      "uiComponent": "page.feature.trustee.positions",
                      "uiLabel": "Positionen",
                      "uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/positions",
                      "order": 20,
                      "objectKey": "ui.feature.trustee.positions"
                    },
                    {
                      "uiComponent": "page.feature.trustee.documents",
                      "uiLabel": "Dokumente",
                      "uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/documents",
                      "order": 30,
                      "objectKey": "ui.feature.trustee.documents"
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          "id": "abc123-mandate-2",
          "uiLabel": "Partner AG",
          "order": 20,
          "features": [
            {
              "uiComponent": "feature.realestate",
              "uiLabel": "Immobilien",
              "order": 10,
              "instances": [
                {
                  "id": "def456-instance",
                  "uiLabel": "Objektstudien ZH",
                  "order": 10,
                  "views": [
                    {
                      "uiComponent": "page.feature.realestate.dashboard",
                      "uiLabel": "Übersicht",
                      "uiPath": "/mandates/abc123-mandate-2/realestate/def456-instance/dashboard",
                      "order": 10,
                      "objectKey": "ui.feature.realestate.dashboard"
                    },
                    {
                      "uiComponent": "page.feature.realestate.projects",
                      "uiLabel": "Projekte",
                      "uiPath": "/mandates/abc123-mandate-2/realestate/def456-instance/projects",
                      "order": 20,
                      "objectKey": "ui.feature.realestate.projects"
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Vorteile dieses Ansatzes

  1. Single Source of Truth: Gateway definiert, was sichtbar ist
  2. Keine Permission-Logik im UI: Alles was kommt, wird gerendert
  3. Flexibles UI: Verschiedene UIs können dieselben Daten anders darstellen
  4. Fehlertoleranz: Synchronisationsprobleme werden sofort sichtbar
  5. Saubere Trennung: Daten (Gateway) vs. Darstellung (UI)
  6. Testbarkeit: API-Response kann einfach getestet werden
  7. Dokumentation: Codes und ObjectKeys sind selbstdokumentierend
  8. Personalisierung: User können Navigation individuell sortieren
  9. Konsistente Sortierung: Gateway sortiert zentral, UI respektiert die Reihenfolge

Offene Fragen (Beantwortet)

  1. Soll der dynamic Block vor oder nach den static Blocks kommen? → Diese initiale Reihenfolge: System, <dynamic>, workflows, basisdaten, migrate to features, administration
  2. Braucht es eine Versionierung der API für Breaking Changes? → Nein
  3. Wie werden Feature-spezifische Sub-Navigationen behandelt (z.B. Tabs innerhalb einer View)? → Tabs innerhalb einer View werden als separate Views behandelt
  4. Soll der Default-Order-Abstand 10 sein (erlaubt Einfügen zwischen Elementen)? → Ja
  5. Soll die Order-Personalisierung auch für Admins pro Mandate/Feature-Instanz möglich sein (System-Default vs. User-Override)? → Ja

Code-Analyse: IST-Zustand

Gateway (Backend)

1. mainSystem.py - Statische Navigation

# IST: Items haben id, label, path, icon, objectKey
# NEU: Brauchen uiComponent, uiLabel, uiPath, order (ohne icon)
NAVIGATION_SECTIONS = [
    {
        "id": "system",
        "order": 10,  # ✓ Vorhanden
        "items": [
            {
                "id": "home",
                "objectKey": "ui.system.home",  # ✓ Korrekt
                "label": {"en": "Home", "de": "Übersicht"},
                "icon": "FaHome",  # ✗ Muss entfernt werden
                "path": "/",
            },
        ]
    }
]

Änderungen:

  • icon entfernen (UI mappt selbst)
  • Item-Level order hinzufügen
  • Felder umbenennen für API Response

2. routeSystem.py - Navigation Endpoint

# IST: GET /api/system/navigation
# Gibt zurück: { "sections": [...], "language": "de" }

# NEU: GET /api/navigation
# Soll zurückgeben: { "blocks": [...], "language": "de" }

Änderungen:

  • Neuer kombinierter Endpoint
  • Static Blocks + Dynamic Block zusammenführen
  • Response-Struktur gemäss Konzept

3. mainTrustee.py - Feature Views

# IST: TEMPLATE_ROLES verwendet Kurznamen
TEMPLATE_ROLES = [
    {
        "roleLabel": "trustee-client",
        "accessRules": [
            {"context": "UI", "item": "dashboard", "view": True},     # ✗ Kurzname!
            {"context": "UI", "item": "positions", "view": True},     # ✗ Kurzname!
        ]
    }
]

# ABER: UI_OBJECTS verwendet vollqualifizierte Namen
UI_OBJECTS = [
    {"objectKey": "ui.feature.trustee.dashboard", ...},  # ✓ Vollständig
]

Inkonsistenz gefunden! item in AccessRules muss objectKey entsprechen.

4. routeAdminFeatures.py - Permission Logic

# IST: _deriveViewPermissions ist TRUSTEE-spezifisch!
def _deriveViewPermissions(permissions):
    # Hard-coded: TrusteePosition → positions view
    # Hard-coded: TrusteeDocument → documents view

Problem: Nicht generisch, funktioniert nur für Trustee.

Frontend (UI)

1. MandateNavigation.tsx

// IST: Zwei separate Datenquellen
const { sections } = useNavigation();  // Static von API
const mandates = useMandates();         // Dynamic von API

// IST: FEATURE_ICONS definiert im Frontend
const FEATURE_ICONS = { trustee: <FaBriefcase /> };

Problem: Navigation wird im Frontend zusammengebaut, nicht vom Backend.

2. mandate.ts - FEATURE_REGISTRY

// IST: Doppelte Definition von Views!
export const FEATURE_REGISTRY = {
  trustee: {
    views: [
      { code: 'dashboard', label: {...}, path: 'dashboard' },  // ✗ Kurzname
      { code: 'positions', label: {...}, path: 'positions' },  // ✗ Kurzname
    ]
  }
};

Duplizierung: Views sind in mainTrustee.py UND mandate.ts definiert!


Impact-Analyse

Phase 1: Gateway - Navigation Endpoint

Datei Änderungstyp Aufwand Breaking
routeSystem.py Neu/Refactor Hoch Ja
mainSystem.py Update Mittel Nein
routeAdminFeatures.py Integrieren Hoch Nein

Konkrete Aufgaben:

  1. Neuen Endpoint GET /api/navigation erstellen
  2. Static Blocks aus NAVIGATION_SECTIONS generieren
  3. Dynamic Block aus _getMyFeatureInstances-Logik generieren
  4. Permission-Filter auf beide anwenden
  5. icon aus Response entfernen
  6. Felder umbenennen: label→uiLabel, path→uiPath, neu uiComponent
  7. order auf Item-Level hinzufügen

Phase 2: Gateway - Access Rules Migration

Datei Änderungstyp Aufwand Breaking
mainTrustee.py Update Mittel Ja (DB)
mainRealEstate.py Update Mittel Ja (DB)
Migration Script Neu Mittel -

Konkrete Aufgaben:

  1. TEMPLATE_ROLES.item auf vollqualifizierte ObjectKeys umstellen
  2. Migration: Bestehende AccessRules in DB aktualisieren
  3. Permission-Check in _checkUiPermission anpassen

Phase 3: Gateway - User Settings

Datei Änderungstyp Aufwand Breaking
datamodelUam.py Erweitern Gering Nein
routeSystem.py Endpoint Gering Nein

Konkrete Aufgaben:

  1. UserSettings Tabelle oder JSON-Feld in User
  2. PUT /api/user/settings/navOrder Endpoint
  3. Merge-Logik in Navigation-Endpoint

Phase 4: Frontend

Datei Änderungstyp Aufwand Breaking
pageRegistry.ts Neu Hoch -
useNavigation.ts Refactor Mittel Ja
MandateNavigation.tsx Refactor Hoch Ja
mandate.ts Cleanup Gering Ja

Konkrete Aufgaben:

  1. PAGE_REGISTRY erstellen mit uiComponent → Component Mapping
  2. FEATURE_REGISTRY mit feature.* Codes
  3. useNavigation für neuen Endpoint anpassen
  4. MandateNavigation für Blocks-Struktur refactoren
  5. Error-Handling für unbekannte uiComponents

Kritische Punkte

1. Breaking Changes

Die Umstellung erfordert simultane Änderungen in Gateway UND Frontend. Empfehlung:

  • Feature-Flag für neuen Endpoint
  • Alte Endpoints temporär behalten
  • Frontend schrittweise migrieren

2. Datenbank-Migration

Bestehende AccessRules haben item="dashboard", neu soll item="ui.feature.trustee.dashboard" sein.

-- Beispiel-Migration
UPDATE access_rules 
SET item = 'ui.feature.trustee.' || item 
WHERE role_id IN (SELECT id FROM roles WHERE feature_code = 'trustee')
  AND context = 'UI' 
  AND item IS NOT NULL;

3. Performance

Navigation mit vielen Instanzen kann langsam werden. Empfehlungen:

  • Response cachen pro User
  • Cache invalidieren bei Permission-Änderungen
  • Lazy-Loading für sehr grosse Deployments prüfen

4. Feature-spezifische View-Ableitung

_deriveViewPermissions ist Trustee-spezifisch. Optionen:

  • A) Jedes Feature definiert seine Ableitungslogik
  • B) Keine Ableitung, nur explizite UI-Rules
  • C) Generische Mapping-Konfiguration pro Feature

Empfehlung: Option B - Explizite UI-Rules sind klarer und testbarer.


Empfohlene Reihenfolge

  1. Schritt 1: PAGE_REGISTRY im Frontend erstellen (unabhängig)
  2. Schritt 2: Neuen /api/navigation Endpoint mit Feature-Flag
  3. Schritt 3: Frontend auf neuen Endpoint umstellen
  4. Schritt 4: AccessRules-Migration
  5. Schritt 5: User-Settings für Order
  6. Schritt 6: Alte Endpoints/Code entfernen