import * as plugins from '../plugins.js'; import * as shared from './shared/index.js'; import * as appstate from '../appstate.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; @customElement('ob-view-settings') export class ObViewSettings extends DeesElement { @state() accessor settingsState: appstate.ISettingsState = { settings: null, backupPasswordConfigured: false, managedDcRouterStatus: null, }; @state() accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false, }; constructor() { super(); const settingsSub = appstate.settingsStatePart .select((s) => s) .subscribe((newState) => { this.settingsState = newState; }); this.rxSubscriptions.push(settingsSub); const loginSub = appstate.loginStatePart .select((s) => s) .subscribe((newState) => { this.loginState = newState; }); this.rxSubscriptions.push(loginSub); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .gateway-card { margin-bottom: 24px; border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; border-radius: 12px; background: ${cssManager.bdTheme('#ffffff', '#09090b')}; overflow: hidden; box-shadow: 0 1px 2px ${cssManager.bdTheme('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.2)')}; } .gateway-header { padding: 16px 20px; border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')}; background: ${cssManager.bdTheme('#fafafa', '#101013')}; } .gateway-title { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; } .gateway-subtitle { margin-top: 4px; font-size: 13px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } .gateway-content { padding: 20px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } .gateway-mode-row, .gateway-status-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')}; } .gateway-mode-row { justify-content: flex-start; } .gateway-mode-button { border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; border-radius: 999px; background: ${cssManager.bdTheme('#ffffff', '#18181b')}; color: ${cssManager.bdTheme('#3f3f46', '#d4d4d8')}; padding: 8px 12px; font: inherit; cursor: pointer; } .gateway-mode-button.active { border-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1d4ed8', '#bfdbfe')}; } .gateway-status-label { font-size: 12px; font-weight: 600; text-transform: uppercase; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } .gateway-status-value { margin-top: 4px; font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; } .gateway-status-error, .gateway-disabled { color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')}; font-size: 13px; } .gateway-disabled { grid-column: 1 / -1; } .gateway-actions { display: flex; gap: 8px; } .gateway-field.full { grid-column: 1 / -1; } .gateway-readonly { padding: 10px 12px; border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; border-radius: 8px; background: ${cssManager.bdTheme('#fafafa', '#18181b')}; } .gateway-readonly-label { font-size: 12px; font-weight: 600; color: ${cssManager.bdTheme('#52525b', '#d4d4d8')}; } .gateway-readonly-value { margin-top: 4px; font-size: 13px; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; word-break: break-all; } .gateway-readonly-hint { margin-top: 4px; font-size: 12px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; } dees-input-text { width: 100%; } .gateway-footer { display: flex; justify-content: flex-end; padding: 0 20px 20px; } @media (max-width: 700px) { .gateway-content { grid-template-columns: 1fr; } .gateway-status-row { align-items: flex-start; flex-direction: column; } } `, ]; async connectedCallback() { super.connectedCallback(); await appstate.settingsStatePart.dispatchAction(appstate.fetchSettingsAction, null); } public render(): TemplateResult { return html` Settings ${this.renderExternalGatewaySettings()} { const { key, value } = e.detail; appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, { settings: { [key]: value }, }); }} @save=${(e: CustomEvent) => { appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, { settings: e.detail, }); }} @change-password=${(e: CustomEvent) => { console.log('Change password requested:', e.detail); }} @reset=${() => { appstate.settingsStatePart.dispatchAction(appstate.fetchSettingsAction, null); }} > `; } private renderExternalGatewaySettings(): TemplateResult { const settings = this.settingsState.settings; const mode = settings?.dcrouterMode || 'managed'; return html`
dcrouter Gateway
Run a local managed dcrouter or delegate routing, DNS, and certificates to an external dcrouter.
${this.renderModeButton('managed', 'Managed Local', mode)} ${this.renderModeButton('external', 'External dcrouter', mode)} ${this.renderModeButton('disabled', 'Disabled', mode)}
${mode === 'managed' ? this.renderManagedGatewayStatus() : null}
${mode === 'managed' ? html` ${this.renderGatewayInput('dcrouterManagedImage', 'dcrouter Image', settings?.dcrouterManagedImage || 'code.foss.global/serve.zone/dcrouter:latest', 'OCI image used for the managed local gateway.')} ${this.renderGatewayInput('dcrouterManagedDataDir', 'Data Directory', settings?.dcrouterManagedDataDir || './.nogit/dcrouter-data', 'Host directory mounted into the dcrouter container.')} ${this.renderGatewayInput('dcrouterManagedOpsPort', 'Local Ops Port', String(settings?.dcrouterManagedOpsPort || 3300), 'Bound to 127.0.0.1 for Onebox to call dcrouter APIs.')} ${this.renderGatewayInput('dcrouterManagedHttpPort', 'Public HTTP Port', String(settings?.dcrouterManagedHttpPort || 80), 'Host port owned by dcrouter for HTTP ingress.')} ${this.renderGatewayInput('dcrouterManagedHttpsPort', 'Public HTTPS Port', String(settings?.dcrouterManagedHttpsPort || 443), 'Host port owned by dcrouter for HTTPS ingress.')} ${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || 'Created when managed dcrouter starts', 'Diagnostic only. Onebox manages this local client automatically.')} ` : mode === 'external' ? html` ${this.renderGatewayInput('dcrouterGatewayUrl', 'Gateway URL', settings?.dcrouterGatewayUrl || '', 'Base URL of the dcrouter OpsServer.')} ${this.renderGatewayInput('dcrouterGatewayApiToken', 'API Token', settings?.dcrouterGatewayApiToken || '', 'Requires gateway-client access in dcrouter.', true)} ${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || 'Derived from token', 'Configure this in dcrouter Gateway Clients, not in Onebox.')} ${this.renderGatewayInput('dcrouterTargetHost', 'Target Host', settings?.dcrouterTargetHost || '', 'Defaults to the configured server IP when empty.')} ${this.renderGatewayInput('dcrouterTargetPort', 'Target Port', String(settings?.dcrouterTargetPort || 80), 'Internal HTTP port dcrouter forwards to.')} ` : html`
dcrouter route delegation is disabled. Onebox will keep using its local SmartProxy directly.
`}
`; } private renderModeButton( mode: 'managed' | 'external' | 'disabled', label: string, activeMode: string, ): TemplateResult { return html` `; } private renderManagedGatewayStatus(): TemplateResult { const status = this.settingsState.managedDcRouterStatus; const stateText = status?.running ? (status.healthy ? 'Running' : 'Starting') : 'Stopped'; return html`
Managed dcrouter
${stateText}${status?.gatewayUrl ? ` at ${status.gatewayUrl}` : ''}
${status?.message ? html`
${status.message}
` : null}
appstate.settingsStatePart.dispatchAction(appstate.startManagedDcRouterAction, null)}> appstate.settingsStatePart.dispatchAction(appstate.restartManagedDcRouterAction, null)}> appstate.settingsStatePart.dispatchAction(appstate.stopManagedDcRouterAction, null)}>
`; } private renderGatewayInput( key: keyof NonNullable, label: string, value: string, hint: string, isPassword = false, ): TemplateResult { return html`
this.updateGatewayDraft(key, (event.target as HTMLInputElement).value)} >
`; } private renderGatewayReadonly(label: string, value: string, hint: string): TemplateResult { return html`
${label}
${value}
${hint}
`; } private updateGatewayDraft( key: keyof NonNullable, value: string, ): void { const currentSettings = this.settingsState.settings || {} as NonNullable; const numberKeys = new Set([ 'dcrouterTargetPort', 'dcrouterManagedOpsPort', 'dcrouterManagedHttpPort', 'dcrouterManagedHttpsPort', ]); const nextValue = numberKeys.has(key as string) ? Number(value) || 0 : value; this.settingsState = { ...this.settingsState, settings: { ...currentSettings, [key]: nextValue, }, }; } private async saveExternalGatewaySettings(): Promise { const settings = this.settingsState.settings; if (!settings) return; await appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, { settings: { dcrouterMode: settings.dcrouterMode || 'managed', dcrouterManagedImage: settings.dcrouterManagedImage || 'code.foss.global/serve.zone/dcrouter:latest', dcrouterManagedOpsPort: Number(settings.dcrouterManagedOpsPort) || 3300, dcrouterManagedHttpPort: Number(settings.dcrouterManagedHttpPort) || 80, dcrouterManagedHttpsPort: Number(settings.dcrouterManagedHttpsPort) || 443, dcrouterManagedDataDir: settings.dcrouterManagedDataDir || './.nogit/dcrouter-data', dcrouterGatewayUrl: settings.dcrouterGatewayUrl || '', dcrouterGatewayApiToken: settings.dcrouterGatewayApiToken || '', dcrouterTargetHost: settings.dcrouterTargetHost || '', dcrouterTargetPort: Number(settings.dcrouterTargetPort) || 80, }, }); await appstate.settingsStatePart.dispatchAction(appstate.fetchManagedDcRouterStatusAction, null); } }