import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as interfaces from '../../ts_interfaces/index.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; import type { ObViewDashboard } from './ob-view-dashboard.js'; import type { ObViewServices } from './ob-view-services.js'; import type { ObViewNetwork } from './ob-view-network.js'; import type { ObViewRegistries } from './ob-view-registries.js'; import type { ObViewTokens } from './ob-view-tokens.js'; import type { ObViewSettings } from './ob-view-settings.js'; @customElement('ob-app-shell') export class ObAppShell extends DeesElement { @state() accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false }; @state() accessor uiState: appstate.IUiState = { activeView: 'dashboard', autoRefresh: true, refreshInterval: 30000, }; @state() accessor loginLoading: boolean = false; @state() accessor loginError: string = ''; private viewTabs = [ { name: 'Dashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() }, { name: 'Services', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() }, { name: 'Network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() }, { name: 'Registries', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() }, { name: 'Tokens', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() }, { name: 'Settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() }, ]; private resolvedViewTabs: Array<{ name: string; element: any }> = []; constructor() { super(); document.title = 'Onebox'; const loginSubscription = appstate.loginStatePart .select((stateArg) => stateArg) .subscribe((loginState) => { this.loginState = loginState; if (loginState.isLoggedIn) { appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null); } }); this.rxSubscriptions.push(loginSubscription); const uiSubscription = appstate.uiStatePart .select((stateArg) => stateArg) .subscribe((uiState) => { this.uiState = uiState; this.syncAppdashView(uiState.activeView); }); this.rxSubscriptions.push(uiSubscription); } public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; } .maincontainer { width: 100%; height: 100vh; } `, ]; public render(): TemplateResult { return html`
`; } public async firstUpdated() { // Resolve async view tab imports this.resolvedViewTabs = await Promise.all( this.viewTabs.map(async (tab) => ({ name: tab.name, element: await tab.element, })), ); this.requestUpdate(); await this.updateComplete; const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any; if (simpleLogin) { simpleLogin.addEventListener('login', (e: CustomEvent) => { this.login(e.detail.data.username, e.detail.data.password); }); } const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any; if (appDash) { appDash.addEventListener('view-select', (e: CustomEvent) => { const viewName = e.detail.view.name.toLowerCase(); appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: viewName }); }); appDash.addEventListener('logout', async () => { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); }); } // Load the initial view on the appdash now that tabs are resolved // (appdash's own firstUpdated already fired when viewTabs was still empty) if (appDash && this.resolvedViewTabs.length > 0) { const initialView = this.resolvedViewTabs.find( (t) => t.name.toLowerCase() === this.uiState.activeView, ) || this.resolvedViewTabs[0]; await appDash.loadView(initialView); } // Check for stored session (persistent login state) const loginState = appstate.loginStatePart.getState(); if (loginState.identity?.jwt) { if (loginState.identity.expiresAt > Date.now()) { // Validate token with server before switching to dashboard // (server may have restarted with a new JWT secret) try { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetSystemStatus >('/typedrequest', 'getSystemStatus'); const response = await typedRequest.fire({ identity: loginState.identity }); // Token is valid - switch to dashboard appstate.systemStatePart.setState({ status: response.status }); this.loginState = loginState; if (simpleLogin) { await simpleLogin.switchToSlottedContent(); } } catch (err) { // Token rejected by server - clear session console.warn('Stored session invalid, returning to login:', err); await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } else { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } } private async login(username: string, password: string) { const domtools = await this.domtoolsPromise; const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any; const form = simpleLogin?.shadowRoot?.querySelector('dees-form') as any; if (form) { form.setStatus('pending', 'Logging in...'); } const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, { username, password, }); if (newState.identity) { if (form) { form.setStatus('success', 'Logged in!'); } if (simpleLogin) { await simpleLogin.switchToSlottedContent(); } await appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null); } else { if (form) { form.setStatus('error', 'Login failed!'); await domtools.convenience.smartdelay.delayFor(2000); form.reset(); } } } private syncAppdashView(viewName: string): void { const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any; if (!appDash || this.resolvedViewTabs.length === 0) return; const targetTab = this.resolvedViewTabs.find((t) => t.name.toLowerCase() === viewName); if (!targetTab) return; // Use appdash's own loadView method for proper view management appDash.loadView(targetTab); } }