NeXus LLD — Low Level Design
AS-2 Mandatory Documentation — v1.1 — 2026-05-05 — REQ-ARCH-004
Ten dokument opisuje wszystkie metody JavaScript/Python, ich relacje i przepływy danych przez cztery główne ekrany systemu NeXus: Login, Hub Dashboard, Orkiestrator, Wejście do aplikacji.
1. Architektura systemu
NeXus to single-page application (SPA) serwowana przez FastAPI (Wormwood). Warstwa UI to jeden plik HTML (NEXUS_APP.html) z osadzonym CSS i JavaScript. Komunikacja z API odbywa się przez REST.
| Warstwa | Technologia | Ścieżka | Port |
| Frontend SPA | Vanilla JS + HTML | docs/NEXUS_APP.html | — |
| Orkiestrator | Vanilla JS + HTML | docs/WORMWOOD_APP.html | — |
| API Backend | FastAPI + SQLAlchemy | wormwood/api/ | 8012 |
| Static Files | FastAPI FileResponse | /arc-ui/ lub /nexus-ui/ | 8012 |
| Admin UI | Flask + Flask-Admin | /admin/ | 8012 |
2. Stan globalny (S)
Obiekt S przechowuje stan całej sesji użytkownika:
var S = {
apiBase: '', // URL serwera API (np. http://localhost:8011)
apiKey: '', // Klucz API dla aktywnej org
orgSlug: '', // Slug aktywnej aplikacji (np. 'arc')
pipelines:{}, // Słownik pipeline'ów: { slug: pipelineObj }
navStack: [], // Stos nawigacyjny widoków (back button)
currentPipelineId: null // ID aktualnie uruchomionego pipeline'u
};
3. Ekran 1: Login
Elementy HTML
| ID | Typ | Opis |
#login-screen | div | Kontener ekranu logowania. Ukryty po zalogowaniu. |
#li-email | input | Email użytkownika |
#li-base | input | URL silnika — ukryty (display:none), zachowany dla kompatybilności |
#li-btn | button | Przycisk "Sign In" — wywołuje doHubLogin() |
#li-status | div | Komunikat statusu/błędu logowania |
Metody JavaScript
| Metoda | Sygnatura | Opis |
doHubLogin() | async | Główny punkt wejścia. Pobiera email z #li-email. Wywołuje GET /orgs/user-hub?email=X. Fallback: GET /orgs/login-list. Po sukcesie: ukrywa #login-screen, pokazuje #hub-screen, wywołuje renderHubCards() i initHubWidgets(). |
doLogin() | async | Stara ścieżka direct-connect (legacy). Używa #li-org i #li-base. Zachowana w kodzie ale nie wywoływana z UI. |
loginStatus(msg, isErr) | sync | Ustawia tekst i klasę CSS #li-status. |
loadSaved() | sync | Odczytuje localStorage 'nexus-login', przywraca base URL do #li-base. |
saveLogin(org, key, base) | sync | Zapisuje org/key/base do localStorage 'nexus-login'. |
autoLoadOrgs() | async | Pobiera listę org z /orgs/login-list i wypełnia #li-org (select). Zachowana dla kompatybilności. |
Przepływ danych: Login
1
Użytkownik klika "Sign In"
onclick="doHubLogin()" lub Enter keydown → doHubLogin()
↓
2
GET /orgs/user-hub?email=X
Zwraca [{org_slug, org_name, key, role, pipeline_count, ...}]. Jeśli 4xx → fallback do /orgs/login-list.
↓
3
Deduplikacja org po slug
hubData.filter() by unique org_slug
↓
4
Przejście do Hub
#login-screen.hidden=true, #hub-screen.hidden=false, #hub-user-email.textContent=email
↓
5
renderHubCards(hubData) + initHubWidgets()
Renderuje karty aplikacji. Inicjuje health bar, RAID log, roadmapy.
4. Ekran 2: Hub Dashboard
Elementy HTML
| ID | Opis |
#hub-screen | Kontener hub dashboard. Ukryty przed logowaniem i po wejściu do aplikacji. |
#hub-cards | Kontener kart aplikacji. Wypełniany przez renderHubCards(). |
#hub-search | Pole wyszukiwania kart — oninput="filterHubCards(this.value)" |
#hub-section-title-text | Tytuł sekcji kart — "Twoje aplikacje" / "Your Applications" (i18n) |
#hub-section-hint | Podpowiedź — "Kliknij kartę, aby otworzyć" (i18n) |
#hub-sort-alpha / #hub-sort-status | Przyciski sortowania A-Z / Status |
#hub-health-detail | Szczegóły statusu silnika (rules count, version) |
#hub-footer-components | Chips komponentów w footer (z wersją i [deployment]) |
#hub-raid | Kontener elementów RAID log |
#hub-roadmap-list | Lista roadmap (z API /omnissiah/roadmaps) |
#hub-lang-toggle | Przycisk PL/EN — onclick="toggleLang()" |
Metody JavaScript — renderowanie hub
| Metoda | Sygnatura | Opis |
renderHubCards(data, skipSave) | sync | Czyści #hub-cards, iteruje po data, wywołuje renderHubCard() dla każdej org. Jeśli !skipSave, zapisuje do window._lastHubData. Sortuje wg bieżącego kryterium. |
renderHubCard(o, uc) | sync → string | Zwraca HTML jednej karty. Renderuje: badge roli, status, use cases, akcje (usecase actions), docs/orchestrator link, pills komponentów. validComps filtruje komponent bez wersji. |
filterHubCards(query) | sync | Iteruje po dzieciach #hub-cards, ukrywa karty nie pasujące do query (szuka po name/domain/desc/slug case-insensitive). |
sortHubCards(by) | sync | Sortuje karty w #hub-cards (in-place DOM sort). by: 'alpha' lub 'status'. |
initHubWidgets() | sync | Wywołuje: renderHealthBar(), fetchFooterVersions(), renderRaidLog(), renderRoadmaps(). |
renderHealthBar() | async | GET /health, aktualizuje #hub-health-detail i status dot. Wywołuje fetchFooterVersions() jeśli dostępne component_versions. |
fetchFooterVersions() | async | GET /health, renderuje chipa w #hub-footer-components. Format: "Name vX.X [env]". Bez emoji. |
renderRaidLog() | async | GET /omnissiah/raid (requires auth). Fallback: _raidStaticFallback (8 statycznych wpisów). Renderuje .hub-raid-item w #hub-raid. Filtrowanie przez .hub-raid-filter buttons. |
renderRoadmaps() | async | GET /omnissiah/roadmaps. Fallback: statyczny komunikat. Renderuje do #hub-roadmap-list. |
toggleLang() | sync | Przełącza _lang między 'pl' i 'en'. Aktualizuje #hub-lang-toggle, #hub-section-title-text, #hub-section-hint, placeholder #hub-search. Zapisuje do localStorage 'na-lang'. |
t(key) | sync → string | Accessor i18n. Zwraca _i18n[_lang][key] || key. |
Metody JavaScript — nawigacja hub
| Metoda | Opis |
navigateToManagement(orgData) | sync | Zapisuje sessionStorage['na-session-skin'] z preferencji użytkownika (na-hub-skin || na-skin), następnie otwiera APP_MGMT.html?org=SLUG (bez ?skin= w URL). Nowy punkt wejścia po kliknięciu karty (REQ-MGMT-001). Zastępuje bezpośrednie wywołanie enterWorkspace(). |
enterWorkspace(orgData) | async | Ustawia S.apiKey, S.orgSlug. Ukrywa #hub-screen, pokazuje #app-shell. GET /pipelines?org_slug=X, próbuje nav-menu pipeline. Wywołuje renderSidebar(navConfig), a następnie automatycznie ładuje pierwszą pozycję nav z pipeline_slug (loadView). Jeśli brak nav_config: renderDefaultDashboard(). Wywołuje renderGmailStatusWidget(orgSlug). Wywoływane z APP_MGMT.html (przycisk Open App), nie bezpośrednio z kart hub. |
goBackToHub() | sync. Ukrywa #app-shell, pokazuje #hub-screen. Re-renderuje karty z window._lastHubData. Wywołuje renderHealthBar(), renderRoadmaps(), renderRaidLog(). |
doHubLogout() | sync. Prawdziwy logout: pokazuje #login-screen, ukrywa #hub-screen i #app-shell. Czyści karty. |
toggleProfileDropdown() | sync. Pokazuje/ukrywa #hub-profile-dropdown. |
Przepływ danych: Wejście do aplikacji z Hub
1
Kliknięcie karty aplikacji
onclick na .hub-card → zapisuje sessionStorage['na-session-skin'] = localStorage['na-hub-skin'] || localStorage['na-skin'] || 'nexus-dark', następnie navigateToManagement(orgData). Otwiera APP_MGMT.html?org=SLUG (bez ?skin= w URL). (REQ-MGMT-001)
↓
2
GET /pipelines?org_slug=X
Pobiera wszystkie pipeline'y organizacji. Zapisuje do S.pipelines.
↓
3
GET nav-menu pipeline (opcjonalnie)
Próbuje pipeline {org_slug}-nav-menu. Jeśli istnieje: executePipeline() → nav_config.
↓
4
renderSidebar(navConfig) + auto-load pierwszej pozycji
Jeśli nav_config: applyTheme(), renderSidebar() (normalizuje format sections i items), następnie setActiveNav() + loadView() dla pierwszej pozycji z pipeline_slug. Jeśli nie: default sidebar + renderDefaultDashboard().
↓
5
renderGmailStatusWidget(orgSlug)
Dla arc: GET /orgs/arc/gmail/status. Pokazuje dot + przycisk "Połącz".
5. Ekran 3: App Shell (Workspace)
Elementy HTML
| ID | Opis |
#app-shell | Główny kontener workspace. hidden=true do czasu wejścia. |
#sidebar | Sidebar nawigacyjny. Wypełniany przez renderSidebar(). |
#sidebar-nav | Element nav wewnątrz sidebara — tutaj wstrzykiwane są elementy menu. |
#app-org-name | Nazwa aktywnej aplikacji (navConfig.app_title || orgSlug) |
#content-area | Główny obszar treści — tutaj renderowane są widoki pipeline'ów. |
#ws-fab | Floating Action Button "N" — fabAction() |
#ws-fab-menu | Rozwijane menu FAB: Hub, Orkiestrator, Logi, Motyw. |
#run-panel | Panel logów uruchomień (M-NEXUS-E3-3). |
#gmail-status-widget | Widget statusu Gmail (hidden dla org != arc). |
Metody JavaScript — App Shell
| Metoda | Opis |
renderSidebar(navConfig) | Czyści #sidebar-nav. Normalizuje dwa formaty nav_config: navConfig.items[] (format płaski) i navConfig.sections[{section,items[]}] (format sekcji). Grupuje pozycje wg section. Tworzy przyciski nawigacyjne. Kliknięcie pozycji → loadView(item.pipeline_slug) jeśli ma pipeline_slug, inaczej disabled. |
loadView(item) | async. Wykonuje pipeline dla item.pipeline_slug. Wywołuje renderView() lub renderGateView() w zależności od statusu. |
executePipeline(pipelineId, inputs) | async. POST /pipelines/{id}/execute. Zwraca {status, node_outputs}. |
renderView(outputs, graph, pipeline) | async. Tworzy wrap div. Renderuje FormNode/DashboardNode/ListViewNode. Fallback: jeśli puste outputs → komunikat PL. Jeśli nieznany typ → raw JSON. |
renderGateView(status, outputs, pipelineId) | async. Dla awaiting_approval: przyciski Zatwierdź/Odrzuć → _gateDecision(). Dla awaiting_human: formularz z human_input_schema → _gateHumanInput(). Dla revision_requested: edycja poprzednich danych. |
renderDefaultDashboard(orgSlug) | sync. Renderuje statyczny dashboard z listą pipeline'ów z S.pipelines. Tekst PL. |
renderGmailStatusWidget(orgSlug) | async. GET /orgs/arc/gmail/status. Aktualizuje #gmail-status-dot i #gmail-status-text. Pokazuje przycisk "Połącz" jeśli !connected. |
reconnectGmail() | sync. window.open() do /orgs/arc/gmail/auth. Po 3s wywołuje ponownie renderGmailStatusWidget(). |
fabAction(action) | sync. 'hub': goBackToHub(). 'orchestrator': otwiera WORMWOOD_APP.html?org=X&skin=Y. 'logs': runLogPanel.open(). 'skin': cyklicznie zmienia data-skin na html. |
showFab() / hideFab() | sync. Pokazuje/ukrywa #ws-fab. |
setNode(el) | sync. Wstrzykuje element do #content-area. Dodaje do S.navStack. |
doBack() | sync. Cofa widok przez S.navStack. |
6. Ekran 4: Orkiestrator (WORMWOOD_APP.html)
Inicjalizacja i skin
1
Odczyt skinu — sessionStorage
?org=SLUG → przekazany do selectOrg(). Skin: czyta sessionStorage['na-session-skin'] (zapisany przez hub przed nawigacją). Mapuje na nexus-theme w localStorage przed załadowaniem nexus-core.js. Parametr URL ?skin= nie jest używany.
↓
2
nexus-core.js: initTheme()
Odczytuje localStorage 'nexus-theme'. Stosuje data-theme na body. Binduje .theme-swatch kliknięcia.
↓
3
DOMContentLoaded → renderCards() + selectOrg(orgParam)
Jeśli ?org=X: selectOrg() ukrywa #uc-dashboard, ładuje pipeline'y org. Jeśli nie: showDashboard() pokazuje karty use case.
Metody JS (WORMWOOD_APP.html + nexus-core.js)
| Metoda | Plik | Opis |
selectOrg(slug) | WORMWOOD_APP | Szuka slug w #sel-org options. Jeśli znaleziony: sel.value=slug, dispatchEvent(change), hideDashboard(). Jeśli nie: GET /pipelines?org_slug=X, dodaje do #sel-org, wypełnia #sel-pipeline. |
renderCards() | WORMWOOD_APP | Renderuje karty USE_CASES w #ucd-grid. Pobiera pipeline count dla każdego aktywnego case. |
showDashboard() / hideDashboard() | WORMWOOD_APP | Toggleuje klasę 'hidden' na #uc-dashboard. |
applyTheme(t) | nexus-core.js | Ustawia/usuwa data-theme na body. Aktualizuje .theme-swatch active states. Zapisuje do localStorage 'nexus-theme'. |
7. API Backend — kluczowe endpointy
Org & Auth
| Endpoint | Metoda | Plik | Opis |
/orgs/user-hub?email=X | GET | routes_orgs.py | Zwraca tablicę org przypisanych do email: [{org_slug, org_name, key, role, pipeline_count, usecase: {}}] |
/orgs/login-list | GET | routes_orgs.py | Lista wszystkich org (dev fallback): [{slug, name, key, pipeline_count}] |
/orgs/authenticate | POST | routes_orgs.py | Body: {email, org_slug}. Zwraca {key, display_name}. |
/omnissiah/raid | GET | routes_orgs.py | Lista wpisów RAID log. Wymaga auth. |
/omnissiah/roadmaps | GET | routes_orgs.py | Lista roadmap projektów. Wymaga auth. |
Pipeline — CRUD & Wykonanie
| Endpoint | Metoda | Plik | Opis |
/pipelines?org_slug=X | GET | routes_pipelines.py | Lista pipeline'ów dla org. Wymaga X-API-Key. |
/pipelines/{id} | GET | routes_pipelines.py | Pełne dane pipeline (graph, config, status, base_slug). |
/pipelines/{id} | PUT | routes_pipelines.py | Aktualizuje pipeline. Zwraca zaktualizowany obiekt. |
/pipelines/{id}/execute | POST | routes_pipelines.py | Wykonuje pipeline. Wymaga status=active (422 jeśli nie). Zwraca {status, node_outputs, run_id}. |
/pipelines/{id}/trigger-run | POST | routes_pipelines.py | Wyzwala run. Parametr force=true omija guard dla draft (dev bypass). |
Pipeline — Wersjonowanie & Cykl życia (REQ-VER, REQ-DESC — 2026-05-05)
| Endpoint | Metoda | Plik | Opis |
/pipelines/{id}/publish | POST | routes_pipelines.py | Przechodzi draft→active. Archiwizuje bieżącą aktywną wersję tego samego base_slug. 409 jeśli już active. |
/pipelines/{id}/deactivate | POST | routes_pipelines.py | Przechodzi active→archived. 400 jeśli nie active. |
/pipelines/{id}/restore | POST | routes_pipelines.py | Klonuje archived jako nowy draft z PATCH-bumped version. Zwraca nowy pipeline. |
/pipelines/{id}/fork | POST | routes_pipelines.py | Klonuje dowolny pipeline jako nowy draft z PATCH-bumped version. |
/pipelines/history?org_slug=X&base_slug=Y | GET | routes_pipelines.py | Wszystkie wersje pipeline'u pogrupowane po base_slug. Posortowane malejąco po wersji. |
/pipelines/{id}/descriptor | GET | routes_pipelines.py | Eksportuje descriptor JSON v1.0 z nagłówkiem Content-Disposition (plik do pobrania). Pola: descriptor_version, slug, name, version, domain, status, description, graph, style_tokens, exported_at, exported_by. |
/pipelines/import | POST | routes_pipelines.py | Importuje descriptor JSON. Body: {descriptor, target_org_slug}. Kolizja slug → dodaje -imported-{timestamp}. Zawsze importuje jako draft. |
Infrastruktura
| Endpoint | Metoda | Plik | Opis |
/health/ | GET | routes_health.py | Status silnika: {status, version, rules_loaded, component_versions[]}. |
/health/resources | GET | routes_health.py | Metryki zasobów: CPU, RAM, disk. |
/arc-ui/{file_path} | GET | app.py | Serwuje pliki z docs/. Historyczny prefix. |
/nexus-ui/{file_path} | GET | app.py | Neutral alias dla docs/. REQ-ARCH-003. |
7b. Środowiska i zarządzanie aplikacją (REQ-ENV, REQ-MGMT)
Nowy ekran APP_MGMT.html — punkt nawigacji między hubem a aplikacją (REQ-MGMT-001). Serwuje jako centrum zarządzania środowiskami.
Elementy HTML (APP_MGMT.html)
| ID | Opis |
#mgmt-header | Nagłówek: org name, env selector, health dot |
#mgmt-env-list | Panel środowisk — lista/siatka wszystkich envs z statusem, wersją, przyciskami akcji |
#mgmt-pipeline-versions | Historia wersji pipeline dla tej org (REQ-VER-002) |
#mgmt-backups | Lista backupów wg środowisk |
#mgmt-settings | Ustawienia: edytor menu RMB, metadane org, klucze API |
#btn-open-app | CTA otwierający live aplikację dla wybranego środowiska |
#btn-back-hub | Powrót do hubu |
Metody JavaScript (APP_MGMT.html)
| Metoda | Sygnatura | Opis |
loadMgmtPage(orgSlug, envSlug) | async | Inicjalizacja strony. GET /orgs/{org_slug}/environments. Renderuje listę środowisk. Wywołuje loadEnvDetail(envSlug || 'production'). |
loadEnvDetail(envSlug) | async | GET /orgs/{org_slug}/environments/{env_slug}. Aktualizuje health dot, wersję, przyciski akcji. |
envAction(envSlug, action) | async | POST /orgs/{org_slug}/environments/{env_slug}/{action}. Pokazuje dialog potwierdzenia dla akcji destruktywnych (stop, downgrade na production). Wywołuje loadEnvDetail() po sukcesie. |
spinUpEnv() | async | Otwiera dialog tworzenia środowiska. POST /orgs/{org_slug}/environments z {name, slug, base_version}. Odświeża listę środowisk. |
loadRmbEditor() | async | GET /orgs/{org_slug}/rmb-items. Renderuje edytor menu RMB z drag-drop (REQ-RMB-004). |
saveRmbItems(items) | async | PUT /orgs/{org_slug}/rmb-items. Zapisuje aktualny stan listy RMB. |
openApp(envSlug) | sync | window.open(app URL for env, '_blank'). Wywołuje enterWorkspace() po stronie NEXUS_APP.html. |
Metody JavaScript — RMB (NEXUS_APP.html)
| Metoda | Sygnatura | Opis |
initCardRmb() | sync | Binduje contextmenu event na każdej .hub-card. Wywołuje renderRmbMenu(orgData, e) na prawym kliknięciu. |
renderRmbMenu(orgData, event) | async | GET /orgs/{org_slug}/rmb-items (z cache). Renderuje menu z 4 default items + dynamic section. Pozycjonuje przy kursorze. Binduje dismiss (click-outside, Escape). |
rmbAction(item, orgData) | async | Wykonuje akcję RMB. Dla env_action: pokazuje inline mini-dialog potwierdzenia. Dla demo mode: uruchamia walkthrough bez wywołania API. Wynik jako toast notification. |
dismissRmbMenu() | sync | Usuwa #hub-rmb-menu z DOM. |
API — Zarządzanie środowiskami (REQ-ENV-004)
| Endpoint | Metoda | Plik | Opis |
/orgs/{org_slug}/environments | GET | routes_orgs.py | Lista wszystkich środowisk org. Zwraca [{slug, name, version, status, url, created_at, last_deployed_at}] |
/orgs/{org_slug}/environments | POST | routes_orgs.py | Tworzy nowe środowisko. Body: {name, slug, base_version, url}. |
/orgs/{org_slug}/environments/{env_slug} | GET | routes_orgs.py | Szczegóły środowiska + bieżący stan zdrowia. |
/orgs/{org_slug}/environments/{env_slug}/start | POST | routes_orgs.py | Uruchamia zatrzymane środowisko. |
/orgs/{org_slug}/environments/{env_slug}/stop | POST | routes_orgs.py | Zatrzymuje działające środowisko. |
/orgs/{org_slug}/environments/{env_slug}/restart | POST | routes_orgs.py | Stop + start. Dialog potwierdzenia dla production. |
/orgs/{org_slug}/environments/{env_slug}/backup | POST | routes_orgs.py | Snapshot danych i konfiguracji. Zwraca {backup_id, download_url, created_at}. |
/orgs/{org_slug}/environments/{env_slug}/upgrade | POST | routes_orgs.py | Body: {target_version}. Wdraża wyższą wersję. |
/orgs/{org_slug}/environments/{env_slug}/downgrade | POST | routes_orgs.py | Body: {target_version}. Cofa do wcześniejszej wersji. |
/orgs/{org_slug}/environments/{env_slug}/backups | GET | routes_orgs.py | Lista backupów dla środowiska. |
/orgs/{org_slug}/rmb-items | GET | routes_orgs.py | Lista dynamicznych pozycji RMB dla org. Zwraca [{label, action_type, action_target, icon, order, enabled}] |
/orgs/{org_slug}/rmb-items | PUT | routes_orgs.py | Pełna zamiana listy RMB. Body: [{...}]. Zwraca zaktualizowaną listę. |
8. System skinowania
8.1 Dostępne skiny (preferencja użytkownika)
| Skin | data-skin | Styl |
| NeXus Dark (domyślny) | nexus-dark | Ciemny, indigo accent |
| CodeZero | codezero | Głęboki granat, cyan accent |
| Office Light | office-light | Biały, niebieski accent |
| Apple Frost | apple-frost | Kremowy, szary accent |
| Glass Aurora | glass-aurora | Glassmorphism, backdrop-filter:blur(12px), green glow |
| Glass Ember | glass-ember | Glassmorphism, backdrop-filter:blur(12px), orange/red glow |
8.2 Dwa rodzaje skina — definicja
System rozróżnia dwa niezależne koncepty skina, które muszą być traktowane jako oddzielne dane:
| Rodzaj | Definicja | Źródło | Gdzie zapisywany |
| Preferencja użytkownika |
Skin wybrany przez użytkownika: skin picker w hub lub FAB akcja "Motyw". Jedna z 6 wartości w tabeli 8.1. |
Kliknięcie swatcha lub FAB cycle |
localStorage['na-skin'], localStorage['na-hub-skin'], sessionStorage['na-session-skin'] |
| Skin marki organizacji |
Skin wizualny przypisany do org (np. arc→'arc', plato→'plato'). Stosowany wyłącznie jako atrybut DOM podczas pobytu w workspace. NIE jest preferencją użytkownika. |
applyOrgSkin(orgSlug) → ORG_SKIN_MAP[orgSlug] |
Wyłącznie data-skin atrybut na <html>. Nigdy w localStorage ani sessionStorage. |
8.3 Klucze przechowywania
| Klucz | Zakres | Kto pisze | Kto czyta | Cel |
localStorage['na-skin'] |
Trwały (między sesjami) |
skin picker, FAB cycle, init IIFE z URL param (legacy) |
restoreHubSkin(), FOUC inline script |
Przywracanie skinu przy załadowaniu hub. Wartość ZAWSZE z grupy preferencji użytkownika. |
localStorage['na-hub-skin'] |
Trwały (między sesjami) |
skin picker, FAB cycle, init IIFE z URL param (legacy) |
FAB cycle (fabAction('skin')) jako baza dla cyklowania |
Izolowany klucz preferencji hub — zabezpieczony przed nadpisaniem przez skiny organizacji. FAB używa tego klucza, nie data-skin. |
sessionStorage['na-session-skin'] |
Tab-scoped (bieżąca karta przeglądarki) |
Hub — przed KAŻDĄ nawigacją do APP_MGMT.html lub WORMWOOD_APP.html |
APP_MGMT.html i WORMWOOD_APP.html przy starcie (parseParams()/boot()) |
Propagacja preferencji użytkownika między stronami bez URL parametru. Zapobiega przechwyceniu wartości przez historię przeglądarki. Izolowany per-tab. |
data-skin (atrybut DOM) |
Bieżący dokument |
applyOrgSkin(), restoreHubSkin(), init IIFE, skin picker |
CSS (selektory [data-skin="..."]) |
Aktywny skin wizualny. Może być skinem marki org — NIE czytać do celów przechowywania. |
8.4 Przepływ skina — pełny round-trip
1
Użytkownik wybiera skin w hub
Kliknięcie swatcha w skin pickerze lub FAB "Motyw" → localStorage['na-skin'] = SKIN, localStorage['na-hub-skin'] = SKIN, setAttribute('data-skin', SKIN).
↓
2
Użytkownik wchodzi do workspace (enterWorkspace)
applyOrgSkin(orgSlug) ustawia data-skin = ORG_SKIN_MAP[orgSlug] (np. 'arc'). NIE dotyka localStorage ani sessionStorage. Preferencja użytkownika w localStorage pozostaje nienaruszona.
↓
3
Nawigacja do APP_MGMT (dowolny punkt wejścia)
Hub PRZED window.location.href: sessionStorage['na-session-skin'] = localStorage['na-hub-skin'] || localStorage['na-skin'] || 'nexus-dark'. URL nie zawiera parametru ?skin=. Dotyczy wszystkich 4 punktów wejścia: karta onclick, przycisk "Zarządzaj", menu kontekstowe RMB, workspace header "Zarządzaj".
↓
4
APP_MGMT.html — parseParams() przy starcie
Czyta sessionStorage['na-session-skin']. Stosuje przez setAttribute('data-skin', skin). Zapisuje do localStorage['na-hub-skin'].
↓
5
Powrót do hub (goBackToHub w APP_MGMT)
Czyta sessionStorage['na-session-skin'] || localStorage['na-hub-skin'] || localStorage['na-skin']. Nawiguje do NEXUS_APP.html?org=X&base=Y bez ?skin=. Hub przy starcie czyta z localStorage.
↓
6
Hub restoreHubSkin()
Czyta localStorage['na-hub-skin'] || localStorage['na-skin']. Stosuje setAttribute('data-skin', skin). Preferencja użytkownika odtworzono bez kontaminacji.
8.5 Reguły zapobiegające kontaminacji
applyOrgSkin() jest jedyną funkcją pisząca data-skin ze skinem marki org. Nie pisze do żadnego storage.
- Żaden punkt nawigacji do APP_MGMT ani Orkiestratora nie może czytać
getAttribute('data-skin') do celów propagacji skinu. Jedyne dozwolone źródło: localStorage['na-hub-skin'] || localStorage['na-skin'].
sessionStorage['na-session-skin'] musi być zapisany PRZED wywołaniem window.location.href lub window.open().
- APP_MGMT i Orkiestrator czytają sessionStorage, nie URL param
?skin=. Parametr ?skin= w URL jest usunięty z wszystkich punktów nawigacji.
Mapowanie skin → Orkiestrator (nexus-core.js data-theme): nexus-dark→nexus, office-light→office, apple-frost→apple, glass-aurora→nexus, glass-ember→codezero.
9. System i18n
Dwa locale: pl (domyślny) i en. Klucze w obiekcie _i18n. Accessor: t(key). Zmiana przez toggleLang(). Locale zapisywane w localStorage['na-lang'].
10. Relacje między metodami
| Zdarzenie | Wywołuje | Następnie |
| Sign In klik | doHubLogin() | renderHubCards() → renderHubCard() × N, initHubWidgets() |
| initHubWidgets() | renderHealthBar() | fetchFooterVersions(), renderRaidLog(), renderRoadmaps() |
| Karta kliknięta | navigateToManagement() | Zapisuje sessionStorage['na-session-skin'] z preferencji użytkownika. window.location → APP_MGMT.html?org=X (bez &skin= w URL) (REQ-MGMT-001) |
| Open App (APP_MGMT) | enterWorkspace() | executePipeline() (nav), renderSidebar(), renderDefaultDashboard(), renderGmailStatusWidget() |
| Nav item kliknięty | loadView(item) | executePipeline() → renderView() lub renderGateView() |
| FAB "Hub" | goBackToHub() | renderHubCards(), renderHealthBar(), renderRoadmaps(), renderRaidLog() |
| FAB "Orkiestrator" | fabAction('orchestrator') | Zapisuje sessionStorage['na-session-skin'] z preferencji użytkownika. window.open(WORMWOOD_APP.html?org=X) — bez &skin= w URL. |
| FAB "Motyw" | fabAction('skin') | Cykluje po 6 skinach. Baza: localStorage['na-hub-skin'] || localStorage['na-skin']. Pisze: localStorage['na-skin'], localStorage['na-hub-skin'], sessionStorage['na-session-skin'], setAttribute('data-skin',...). |
| Zatwierdź/Odrzuć gate | _gateDecision() | POST /pipelines/{id}/gate-decision |
| Submit gate form | _gateHumanInput() | POST /pipelines/{id}/resume |
7. Uwierzytelnianie i RBAC — Google OAuth 2.0 + JWT
Zaimplementowane 2026-05-11, commit 2bc07f2 (Wormwood). Dotyczy wormwood/api/routes_auth.py, wormwood/auth_jwt.py, admin/models.py (NexusPlatformUser), docs/NEXUS_APP.html (SPA). NAPRAWIONE 2026-05-12 — ISSUE-073 zamknięty: _STATE_STORE zastąpiony cookie HMAC-SHA256 (nexus_oauth_state HttpOnly, SameSite=Lax), wdrożone jako nexus-prod-engine:11 (commit 9ec343b, Wormwood v1.4.5). Poprawki SPA NEXUS_APP.html (commit aef9840 + 174ece6) wdrożone jako nexus-prod-hub:5.
7.1 Endpoints silnika (routes_auth.py)
| Endpoint | Metoda | Auth | Opis |
GET /auth/google | sync | Brak | Generuje URL autoryzacyjny Google OAuth 2.0 z PKCE (code_challenge S256). Przechowuje code_verifier i state w _STATE_STORE (dict w pamięci, max 100 wpisów). Redirectuje 302 do Google. |
GET /auth/callback | sync | Brak | Odbiera code i state od Google. Weryfikuje CSRF state. Wymienia kod na tokeny z PKCE (code_verifier). Weryfikuje id_token przez google.oauth2.id_token.verify_oauth2_token. Upsertuje NexusPlatformUser. Podpisuje Nexus JWT. Redirectuje 302 do SPA z #token={jwt} lub #auth_error={kod}. |
GET /auth/me | async | Bearer JWT | Zwraca profil zalogowanego użytkownika z tabeli nexus_platform_user. Pola: id, email, display_name, picture_url, nexus_role, created_at, last_login_at. |
PATCH /auth/me | async | Bearer JWT | Aktualizuje display_name i/lub picture_url zalogowanego użytkownika. |
7.2 Przepływ OAuth 2.0 z PKCE (sekwencja)
1
SPA: klik „Sign in with Google”
window.location.href = BASE_URL + '/auth/google'
↓
2
Silnik: GET /auth/google
Generuje state (32B random), code_verifier (PKCE), code_challenge = BASE64URL(SHA256(verifier)). Zapisuje _STATE_STORE[state] = code_verifier. 302 → Google.
↓
3
Google: ekran wyboru konta i zgody
Scope: openid email profile. Po zatwierdzeniu 302 → /auth/callback?code=...&state=...
↓
4
Silnik: GET /auth/callback
Weryfikacja state (CSRF). code_verifier = _STATE_STORE.pop(state). flow.fetch_token(code=code, code_verifier=code_verifier). Ustawia OAUTHLIB_RELAX_TOKEN_SCOPE=1 (Google zwraca pełne URI scope zamiast krótkich nazw).
↓
5
Silnik: weryfikacja id_token i upsert użytkownika
google_id_token.verify_oauth2_token(id_token, Request(), CLIENT_ID). Pobiera sub, email, name, picture. Jeżeli email jest w NEXUS_ADMIN_EMAILS → nexus_role = nexus_admin (wymuszenie przy każdym logowaniu). Tworzy lub aktualizuje NexusPlatformUser. db.session.commit().
↓
6
Silnik: emisja JWT i redirect do SPA
sign_token(user_id, email, name, role) → JWT HS256, exp=8h. 302 → NEXUS_APP.html#token={jwt}.
↓
7
SPA: odebranie tokena z URL hash
loadSaved() wykrywa #token= w window.location.hash. Zapisuje JWT do sessionStorage['nexus-jwt']. Czyści hash. Wywołuje doHubLoginWithJwt(token).
↓
8
SPA: załadowanie profilu i zastosowanie RBAC
GET /auth/me z Authorization: Bearer {jwt}. _decodeJwtPayload(token) → nexusRole. _applyNavRbac(role) → pokazuje/ukrywa grupy nawigacyjne per rola.
7.3 Kod JWT (auth_jwt.py)
| Funkcja | Parametry | Opis |
sign_token(user_id, email, name, role) | str/int | Podpisuje JWT HS256 kluczem NEXUS_JWT_SECRET. Payload: {sub, email, name, role, iat, exp}. Exp: 8h (28800s). |
token_from_header(authorization) | str | None | Wyodrębnia i weryfikuje JWT z headera Authorization: Bearer. Rzuca NexusJwtError na błąd. |
7.4 RBAC — funkcje SPA (NEXUS_APP.html)
| Funkcja | Typ | Opis |
_decodeJwtPayload(token) | sync | Dekoduje część payload JWT (base64url) bez weryfikacji sygnatury. Zwraca obiekt JSON z polami sub, email, name, role, exp. Weryfikacja sygnatury odbywa się po stronie silnika. |
_applyNavRbac(role) | sync | Steruje widocznością grup nawigacyjnych na podstawie roli. role=null: ukrywa wszystko. nexus_user: pokazuje tylko data-nav-access="user". nexus_admin: pokazuje data-nav-access="admin" i data-nav-access="user". |
doHubLoginWithJwt(token) | async | Główna ścieżka po odebraniu tokena. Wywołuje GET /auth/me, pobiera profil. Renderuje Hub. Wywołuje _applyNavRbac(nexusRole). |
doHubLogout() | sync | Usuwa sessionStorage['nexus-jwt']. Wywołuje _applyNavRbac(null) (ukrywa nawigację). Pokazuje ekran logowania. |
loadSaved() | sync | Uruchamiana przy starcie SPA (DOMContentLoaded). Wykrywa #token= lub #auth_error= w URL hash. Błąd: czyści JWT z sessionStorage, wywołuje _initLoginScreen(), return (zapobiega maskowania błędu przez stary JWT). |
7.5 Obsługa błędów OAuth (kody #auth_error=)
| Kod błędu | Źródło | Znaczenie |
token_exchange_failed | Silnik /auth/callback | Wymiana kodu OAuth na tokeny nie powiódła się (np. wygaśnięty kod, błąd PKCE) |
invalid_state | Silnik /auth/callback | Weryfikacja CSRF state nie powiodła się. ISSUE-073 NAPRAWIONY 2026-05-12: _STATE_STORE (in-memory dict niekompatybilny z rolling deployment ECS) zastąpiony cookie HMAC-SHA256 (nexus_oauth_state, HttpOnly, SameSite=Lax). Cookie podąża za przeglądarką przez redirect OAuth — nie wymaga współdzielonego stanu serwera. Wdrożone jako nexus-prod-engine:11. |
no_code | Silnik /auth/callback | Google nie przesłało kodu autoryzacyjnego |
no_email | Silnik /auth/callback | id_token nie zawiera pola email |
db_error | Silnik /auth/callback | Błąd zapisu do bazy danych podczas upsert NexusPlatformUser |
access_denied | Google | Użytkownik odmówił zgody na scope |
7.6 Zmienne środowiskowych (ECS task def nexus-prod-engine:11)
| Zmienna | Wymagana | Opis |
GOOGLE_CLIENT_ID | Tak | ID klienta OAuth z Google Cloud Console (projekt 828693947055) |
GOOGLE_CLIENT_SECRET | Tak | Sekret klienta OAuth |
NEXUS_JWT_SECRET | Tak | Klucz HMAC-SHA256 do podpisywania JWT platformy |
NEXUS_PUBLIC_URL | Tak | Bazowy URL SPA — używany do redirect po OAuth (https://app.nexus-dev.codezerogroup.com) |
NEXUS_ADMIN_EMAILS | Nie | Lista emailów adminów oddzielona przecinkami. Każde logowanie z tego emaila dostaje rolę nexus_admin. |
7.7 Endpoints zarządzania użytkownikami (REQ-AUTH-005)
ZAIMPLEMENTOWANE 2026-05-11 — commit 5d77594 (Wormwood/master), engine:10 w produkcji (ECS nexus-prod). Powiązane wymagania: REQ-AUTH-005, data model: data-model.html#_tbl_nexus_platform_users, mockupy: NEXUS_APP_MOCKUPS.html sekcja 07.
| Endpoint | Metoda | Auth | Request body | Response (200) | Kody błędów |
GET /auth/users | async | Bearer JWT (nexus_admin) |
Brak |
[{id, email, display_name, nexus_role, is_active, last_login_at, created_at}, ...] |
401 brak/nieważny JWT; 403 rola nexus_user |
PATCH /auth/users/{id}/role | async | Bearer JWT (nexus_admin) |
{"role": "nexus_admin" | "nexus_user"} |
Zaktualizowany rekord użytkownika |
401 brak JWT; 403 rola nexus_user; 404 user nie istnieje; 409 cannot_demote_self; 422 nieprawidłowa wartość role |
PATCH /auth/users/{id}/active | async | Bearer JWT (nexus_admin) |
{"is_active": true | false} |
Zaktualizowany rekord użytkownika |
401 brak JWT; 403 rola nexus_user; 404 user nie istnieje; 409 cannot_deactivate_self |
7.8 Funkcje SPA — panel użytkowników (APP_MGMT.html)
ZAIMPLEMENTOWANE 2026-05-11 — commit 3e2b577 (Nexus2/main). Lokalizacja: zakładka „Użytkownicy” w docs/APP_MGMT.html. Widoczna wyłącznie gdy JWT zawiera role=nexus_admin.
| Funkcja | Typ | Opis |
loadUsersTable() | async | Wywołuje GET /auth/users z Authorization: Bearer {jwt}. Renderuje tabelę z kolumnami: email, display_name, nexus_role (badge), is_active (dot), last_login_at, created_at, przyciski akcji. Własny wiersz (email == JWT email) ma zablokowane przyciski (self-protection guard). |
openRoleModal(userId, currentRole) | sync | Otwiera modal zmiany roli (mockup 07b). Wypełnia dane użytkownika. Blokuje wyswietlenie jeśli userId == jwt.sub. |
patchUserRole(userId, newRole) | async | Wywołuje PATCH /auth/users/{userId}/role. Po sukcesie: wyświetla toast potwierdzenia, odświeża wiersz w tabeli (optimistic update). Po błędzie 409 (cannot_demote_self): wyświetla komunikat z wyjaśnieniem. |
patchUserActive(userId, isActive) | async | Wywołuje PATCH /auth/users/{userId}/active. Po sukcesie: wyświetla toast, przełącza wizualny stan wiersza (aktywny/dezaktywowany). Po błędzie 409 (cannot_deactivate_self): wyświetla komunikat. |
7.9 Migracja bazy danych
ZAIMPLEMENTOWANE 2026-05-11 — commit 5d77594 (Wormwood/master). Plik: admin/migrate_m_nexus_06.py. SQLite jest efemeryczny na Fargate — create_all() tworzy kolumny w nowych instancjach. Migracja ALTER TABLE jest idempotentna (pomija duplikaty).
| Kolumna | Typ | Default | Cel |
is_active | INTEGER | 1 | Flaga aktywności (0 = zablokowane logowanie). Wymagana przez AC-005-05. |
role_updated_at | DATETIME | NULL | Timestamp ostatniej zmiany roli (audit trail). |
role_updated_by | TEXT | NULL | Email admina który zmienił rolę (audit trail, brak osobnej tabeli logów). |
Plik migracji: E:\repos\UV\Wormwood\admin\migrate_m_nexus_06.py — zaimplementowany commit 5d77594.