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.

WarstwaTechnologiaŚcieżkaPort
Frontend SPAVanilla JS + HTMLdocs/NEXUS_APP.html
OrkiestratorVanilla JS + HTMLdocs/WORMWOOD_APP.html
API BackendFastAPI + SQLAlchemywormwood/api/8012
Static FilesFastAPI FileResponse/arc-ui/ lub /nexus-ui/8012
Admin UIFlask + 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

IDTypOpis
#login-screendivKontener ekranu logowania. Ukryty po zalogowaniu.
#li-emailinputEmail użytkownika
#li-baseinputURL silnika — ukryty (display:none), zachowany dla kompatybilności
#li-btnbuttonPrzycisk "Sign In" — wywołuje doHubLogin()
#li-statusdivKomunikat statusu/błędu logowania

Metody JavaScript

MetodaSygnaturaOpis
doHubLogin()asyncGłó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()asyncStara ścieżka direct-connect (legacy). Używa #li-org i #li-base. Zachowana w kodzie ale nie wywoływana z UI.
loginStatus(msg, isErr)syncUstawia tekst i klasę CSS #li-status.
loadSaved()syncOdczytuje localStorage 'nexus-login', przywraca base URL do #li-base.
saveLogin(org, key, base)syncZapisuje org/key/base do localStorage 'nexus-login'.
autoLoadOrgs()asyncPobiera 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

IDOpis
#hub-screenKontener hub dashboard. Ukryty przed logowaniem i po wejściu do aplikacji.
#hub-cardsKontener kart aplikacji. Wypełniany przez renderHubCards().
#hub-searchPole wyszukiwania kart — oninput="filterHubCards(this.value)"
#hub-section-title-textTytuł sekcji kart — "Twoje aplikacje" / "Your Applications" (i18n)
#hub-section-hintPodpowiedź — "Kliknij kartę, aby otworzyć" (i18n)
#hub-sort-alpha / #hub-sort-statusPrzyciski sortowania A-Z / Status
#hub-health-detailSzczegóły statusu silnika (rules count, version)
#hub-footer-componentsChips komponentów w footer (z wersją i [deployment])
#hub-raidKontener elementów RAID log
#hub-roadmap-listLista roadmap (z API /omnissiah/roadmaps)
#hub-lang-togglePrzycisk PL/EN — onclick="toggleLang()"

Metody JavaScript — renderowanie hub

MetodaSygnaturaOpis
renderHubCards(data, skipSave)syncCzyś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 → stringZwraca 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)syncIteruje po dzieciach #hub-cards, ukrywa karty nie pasujące do query (szuka po name/domain/desc/slug case-insensitive).
sortHubCards(by)syncSortuje karty w #hub-cards (in-place DOM sort). by: 'alpha' lub 'status'.
initHubWidgets()syncWywołuje: renderHealthBar(), fetchFooterVersions(), renderRaidLog(), renderRoadmaps().
renderHealthBar()asyncGET /health, aktualizuje #hub-health-detail i status dot. Wywołuje fetchFooterVersions() jeśli dostępne component_versions.
fetchFooterVersions()asyncGET /health, renderuje chipa w #hub-footer-components. Format: "Name vX.X [env]". Bez emoji.
renderRaidLog()asyncGET /omnissiah/raid (requires auth). Fallback: _raidStaticFallback (8 statycznych wpisów). Renderuje .hub-raid-item w #hub-raid. Filtrowanie przez .hub-raid-filter buttons.
renderRoadmaps()asyncGET /omnissiah/roadmaps. Fallback: statyczny komunikat. Renderuje do #hub-roadmap-list.
toggleLang()syncPrzełą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 → stringAccessor i18n. Zwraca _i18n[_lang][key] || key.

Metody JavaScript — nawigacja hub

MetodaOpis
navigateToManagement(orgData)syncZapisuje 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)asyncUstawia 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

IDOpis
#app-shellGłówny kontener workspace. hidden=true do czasu wejścia.
#sidebarSidebar nawigacyjny. Wypełniany przez renderSidebar().
#sidebar-navElement nav wewnątrz sidebara — tutaj wstrzykiwane są elementy menu.
#app-org-nameNazwa aktywnej aplikacji (navConfig.app_title || orgSlug)
#content-areaGłówny obszar treści — tutaj renderowane są widoki pipeline'ów.
#ws-fabFloating Action Button "N" — fabAction()
#ws-fab-menuRozwijane menu FAB: Hub, Orkiestrator, Logi, Motyw.
#run-panelPanel logów uruchomień (M-NEXUS-E3-3).
#gmail-status-widgetWidget statusu Gmail (hidden dla org != arc).

Metody JavaScript — App Shell

MetodaOpis
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)

MetodaPlikOpis
selectOrg(slug)WORMWOOD_APPSzuka 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_APPRenderuje karty USE_CASES w #ucd-grid. Pobiera pipeline count dla każdego aktywnego case.
showDashboard() / hideDashboard()WORMWOOD_APPToggleuje klasę 'hidden' na #uc-dashboard.
applyTheme(t)nexus-core.jsUstawia/usuwa data-theme na body. Aktualizuje .theme-swatch active states. Zapisuje do localStorage 'nexus-theme'.

7. API Backend — kluczowe endpointy

Org & Auth

EndpointMetodaPlikOpis
/orgs/user-hub?email=XGETroutes_orgs.pyZwraca tablicę org przypisanych do email: [{org_slug, org_name, key, role, pipeline_count, usecase: {}}]
/orgs/login-listGETroutes_orgs.pyLista wszystkich org (dev fallback): [{slug, name, key, pipeline_count}]
/orgs/authenticatePOSTroutes_orgs.pyBody: {email, org_slug}. Zwraca {key, display_name}.
/omnissiah/raidGETroutes_orgs.pyLista wpisów RAID log. Wymaga auth.
/omnissiah/roadmapsGETroutes_orgs.pyLista roadmap projektów. Wymaga auth.

Pipeline — CRUD & Wykonanie

EndpointMetodaPlikOpis
/pipelines?org_slug=XGETroutes_pipelines.pyLista pipeline'ów dla org. Wymaga X-API-Key.
/pipelines/{id}GETroutes_pipelines.pyPełne dane pipeline (graph, config, status, base_slug).
/pipelines/{id}PUTroutes_pipelines.pyAktualizuje pipeline. Zwraca zaktualizowany obiekt.
/pipelines/{id}/executePOSTroutes_pipelines.pyWykonuje pipeline. Wymaga status=active (422 jeśli nie). Zwraca {status, node_outputs, run_id}.
/pipelines/{id}/trigger-runPOSTroutes_pipelines.pyWyzwala run. Parametr force=true omija guard dla draft (dev bypass).

Pipeline — Wersjonowanie & Cykl życia (REQ-VER, REQ-DESC — 2026-05-05)

EndpointMetodaPlikOpis
/pipelines/{id}/publishPOSTroutes_pipelines.pyPrzechodzi draft→active. Archiwizuje bieżącą aktywną wersję tego samego base_slug. 409 jeśli już active.
/pipelines/{id}/deactivatePOSTroutes_pipelines.pyPrzechodzi active→archived. 400 jeśli nie active.
/pipelines/{id}/restorePOSTroutes_pipelines.pyKlonuje archived jako nowy draft z PATCH-bumped version. Zwraca nowy pipeline.
/pipelines/{id}/forkPOSTroutes_pipelines.pyKlonuje dowolny pipeline jako nowy draft z PATCH-bumped version.
/pipelines/history?org_slug=X&base_slug=YGETroutes_pipelines.pyWszystkie wersje pipeline'u pogrupowane po base_slug. Posortowane malejąco po wersji.
/pipelines/{id}/descriptorGETroutes_pipelines.pyEksportuje 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/importPOSTroutes_pipelines.pyImportuje descriptor JSON. Body: {descriptor, target_org_slug}. Kolizja slug → dodaje -imported-{timestamp}. Zawsze importuje jako draft.

Infrastruktura

EndpointMetodaPlikOpis
/health/GETroutes_health.pyStatus silnika: {status, version, rules_loaded, component_versions[]}.
/health/resourcesGETroutes_health.pyMetryki zasobów: CPU, RAM, disk.
/arc-ui/{file_path}GETapp.pySerwuje pliki z docs/. Historyczny prefix.
/nexus-ui/{file_path}GETapp.pyNeutral 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)

IDOpis
#mgmt-headerNagłówek: org name, env selector, health dot
#mgmt-env-listPanel środowisk — lista/siatka wszystkich envs z statusem, wersją, przyciskami akcji
#mgmt-pipeline-versionsHistoria wersji pipeline dla tej org (REQ-VER-002)
#mgmt-backupsLista backupów wg środowisk
#mgmt-settingsUstawienia: edytor menu RMB, metadane org, klucze API
#btn-open-appCTA otwierający live aplikację dla wybranego środowiska
#btn-back-hubPowrót do hubu

Metody JavaScript (APP_MGMT.html)

MetodaSygnaturaOpis
loadMgmtPage(orgSlug, envSlug)asyncInicjalizacja strony. GET /orgs/{org_slug}/environments. Renderuje listę środowisk. Wywołuje loadEnvDetail(envSlug || 'production').
loadEnvDetail(envSlug)asyncGET /orgs/{org_slug}/environments/{env_slug}. Aktualizuje health dot, wersję, przyciski akcji.
envAction(envSlug, action)asyncPOST /orgs/{org_slug}/environments/{env_slug}/{action}. Pokazuje dialog potwierdzenia dla akcji destruktywnych (stop, downgrade na production). Wywołuje loadEnvDetail() po sukcesie.
spinUpEnv()asyncOtwiera dialog tworzenia środowiska. POST /orgs/{org_slug}/environments z {name, slug, base_version}. Odświeża listę środowisk.
loadRmbEditor()asyncGET /orgs/{org_slug}/rmb-items. Renderuje edytor menu RMB z drag-drop (REQ-RMB-004).
saveRmbItems(items)asyncPUT /orgs/{org_slug}/rmb-items. Zapisuje aktualny stan listy RMB.
openApp(envSlug)syncwindow.open(app URL for env, '_blank'). Wywołuje enterWorkspace() po stronie NEXUS_APP.html.

Metody JavaScript — RMB (NEXUS_APP.html)

MetodaSygnaturaOpis
initCardRmb()syncBinduje contextmenu event na każdej .hub-card. Wywołuje renderRmbMenu(orgData, e) na prawym kliknięciu.
renderRmbMenu(orgData, event)asyncGET /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)asyncWykonuje akcję RMB. Dla env_action: pokazuje inline mini-dialog potwierdzenia. Dla demo mode: uruchamia walkthrough bez wywołania API. Wynik jako toast notification.
dismissRmbMenu()syncUsuwa #hub-rmb-menu z DOM.

API — Zarządzanie środowiskami (REQ-ENV-004)

EndpointMetodaPlikOpis
/orgs/{org_slug}/environmentsGETroutes_orgs.pyLista wszystkich środowisk org. Zwraca [{slug, name, version, status, url, created_at, last_deployed_at}]
/orgs/{org_slug}/environmentsPOSTroutes_orgs.pyTworzy nowe środowisko. Body: {name, slug, base_version, url}.
/orgs/{org_slug}/environments/{env_slug}GETroutes_orgs.pySzczegóły środowiska + bieżący stan zdrowia.
/orgs/{org_slug}/environments/{env_slug}/startPOSTroutes_orgs.pyUruchamia zatrzymane środowisko.
/orgs/{org_slug}/environments/{env_slug}/stopPOSTroutes_orgs.pyZatrzymuje działające środowisko.
/orgs/{org_slug}/environments/{env_slug}/restartPOSTroutes_orgs.pyStop + start. Dialog potwierdzenia dla production.
/orgs/{org_slug}/environments/{env_slug}/backupPOSTroutes_orgs.pySnapshot danych i konfiguracji. Zwraca {backup_id, download_url, created_at}.
/orgs/{org_slug}/environments/{env_slug}/upgradePOSTroutes_orgs.pyBody: {target_version}. Wdraża wyższą wersję.
/orgs/{org_slug}/environments/{env_slug}/downgradePOSTroutes_orgs.pyBody: {target_version}. Cofa do wcześniejszej wersji.
/orgs/{org_slug}/environments/{env_slug}/backupsGETroutes_orgs.pyLista backupów dla środowiska.
/orgs/{org_slug}/rmb-itemsGETroutes_orgs.pyLista dynamicznych pozycji RMB dla org. Zwraca [{label, action_type, action_target, icon, order, enabled}]
/orgs/{org_slug}/rmb-itemsPUTroutes_orgs.pyPełna zamiana listy RMB. Body: [{...}]. Zwraca zaktualizowaną listę.

8. System skinowania

8.1 Dostępne skiny (preferencja użytkownika)

Skindata-skinStyl
NeXus Dark (domyślny)nexus-darkCiemny, indigo accent
CodeZerocodezeroGłęboki granat, cyan accent
Office Lightoffice-lightBiały, niebieski accent
Apple Frostapple-frostKremowy, szary accent
Glass Auroraglass-auroraGlassmorphism, backdrop-filter:blur(12px), green glow
Glass Emberglass-emberGlassmorphism, 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:

RodzajDefinicjaŹródłoGdzie 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

KluczZakresKto piszeKto czytaCel
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

  1. applyOrgSkin() jest jedyną funkcją pisząca data-skin ze skinem marki org. Nie pisze do żadnego storage.
  2. Ż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'].
  3. sessionStorage['na-session-skin'] musi być zapisany PRZED wywołaniem window.location.href lub window.open().
  4. 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

ZdarzenieWywołujeNastępnie
Sign In klikdoHubLogin()renderHubCards() → renderHubCard() × N, initHubWidgets()
initHubWidgets()renderHealthBar()fetchFooterVersions(), renderRaidLog(), renderRoadmaps()
Karta klikniętanavigateToManagement()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ętyloadView(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)

EndpointMetodaAuthOpis
GET /auth/googlesyncBrakGeneruje 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/callbacksyncBrakOdbiera 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/measyncBearer JWTZwraca 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/measyncBearer JWTAktualizuje 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_EMAILSnexus_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)

FunkcjaParametryOpis
sign_token(user_id, email, name, role)str/intPodpisuje JWT HS256 kluczem NEXUS_JWT_SECRET. Payload: {sub, email, name, role, iat, exp}. Exp: 8h (28800s).
token_from_header(authorization)str | NoneWyodrębnia i weryfikuje JWT z headera Authorization: Bearer. Rzuca NexusJwtError na błąd.

7.4 RBAC — funkcje SPA (NEXUS_APP.html)

FunkcjaTypOpis
_decodeJwtPayload(token)syncDekoduje 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)syncSteruje 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)asyncGłówna ścieżka po odebraniu tokena. Wywołuje GET /auth/me, pobiera profil. Renderuje Hub. Wywołuje _applyNavRbac(nexusRole).
doHubLogout()syncUsuwa sessionStorage['nexus-jwt']. Wywołuje _applyNavRbac(null) (ukrywa nawigację). Pokazuje ekran logowania.
loadSaved()syncUruchamiana 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łoZnaczenie
token_exchange_failedSilnik /auth/callbackWymiana kodu OAuth na tokeny nie powiódła się (np. wygaśnięty kod, błąd PKCE)
invalid_stateSilnik /auth/callbackWeryfikacja 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_codeSilnik /auth/callbackGoogle nie przesłało kodu autoryzacyjnego
no_emailSilnik /auth/callbackid_token nie zawiera pola email
db_errorSilnik /auth/callbackBłąd zapisu do bazy danych podczas upsert NexusPlatformUser
access_deniedGoogleUżytkownik odmówił zgody na scope

7.6 Zmienne środowiskowych (ECS task def nexus-prod-engine:11)

ZmiennaWymaganaOpis
GOOGLE_CLIENT_IDTakID klienta OAuth z Google Cloud Console (projekt 828693947055)
GOOGLE_CLIENT_SECRETTakSekret klienta OAuth
NEXUS_JWT_SECRETTakKlucz HMAC-SHA256 do podpisywania JWT platformy
NEXUS_PUBLIC_URLTakBazowy URL SPA — używany do redirect po OAuth (https://app.nexus-dev.codezerogroup.com)
NEXUS_ADMIN_EMAILSNieLista 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.
EndpointMetodaAuthRequest bodyResponse (200)Kody błędów
GET /auth/usersasyncBearer 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}/roleasyncBearer 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}/activeasyncBearer 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.
FunkcjaTypOpis
loadUsersTable()asyncWywoł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)syncOtwiera modal zmiany roli (mockup 07b). Wypełnia dane użytkownika. Blokuje wyswietlenie jeśli userId == jwt.sub.
patchUserRole(userId, newRole)asyncWywoł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)asyncWywoł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).
KolumnaTypDefaultCel
is_activeINTEGER1Flaga aktywności (0 = zablokowane logowanie). Wymagana przez AC-005-05.
role_updated_atDATETIMENULLTimestamp ostatniej zmiany roli (audit trail).
role_updated_byTEXTNULLEmail 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.