import { DeesElement, css, cssManager, customElement, html, property, state, type TemplateResult, } from '@design.estate/dees-element'; import { type IStatsTile } from '@design.estate/dees-catalog'; import * as appstate from '../../appstate.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; import { viewHostCss } from '../shared/css.js'; import { getRouteOwner, routeMatchesOwner, type TRouteOwnerFilter, } from './route-view-filters.js'; import { getSpecialForwardDomains, getSpecialForwardTarget, letsEncryptHttp01ManagedRouteKind, showLetsEncryptHttp01ForwardDialog, } from './special-forward-dialog.js'; @customElement('ops-view-special-forwards') export class OpsViewSpecialForwards extends DeesElement { @property({ type: Boolean, reflect: true }) accessor embedded = false; @property({ type: String }) accessor ownerFilter: TRouteOwnerFilter = 'all'; @state() accessor routeState: appstate.IRouteManagementState = appstate.routeManagementStatePart.getState()!; @state() accessor profilesTargetsState: appstate.IProfilesTargetsState = appstate.profilesTargetsStatePart.getState()!; constructor() { super(); this.rxSubscriptions.push( appstate.routeManagementStatePart.select().subscribe((routeState) => { this.routeState = routeState; }), appstate.profilesTargetsStatePart.select().subscribe((profilesTargetsState) => { this.profilesTargetsState = profilesTargetsState; }), ); } public static styles = [ cssManager.defaultStyles, viewHostCss, css` :host { display: block; } :host([embedded]) { margin: 0; max-width: none; padding: 0; } .specialForwardsContainer { display: flex; flex-direction: column; gap: 20px; } .explanation { padding: 14px 16px; border: 1px solid var(--dees-color-border-default); border-radius: 8px; color: var(--dees-color-text-secondary); font-size: 14px; line-height: 1.5; } .explanation strong { color: var(--dees-color-text-primary); } .error { padding: 12px 16px; border: 1px solid #ef4444; border-radius: 8px; color: #ef4444; } `, ]; public render(): TemplateResult { const forwards = this.routeState.mergedRoutes.filter((route) => { return route.metadata?.managedRouteKind === letsEncryptHttp01ManagedRouteKind && routeMatchesOwner(route, this.ownerFilter); }); const enabledCount = forwards.filter((route) => route.enabled).length; const disabledCount = forwards.length - enabledCount; const readOnlyCount = forwards.filter((route) => getRouteOwner(route) !== 'operator').length; const statsTiles: IStatsTile[] = [ { id: 'matchingSpecialForwards', title: 'Matching', type: 'number', value: forwards.length, icon: 'lucide:route', description: 'HTTP-01 forwards in this owner scope', color: '#3b82f6', }, { id: 'enabledSpecialForwards', title: 'Enabled', type: 'number', value: enabledCount, icon: 'lucide:circleCheck', description: 'Currently forwarding challenges', color: '#22c55e', }, { id: 'disabledSpecialForwards', title: 'Disabled', type: 'number', value: disabledCount, icon: 'lucide:circlePause', description: 'Configured but inactive', color: disabledCount > 0 ? '#f59e0b' : '#6b7280', }, { id: 'readOnlySpecialForwards', title: 'Read-only', type: 'number', value: readOnlyCount, icon: 'lucide:lockKeyhole', description: 'Managed by gateway clients or system', color: '#8b5cf6', }, ]; return html` ${this.embedded ? '' : html`Special Forwards`}
${this.embedded ? '' : html` `}
Let's Encrypt HTTP-01 Forward exposes only /.well-known/acme-challenge/* on public port 80. DCRouter manages the path, priority, protocol, and PUBLIC source profile; you choose domains and a target.
${this.routeState.error ? html`
${this.routeState.error}
` : ''} { const target = getSpecialForwardTarget(route); const owner = getRouteOwner(route); return { Status: route.enabled ? 'Enabled' : 'Disabled', Type: 'Let\'s Encrypt HTTP-01', Owner: owner === 'operator' ? 'User' : owner === 'gatewayClient' ? 'Gateway Client' : 'System', Name: route.route.name || route.id, Domains: getSpecialForwardDomains(route).join(', '), Path: route.route.match.path || '-', Target: route.metadata?.networkTargetName || (target ? `${target.host}:${target.port}` : '-'), 'Remote Ingress': route.route.remoteIngress?.enabled ? 'Enabled' : 'Disabled', }; }} .dataActions=${[ { name: 'Edit', iconName: 'lucide:pencil', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => { return getRouteOwner(route) === 'operator'; }, actionFunc: async (actionData: any) => { await showLetsEncryptHttp01ForwardDialog({ existing: actionData.item as interfaces.data.IMergedRoute, targets: this.profilesTargetsState.targets, }); }, }, { name: 'Enable / Disable', iconName: 'lucide:power', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => { return getRouteOwner(route) === 'operator'; }, actionFunc: async (actionData: any) => { const route = actionData.item as interfaces.data.IMergedRoute; await appstate.routeManagementStatePart.dispatchAction( appstate.toggleRouteAction, { id: route.id, enabled: !route.enabled }, ); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (route: interfaces.data.IMergedRoute) => { return getRouteOwner(route) === 'operator'; }, actionFunc: async (actionData: any) => { await this.confirmDelete(actionData.item as interfaces.data.IMergedRoute); }, }, ]} >
`; } private async refreshData(): Promise { await Promise.all([ appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null), appstate.profilesTargetsStatePart.dispatchAction( appstate.fetchProfilesAndTargetsAction, null, ), ]); } private getHeaderActions() { return [ ...(this.ownerFilter === 'all' || this.ownerFilter === 'operator' ? [{ name: 'Add User Special Forward', iconName: 'lucide:plus', action: async () => showLetsEncryptHttp01ForwardDialog({ targets: this.profilesTargetsState.targets, }), }] : []), { name: 'Refresh', iconName: 'lucide:refreshCw', action: async () => this.refreshData(), }, ]; } private async confirmDelete(route: interfaces.data.IMergedRoute): Promise { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: `Delete Special Forward: ${route.route.name || route.id}`, content: html`

This removes the managed HTTP-01 route for ${getSpecialForwardDomains(route).join(', ')}.

`, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy(), }, { name: 'Delete', iconName: 'lucide:trash2', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.deleteLetsEncryptHttp01ForwardAction, route.id, ); await modalArg.destroy(); }, }, ], }); } }