import * as appstate from '../appstate.js'; import * as interfaces from '../../dist_ts_interfaces/index.js'; import { viewHostCss } from './shared/css.js'; import { type IStatsTile } from '@design.estate/dees-catalog'; import { DeesElement, css, cssManager, customElement, html, state, type TemplateResult, } from '@design.estate/dees-element'; @customElement('ops-view-routes') export class OpsViewRoutes extends DeesElement { @state() accessor routeState: appstate.IRouteManagementState = { mergedRoutes: [], warnings: [], apiTokens: [], isLoading: false, error: null, lastUpdated: 0, }; constructor() { super(); const sub = appstate.routeManagementStatePart .select((s) => s) .subscribe((routeState) => { this.routeState = routeState; }); this.rxSubscriptions.push(sub); // Re-fetch routes when user logs in (fixes race condition where // the view is created before authentication completes) const loginSub = appstate.loginStatePart .select((s) => s.isLoggedIn) .subscribe((isLoggedIn) => { if (isLoggedIn) { appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); } }); this.rxSubscriptions.push(loginSub); } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .routesContainer { display: flex; flex-direction: column; gap: 24px; } .warnings-bar { background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')}; border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')}; border-radius: 8px; padding: 12px 16px; } .warning-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; color: ${cssManager.bdTheme('#b45309', '#fa0')}; } .warning-icon { flex-shrink: 0; } .empty-state { text-align: center; padding: 48px 24px; color: ${cssManager.bdTheme('#6b7280', '#666')}; } .empty-state p { margin: 8px 0; } `, ]; public render(): TemplateResult { const { mergedRoutes, warnings } = this.routeState; const hardcodedCount = mergedRoutes.filter((mr) => mr.source === 'hardcoded').length; const programmaticCount = mergedRoutes.filter((mr) => mr.source === 'programmatic').length; const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length; const statsTiles: IStatsTile[] = [ { id: 'totalRoutes', title: 'Total Routes', type: 'number', value: mergedRoutes.length, icon: 'lucide:route', description: 'All configured routes', color: '#3b82f6', }, { id: 'hardcoded', title: 'Hardcoded', type: 'number', value: hardcodedCount, icon: 'lucide:lock', description: 'Routes from constructor config', color: '#8b5cf6', }, { id: 'programmatic', title: 'Programmatic', type: 'number', value: programmaticCount, icon: 'lucide:code', description: 'Routes added via API', color: '#0ea5e9', }, { id: 'disabled', title: 'Disabled', type: 'number', value: disabledCount, icon: 'lucide:pauseCircle', description: 'Currently disabled routes', color: disabledCount > 0 ? '#ef4444' : '#6b7280', }, ]; // Map merged routes to sz-route-list-view format const szRoutes = mergedRoutes.map((mr) => { const tags = [...(mr.route.tags || [])]; tags.push(mr.source); if (!mr.enabled) tags.push('disabled'); if (mr.overridden) tags.push('overridden'); return { ...mr.route, enabled: mr.enabled, tags, id: mr.storedRouteId || mr.route.name || undefined, }; }); return html` Route Management
this.showCreateRouteDialog(), }, { name: 'Refresh', iconName: 'lucide:refreshCw', action: () => this.refreshData(), }, ]} > ${warnings.length > 0 ? html`
${warnings.map( (w) => html`
${w.message}
`, )}
` : ''} ${szRoutes.length > 0 ? html` this.handleRouteClick(e)} > ` : html`

No routes configured

Add a programmatic route or check your constructor configuration.

`}
`; } private async handleRouteClick(e: CustomEvent) { const clickedRoute = e.detail; if (!clickedRoute) return; // Find the corresponding merged route const merged = this.routeState.mergedRoutes.find( (mr) => mr.route.name === clickedRoute.name, ); if (!merged) return; const { DeesModal } = await import('@design.estate/dees-catalog'); if (merged.source === 'hardcoded') { const menuOptions = merged.enabled ? [ { name: 'Disable Route', iconName: 'lucide:pause', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.setRouteOverrideAction, { routeName: merged.route.name!, enabled: false }, ); await modalArg.destroy(); }, }, { name: 'Close', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, ] : [ { name: 'Enable Route', iconName: 'lucide:play', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.setRouteOverrideAction, { routeName: merged.route.name!, enabled: true }, ); await modalArg.destroy(); }, }, { name: 'Remove Override', iconName: 'lucide:undo', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.removeRouteOverrideAction, merged.route.name!, ); await modalArg.destroy(); }, }, { name: 'Close', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, ]; await DeesModal.createAndShow({ heading: `Route: ${merged.route.name}`, content: html`

Source: hardcoded

Status: ${merged.enabled ? 'Enabled' : 'Disabled (overridden)'}

Hardcoded routes cannot be edited or deleted, but they can be disabled via an override.

`, menuOptions, }); } else { // Programmatic route await DeesModal.createAndShow({ heading: `Route: ${merged.route.name}`, content: html`

Source: programmatic

Status: ${merged.enabled ? 'Enabled' : 'Disabled'}

ID: ${merged.storedRouteId}

`, menuOptions: [ { name: merged.enabled ? 'Disable' : 'Enable', iconName: merged.enabled ? 'lucide:pause' : 'lucide:play', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.toggleRouteAction, { id: merged.storedRouteId!, enabled: !merged.enabled }, ); await modalArg.destroy(); }, }, { name: 'Delete', iconName: 'lucide:trash-2', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.deleteRouteAction, merged.storedRouteId!, ); await modalArg.destroy(); }, }, { name: 'Close', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, ], }); } } private async showCreateRouteDialog() { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Add Programmatic Route', content: html` `, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, { name: 'Create', iconName: 'lucide:plus', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); if (!form) return; const formData = await form.collectFormData(); if (!formData.name || !formData.ports) return; const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p)); const domains = formData.domains ? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean) : undefined; const route: any = { name: formData.name, match: { ports, ...(domains && domains.length > 0 ? { domains } : {}), }, action: { type: 'forward', targets: [ { host: formData.targetHost || 'localhost', port: parseInt(formData.targetPort, 10), }, ], }, }; await appstate.routeManagementStatePart.dispatchAction( appstate.createRouteAction, { route }, ); await modalArg.destroy(); }, }, ], }); } private refreshData() { appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); } async firstUpdated() { await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); } }