2026-02-24 18:15:44 +00:00
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 ,
2026-05-09 20:04:02 +00:00
managedDcRouterStatus : null ,
2026-02-24 18:15:44 +00:00
} ;
@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 ,
2026-04-29 15:57:10 +00:00
css `
.gateway-card {
margin-bottom: 24px;
2026-05-08 19:32:40 +00:00
border: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
2026-04-29 15:57:10 +00:00
border-radius: 12px;
2026-05-08 19:32:40 +00:00
background: ${ cssManager . bdTheme ( '#ffffff' , '#09090b' ) } ;
2026-04-29 15:57:10 +00:00
overflow: hidden;
2026-05-08 19:32:40 +00:00
box-shadow: 0 1px 2px ${ cssManager . bdTheme ( 'rgba(0,0,0,0.04)' , 'rgba(0,0,0,0.2)' ) } ;
2026-04-29 15:57:10 +00:00
}
.gateway-header {
padding: 16px 20px;
2026-05-08 19:32:40 +00:00
border-bottom: 1px solid ${ cssManager . bdTheme ( '#f4f4f5' , '#27272a' ) } ;
background: ${ cssManager . bdTheme ( '#fafafa' , '#101013' ) } ;
2026-04-29 15:57:10 +00:00
}
.gateway-title {
font-size: 15px;
font-weight: 600;
2026-05-08 19:32:40 +00:00
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
2026-04-29 15:57:10 +00:00
}
.gateway-subtitle {
margin-top: 4px;
font-size: 13px;
2026-05-08 19:32:40 +00:00
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
2026-04-29 15:57:10 +00:00
}
.gateway-content {
padding: 20px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
2026-05-09 20:04:02 +00:00
.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;
}
2026-04-29 15:57:10 +00:00
.gateway-field.full {
grid-column: 1 / -1;
}
2026-05-09 22:36:26 +00:00
.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' ) } ;
}
2026-05-08 19:32:40 +00:00
dees-input-text {
2026-04-29 15:57:10 +00:00
width: 100%;
}
.gateway-footer {
display: flex;
justify-content: flex-end;
padding: 0 20px 20px;
}
@media (max-width: 700px) {
.gateway-content {
grid-template-columns: 1fr;
}
2026-05-09 20:04:02 +00:00
.gateway-status-row {
align-items: flex-start;
flex-direction: column;
}
2026-04-29 15:57:10 +00:00
}
` ,
2026-02-24 18:15:44 +00:00
] ;
async connectedCallback() {
super . connectedCallback ( ) ;
await appstate . settingsStatePart . dispatchAction ( appstate . fetchSettingsAction , null ) ;
}
public render ( ) : TemplateResult {
return html `
<ob-sectionheading>Settings</ob-sectionheading>
2026-04-29 15:57:10 +00:00
${ this . renderExternalGatewaySettings ( ) }
2026-02-24 18:15:44 +00:00
<sz-settings-view
.settings= ${ this . settingsState . settings || {
darkMode : true ,
cloudflareToken : '' ,
cloudflareZoneId : '' ,
2026-05-09 20:04:02 +00:00
dcrouterMode : 'managed' ,
dcrouterManagedImage : 'code.foss.global/serve.zone/dcrouter:latest' ,
dcrouterManagedOpsPort : 3300 ,
dcrouterManagedHttpPort : 80 ,
dcrouterManagedHttpsPort : 443 ,
dcrouterManagedDataDir : './.nogit/dcrouter-data' ,
2026-05-09 11:58:51 +00:00
dcrouterGatewayClientId : '' ,
dcrouterWorkHosterId : '' ,
2026-02-24 18:15:44 +00:00
autoRenewCerts : false ,
renewalThreshold : 30 ,
acmeEmail : '' ,
httpPort : 80 ,
httpsPort : 443 ,
forceHttps : false ,
} }
.currentUser= ${ this . loginState . identity ? . username || 'admin' }
@setting-change= ${ ( e : CustomEvent ) = > {
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 ) ;
} }
></sz-settings-view>
` ;
}
2026-04-29 15:57:10 +00:00
private renderExternalGatewaySettings ( ) : TemplateResult {
const settings = this . settingsState . settings ;
2026-05-09 20:04:02 +00:00
const mode = settings ? . dcrouterMode || 'managed' ;
2026-04-29 15:57:10 +00:00
return html `
<section class="gateway-card">
<div class="gateway-header">
2026-05-09 20:04:02 +00:00
<div class="gateway-title">dcrouter Gateway</div>
<div class="gateway-subtitle">Run a local managed dcrouter or delegate routing, DNS, and certificates to an external dcrouter.</div>
</div>
<div class="gateway-mode-row">
${ this . renderModeButton ( 'managed' , 'Managed Local' , mode ) }
${ this . renderModeButton ( 'external' , 'External dcrouter' , mode ) }
${ this . renderModeButton ( 'disabled' , 'Disabled' , mode ) }
2026-04-29 15:57:10 +00:00
</div>
2026-05-09 20:04:02 +00:00
${ mode === 'managed' ? this . renderManagedGatewayStatus ( ) : null }
2026-04-29 15:57:10 +00:00
<div class="gateway-content">
2026-05-09 20:04:02 +00:00
${ 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.' ) }
2026-05-09 22:36:26 +00:00
${ this . renderGatewayReadonly ( 'Gateway Client ID' , settings ? . dcrouterGatewayClientId || settings ? . dcrouterWorkHosterId || 'Created when managed dcrouter starts' , 'Diagnostic only. Onebox manages this local client automatically.' ) }
2026-05-09 20:04:02 +00:00
` : 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 ) }
2026-05-09 22:36:26 +00:00
${ this . renderGatewayReadonly ( 'Gateway Client ID' , settings ? . dcrouterGatewayClientId || settings ? . dcrouterWorkHosterId || 'Derived from token' , 'Configure this in dcrouter Gateway Clients, not in Onebox.' ) }
2026-05-09 20:04:02 +00:00
${ 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 `
<div class="gateway-disabled">dcrouter route delegation is disabled. Onebox will keep using its local SmartProxy directly.</div>
` }
2026-04-29 15:57:10 +00:00
</div>
<div class="gateway-footer">
2026-05-08 19:32:40 +00:00
<dees-button
2026-05-09 20:04:02 +00:00
.text= ${ 'Save dcrouter Settings' }
2026-05-08 19:32:40 +00:00
.type= ${ 'default' }
.icon= ${ 'lucide:Save' }
@click= ${ ( ) = > this . saveExternalGatewaySettings ( ) }
></dees-button>
2026-04-29 15:57:10 +00:00
</div>
</section>
` ;
}
2026-05-09 20:04:02 +00:00
private renderModeButton (
mode : 'managed' | 'external' | 'disabled' ,
label : string ,
activeMode : string ,
) : TemplateResult {
return html `
<button
class="gateway-mode-button ${ activeMode === mode ? 'active' : '' } "
@click= ${ ( ) = > this . updateGatewayDraft ( 'dcrouterMode' , mode ) }
> ${ label } </button>
` ;
}
private renderManagedGatewayStatus ( ) : TemplateResult {
const status = this . settingsState . managedDcRouterStatus ;
const stateText = status ? . running ? ( status . healthy ? 'Running' : 'Starting' ) : 'Stopped' ;
return html `
<div class="gateway-status-row">
<div>
<div class="gateway-status-label">Managed dcrouter</div>
<div class="gateway-status-value"> ${ stateText } ${ status ? . gatewayUrl ? ` at ${ status . gatewayUrl } ` : '' } </div>
${ status ? . message ? html ` <div class="gateway-status-error"> ${ status . message } </div> ` : null }
</div>
<div class="gateway-actions">
<dees-button .text= ${ 'Start' } .type= ${ 'default' } @click= ${ ( ) = > appstate . settingsStatePart . dispatchAction ( appstate . startManagedDcRouterAction , null ) } ></dees-button>
<dees-button .text= ${ 'Restart' } .type= ${ 'default' } @click= ${ ( ) = > appstate . settingsStatePart . dispatchAction ( appstate . restartManagedDcRouterAction , null ) } ></dees-button>
<dees-button .text= ${ 'Stop' } .type= ${ 'default' } @click= ${ ( ) = > appstate . settingsStatePart . dispatchAction ( appstate . stopManagedDcRouterAction , null ) } ></dees-button>
</div>
</div>
` ;
}
2026-04-29 15:57:10 +00:00
private renderGatewayInput (
key : keyof NonNullable < appstate.ISettingsState [ 'settings' ] > ,
label : string ,
value : string ,
hint : string ,
2026-05-08 19:32:40 +00:00
isPassword = false ,
2026-04-29 15:57:10 +00:00
) : TemplateResult {
return html `
2026-05-08 19:32:40 +00:00
<div class="gateway-field ${ key === 'dcrouterGatewayUrl' ? 'full' : '' } ">
<dees-input-text
.key= ${ key }
.label= ${ label }
2026-04-29 15:57:10 +00:00
.value= ${ value }
2026-05-08 19:32:40 +00:00
.description= ${ hint }
.isPasswordBool= ${ isPassword }
2026-04-29 15:57:10 +00:00
@input= ${ ( event : Event ) = > this . updateGatewayDraft ( key , ( event . target as HTMLInputElement ) . value ) }
2026-05-08 19:32:40 +00:00
></dees-input-text>
</div>
2026-04-29 15:57:10 +00:00
` ;
}
2026-05-09 22:36:26 +00:00
private renderGatewayReadonly ( label : string , value : string , hint : string ) : TemplateResult {
return html `
<div class="gateway-readonly">
<div class="gateway-readonly-label"> ${ label } </div>
<div class="gateway-readonly-value"> ${ value } </div>
<div class="gateway-readonly-hint"> ${ hint } </div>
</div>
` ;
}
2026-04-29 15:57:10 +00:00
private updateGatewayDraft (
key : keyof NonNullable < appstate.ISettingsState [ 'settings' ] > ,
value : string ,
) : void {
const currentSettings = this . settingsState . settings || { } as NonNullable < appstate.ISettingsState [ 'settings' ] > ;
2026-05-09 20:04:02 +00:00
const numberKeys = new Set ( [
'dcrouterTargetPort' ,
'dcrouterManagedOpsPort' ,
'dcrouterManagedHttpPort' ,
'dcrouterManagedHttpsPort' ,
] ) ;
const nextValue = numberKeys . has ( key as string ) ? Number ( value ) || 0 : value ;
2026-04-29 15:57:10 +00:00
this . settingsState = {
. . . this . settingsState ,
settings : {
. . . currentSettings ,
[ key ] : nextValue ,
} ,
} ;
}
private async saveExternalGatewaySettings ( ) : Promise < void > {
const settings = this . settingsState . settings ;
if ( ! settings ) return ;
await appstate . settingsStatePart . dispatchAction ( appstate . updateSettingsAction , {
settings : {
2026-05-09 20:04:02 +00:00
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' ,
2026-04-29 15:57:10 +00:00
dcrouterGatewayUrl : settings.dcrouterGatewayUrl || '' ,
dcrouterGatewayApiToken : settings.dcrouterGatewayApiToken || '' ,
dcrouterTargetHost : settings.dcrouterTargetHost || '' ,
dcrouterTargetPort : Number ( settings . dcrouterTargetPort ) || 80 ,
} ,
} ) ;
2026-05-09 20:04:02 +00:00
await appstate . settingsStatePart . dispatchAction ( appstate . fetchManagedDcRouterStatusAction , null ) ;
2026-04-29 15:57:10 +00:00
}
2026-02-24 18:15:44 +00:00
}