import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs"; import { getComponentSvc } from "@ibgib/web-gib/dist/ui/component/ibgib-component-service.mjs"; import { IbGibDynamicComponentMeta } from "@ibgib/web-gib/dist/ui/component/component-types.mjs"; import { APP_CONFIG } from "../../constants.mjs"; import { getIbGibGlobalThis_SpaceGib } from "../../helpers.web.mjs"; import { ID_APP_ROOT, ID_HEADER_PANEL, ID_LEFT_PANEL, ID_CENTER_PANEL, ID_RIGHT_PANEL, ID_FOOTER_PANEL, ID_LEFT_RESIZER, ID_RIGHT_RESIZER, ID_FOOTER_RESIZER, ID_BTN_LEFT_PANEL_TOGGLE, ID_BTN_RIGHT_PANEL_TOGGLE, ID_BTN_CREATE_KEYSTONE, ID_CENTER_PANEL_CONTENT } from "./space-gib-shell-constants.mjs"; import { EVENT_IBGIB_SHELL_READY, EVENT_IBGIB_UI_BUSY, EVENT_IBGIB_UI_MESSAGE } from "@ibgib/web-gib/dist/ui/ui-constants.mjs"; import { showFullscreenDialog, FullscreenDialogController } from "@ibgib/web-gib/dist/ui/ui-helpers.mjs"; import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { getKeystoneSyncService } from "@ibgib/web-gib/dist/identity/keystone-sync-service.mjs"; import { PanelState } from "./space-gib-shell-types.mjs"; import { registerStandardIdentityComponents } from "@ibgib/web-gib/dist/ui/component/identity/index.mjs"; export class SpaceGibShellService { private lc: string = `[SpaceGibShellService]`; public initialized: Promise; // Panel States private leftPanelState: PanelState = 'collapsed'; private rightPanelState: PanelState = 'collapsed'; // Resizing State private isResizingLeft = false; private isResizingRight = false; private isResizingFooter = false; constructor() { this.initialized = this.initialize(); } private async initialize(): Promise { this.initElements(); this.initEventHandlers(); await this.registerComponents(); } private initElements(): void { const lc = `${this.lc}[${this.initElements.name}]`; try { // Initial CSS vars setup this.updateCssVariables(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private initEventHandlers(): void { const lc = `${this.lc}[${this.initEventHandlers.name}]`; try { // Left Panel Toggle const btnLeft = document.getElementById(ID_BTN_LEFT_PANEL_TOGGLE); if (btnLeft) { btnLeft.addEventListener('click', () => this.toggleLeftPanel()); } // Right Panel Toggle const btnRight = document.getElementById(ID_BTN_RIGHT_PANEL_TOGGLE); if (btnRight) { btnRight.addEventListener('click', () => this.toggleRightPanel()); } // Resizers const leftResizer = document.getElementById(ID_LEFT_RESIZER); if (leftResizer) { leftResizer.addEventListener('mousedown', () => { if (this.leftPanelState === 'expanded') { this.isResizingLeft = true; } }); leftResizer.addEventListener('dblclick', () => this.toggleLeftPanel()); } const rightResizer = document.getElementById(ID_RIGHT_RESIZER); if (rightResizer) { rightResizer.addEventListener('mousedown', () => { if (this.rightPanelState === 'expanded') { this.isResizingRight = true; } }); rightResizer.addEventListener('dblclick', () => this.toggleRightPanel()); } const footerResizer = document.getElementById(ID_FOOTER_RESIZER); if (footerResizer) { footerResizer.addEventListener('mousedown', () => this.isResizingFooter = true); } // Window mouse events for dragging window.addEventListener('mousemove', (e) => this.handleMouseMove(e)); window.addEventListener('mouseup', () => this.handleMouseUp()); // Create Keystone Button const btnCreate = document.getElementById(ID_BTN_CREATE_KEYSTONE); if (btnCreate) { btnCreate.addEventListener('click', () => this.showIdentityManager()); } window.addEventListener(EVENT_IBGIB_UI_MESSAGE, (ev: any) => { const action = ev.detail?.action; if (action === 'show-identity-manager') { this.showIdentityManager(); } else if (action === 'show-keystone-creator') { this.showKeystoneCreator(); } else if (action === 'show-add-device') { this.showAddDevice(); } }); // Listen for global busy events from components let currentBusyOverlay: FullscreenDialogController | undefined; let isShellBusy = false; window.addEventListener(EVENT_IBGIB_UI_BUSY, (ev: any) => { const { isBusy, title, msg, animationEmoji } = ev.detail; if (isBusy) { isShellBusy = true; if (currentBusyOverlay) { currentBusyOverlay.update({ title, msg, animationEmoji }); } else { // we use a .then() only because we are within // synchronous code. showFullscreenDialog({ title: title || 'Processing...', msg: msg || 'Please do not navigate away or refresh.', isBusy: true, animationEmoji }).then((overlay) => { if (isShellBusy) { currentBusyOverlay = overlay; } else { overlay.close(); } }).catch((e) => { console.error(`${lc}[showFullscreenDialog] ${extractErrorMsg(e)}`) }); } } else { isShellBusy = false; if (currentBusyOverlay) { currentBusyOverlay.close(); currentBusyOverlay = undefined; } } }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } public async showIdentityManager() { const lc = `${this.lc}[${this.showIdentityManager.name}]`; try { // 2. Instantiate and inject the component const componentSvc = await getComponentSvc(); const component = await componentSvc.getComponentInstance({ path: 'ibgib-identity-manager', ibGibAddr: ROOT_ADDR, // Virtual address for initial render useRegExpPrefilter: true, }); const centerPanel = document.getElementById(ID_CENTER_PANEL_CONTENT); if (centerPanel && component) { centerPanel.innerHTML = ''; // Clear hero section centerPanel.appendChild(component as any); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private async showKeystoneCreator() { const lc = `${this.lc}[${this.showKeystoneCreator.name}]`; try { // 2. Instantiate and inject the component const componentSvc = await getComponentSvc(); const component = await componentSvc.getComponentInstance({ path: 'ibgib-keystone-creator', ibGibAddr: ROOT_ADDR, // Virtual address for initial render useRegExpPrefilter: true, }); const centerPanel = document.getElementById(ID_CENTER_PANEL_CONTENT); if (centerPanel && component) { centerPanel.innerHTML = ''; // Clear hero section centerPanel.appendChild(component as any); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } public async showAddDevice() { const lc = `${this.lc}[${this.showAddDevice.name}]`; try { const componentSvc = await getComponentSvc(); const component = await componentSvc.getComponentInstance({ path: 'ibgib-add-device', ibGibAddr: ROOT_ADDR, useRegExpPrefilter: true, }); const centerPanel = document.getElementById(ID_CENTER_PANEL_CONTENT); if (centerPanel && component) { centerPanel.innerHTML = ''; // Clear hero section centerPanel.appendChild(component as any); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private toggleLeftPanel() { this.leftPanelState = this.leftPanelState === 'expanded' ? 'collapsed' : 'expanded'; const panel = document.getElementById(ID_LEFT_PANEL); if (panel) { if (this.leftPanelState === 'collapsed') { panel.classList.add('collapsed'); } else { panel.classList.remove('collapsed'); } } } private toggleRightPanel() { this.rightPanelState = this.rightPanelState === 'expanded' ? 'collapsed' : 'expanded'; const panel = document.getElementById(ID_RIGHT_PANEL); if (panel) { if (this.rightPanelState === 'collapsed') { panel.classList.add('collapsed'); } else { panel.classList.remove('collapsed'); } } } private handleMouseMove(e: MouseEvent) { if (!this.isResizingLeft && !this.isResizingRight && !this.isResizingFooter) return; // Prevent text selection while dragging e.preventDefault(); if (this.isResizingLeft) { // Constrain between 100px and 600px let newWidth = Math.max(100, Math.min(e.clientX, 600)); document.documentElement.style.setProperty('--left-panel-width', `${newWidth}px`); } if (this.isResizingRight) { // Right panel width = window inner width - mouse X let newWidth = Math.max(100, Math.min(window.innerWidth - e.clientX, 600)); document.documentElement.style.setProperty('--right-panel-width', `${newWidth}px`); } if (this.isResizingFooter) { // Footer height = window inner height - mouse Y let newHeight = Math.max(24, Math.min(window.innerHeight - e.clientY, 400)); document.documentElement.style.setProperty('--footer-panel-height', `${newHeight}px`); } } private handleMouseUp() { this.isResizingLeft = false; this.isResizingRight = false; this.isResizingFooter = false; } private updateCssVariables() { // We set defaults in CSS, but this is a placeholder if we need to load saved user prefs later } private async registerComponents(): Promise { const lc = `${this.lc}[${this.registerComponents.name}]`; try { registerStandardIdentityComponents(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } public async onEngineReady(): Promise { const lc = `${this.lc}[${this.onEngineReady.name}]`; try { await this.initialized; console.log(`${lc} SpaceGib Shell Service is ready.`); // Ensure the UI removes the loading text const statusSection = document.getElementById('status-section'); if (statusSection) { statusSection.style.display = 'none'; } // Mount identity-header component in the header bar const componentSvc = await getComponentSvc(); const identityHeader = await componentSvc.getComponentInstance({ path: 'ibgib-identity-header', ibGibAddr: ROOT_ADDR, useRegExpPrefilter: true, }); const headerContainer = document.getElementById('identity-header-container'); if (headerContainer && identityHeader) { headerContainer.innerHTML = ''; headerContainer.appendChild(identityHeader as any); } window.dispatchEvent(new CustomEvent(EVENT_IBGIB_SHELL_READY, { detail: { timestamp: Date.now() } })); // Trigger 1 (App Boot): On onEngineReady, if active identity exists, check for updates. const domainIdentity = (globalThis as any).ibgib?.identity?.domainIdentity; if (domainIdentity) { const activePrimaryAddr = getIbGibAddr({ ibGib: domainIdentity }); const syncSvc = getKeystoneSyncService(); const updateCheck = await syncSvc.checkForKeystoneUpdate({ activePrimaryAddr }); if (updateCheck.hasUpdate) { console.log(`${lc} 🔔 Keystone update available on app boot! Server n=${updateCheck.serverN}, Local n=${updateCheck.localN}`); await syncSvc.updateActiveKeystone({ activePrimaryAddr }); } else { console.log(`${lc} Keystone identity up to date on app boot (n=${updateCheck.localN}).`); } } // Trigger 5 (30s Test Heartbeat): Start background 30-second setInterval heartbeat log nag // todo: commenting out just while testing keystone creation workflow. need to reenable this code, but also maybe make it smarter so that it doesn't trigger **during** account creation/sign in workflows. // setInterval(async () => { // console.log(`[KeystoneSyncService] ⚠️ HEARTBEAT TEST MODE: Interval set to 30s for testing (MUST be changed to 5m for production)`); // try { // const currentDomainIdentity = (globalThis as any).ibgib?.identity?.domainIdentity; // if (currentDomainIdentity) { // const activeAddr = getIbGibAddr({ ibGib: currentDomainIdentity }); // const syncSvc = getKeystoneSyncService(); // const res = await syncSvc.checkForKeystoneUpdate({ activePrimaryAddr: activeAddr }); // if (res.hasUpdate) { // console.log(`[KeystoneSyncService] 🔔 Heartbeat detected keystone update on server! Server n=${res.serverN}, Local n=${res.localN}`); // } // } // } catch (err) { // console.warn(`[KeystoneSyncService] Heartbeat update check error: ${extractErrorMsg(err)}`); // } // }, 30000); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } } export function getSpaceGibShellSvc(): SpaceGibShellService { const lc = `[getSpaceGibShellSvc]`; const ibGibGlobalThis = getIbGibGlobalThis_SpaceGib(APP_CONFIG); if (!ibGibGlobalThis.spaceGibShellSvc) { if (typeof (console) !== 'undefined') { console.log(`${lc} initializing SpaceGibShellService singleton on globalThis... (I: genuuid)`); } ibGibGlobalThis.spaceGibShellSvc = new SpaceGibShellService(); } return ibGibGlobalThis.spaceGibShellSvc; }