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` dees-tile { display: block; margin-bottom: 24px; } .gateway-header { height: 36px; display: flex; align-items: center; padding: 0 16px; width: 100%; box-sizing: border-box; } .gateway-heading { flex: 1; display: flex; align-items: baseline; gap: 8px; min-width: 0; } .gateway-title { font-size: 13px; font-weight: 500; letter-spacing: -0.01em; color: var(--dees-color-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .gateway-subtitle { font-size: 12px; color: var(--dees-color-text-muted); letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .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; flex-direction: row; justify-content: flex-end; align-items: center; gap: 0; height: 36px; width: 100%; box-sizing: border-box; } .tile-button { padding: 0 16px; height: 100%; text-align: center; font-size: 12px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.15s ease; background: transparent; border: none; border-left: 1px solid var(--dees-color-border-subtle); color: var(--dees-color-text-muted); white-space: nowrap; display: flex; align-items: center; gap: 6px; } .tile-button:first-child { border-left: none; } .tile-button:hover { background: var(--dees-color-hover); color: var(--dees-color-text-primary); } .tile-button.primary { color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; font-weight: 600; } .tile-button.primary:hover { background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')}; color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')}; } @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.renderAdminUiSettings()} ${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 renderAdminUiSettings(): TemplateResult { const settings = this.settingsState.settings; return html`
Onebox Admin UI Configure the public hostname for this Onebox dashboard
${this.renderGatewayInput('adminUiDomain', 'Admin UI Domain', settings?.adminUiDomain || '', 'Example: onebox.example.com. Leave empty to disable the public Admin UI route.')} ${this.renderGatewayReadonly('Local Target', 'Onebox OpsServer on port 3000', 'The external gateway forwards to SmartProxy, which forwards this hostname to the Onebox Admin UI.')}
`; } 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 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); } private async saveAdminUiSettings(): Promise { const settings = this.settingsState.settings; if (!settings) return; await appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, { settings: { adminUiDomain: settings.adminUiDomain || '', }, }); } }