Nico 3a9c2795cf v0.15.2: ES6 module refactor, 2-row layout, dashboard test, PA routing fix
Frontend refactored to ES6 modules (no bundler):
  js/main.js    — entry point, wires all modules
  js/auth.js    — OIDC login, token management
  js/ws.js      — /ws, /ws/test, /ws/trace connections + HUD handler
  js/chat.js    — messages, send, streaming
  js/graph.js   — Cytoscape visualization + animation
  js/trace.js   — trace panel
  js/dashboard.js — workspace controls rendering
  js/awareness.js — state panel, sensors, meters
  js/tests.js   — test status display
  js/util.js    — shared utilities

New 2-row layout:
  Top:    test status | connection status
  Middle: Workspace | Node Details | Graph
  Bottom: Chat | Awareness | Trace

PA routing: routes ALL tool requests to expert (DB, UI, buttons, machines)
Dashboard integration test: 15/15

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:58:47 +02:00

89 lines
2.7 KiB
JavaScript

/** Authentication: OIDC login, token management. */
export let authToken = localStorage.getItem('cog_token');
export let authConfig = null;
let _authFailed = false;
export function isAuthFailed() { return _authFailed; }
export function setAuthFailed(v) { _authFailed = v; }
export async function initAuth(onReady) {
try {
const r = await fetch('/auth/config');
authConfig = await r.json();
} catch (e) {
authConfig = { enabled: false };
}
if (!authConfig.enabled) { onReady(); return; }
// Check for OIDC callback
const params = new URLSearchParams(location.search);
if (params.has('code')) {
// Exchange code for token (PKCE)
const code = params.get('code');
const verifier = sessionStorage.getItem('cog_pkce_verifier');
try {
const tokenResp = await fetch(authConfig.issuer + '/oauth/v2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: location.origin + '/callback',
client_id: authConfig.clientId,
code_verifier: verifier || '',
}),
});
const tokenData = await tokenResp.json();
if (tokenData.id_token) {
authToken = tokenData.id_token;
localStorage.setItem('cog_token', authToken);
if (tokenData.access_token) {
localStorage.setItem('cog_access_token', tokenData.access_token);
}
}
} catch (e) { console.error('token exchange failed', e); }
history.replaceState(null, '', '/');
}
if (authToken) {
onReady();
} else {
showLogin();
}
}
export function showLogin() {
const el = document.getElementById('login-overlay');
if (el) el.style.display = 'flex';
}
export async function startLogin() {
if (!authConfig) return;
const verifier = randomString(64);
sessionStorage.setItem('cog_pkce_verifier', verifier);
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const params = new URLSearchParams({
response_type: 'code',
client_id: authConfig.clientId,
redirect_uri: location.origin + '/callback',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
prompt: 'login',
});
location.href = authConfig.issuer + '/oauth/v2/authorize?' + params;
}
function randomString(len) {
const arr = new Uint8Array(len);
crypto.getRandomValues(arr);
return Array.from(arr, b => b.toString(36).slice(-1)).join('').slice(0, len);
}