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 ,
} ;
@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;
border: 1px solid var(--dees-color-border-subtle);
border-radius: 12px;
background: var(--dees-color-background, #ffffff);
overflow: hidden;
}
.gateway-header {
padding: 16px 20px;
border-bottom: 1px solid var(--dees-color-border-subtle);
}
.gateway-title {
font-size: 15px;
font-weight: 600;
color: var(--dees-color-text-primary);
}
.gateway-subtitle {
margin-top: 4px;
font-size: 13px;
color: var(--dees-color-text-muted);
}
.gateway-content {
padding: 20px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.gateway-field.full {
grid-column: 1 / -1;
}
.field-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--dees-color-text-secondary);
}
input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border: 1px solid var(--dees-color-border-subtle);
border-radius: 8px;
background: transparent;
color: var(--dees-color-text-primary);
font-size: 14px;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
.field-hint {
margin-top: 5px;
font-size: 12px;
color: var(--dees-color-text-muted);
}
.gateway-footer {
display: flex;
justify-content: flex-end;
padding: 0 20px 20px;
}
.save-button {
border: none;
border-radius: 8px;
background: #2563eb;
color: white;
cursor: pointer;
font-size: 13px;
font-weight: 600;
padding: 9px 14px;
}
.save-button:hover {
background: #1d4ed8;
}
@media (max-width: 700px) {
.gateway-content {
grid-template-columns: 1fr;
}
}
` ,
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 : '' ,
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 ;
return html `
<section class="gateway-card">
<div class="gateway-header">
<div class="gateway-title">External dcrouter Gateway</div>
<div class="gateway-subtitle">Delegate public WorkApp routing, DNS, and certificates to a dcrouter edge authority.</div>
</div>
<div class="gateway-content">
${ this . renderGatewayInput ( 'dcrouterGatewayUrl' , 'Gateway URL' , settings ? . dcrouterGatewayUrl || '' , 'https://edge.example.com' , 'Base URL of the dcrouter OpsServer.' ) }
${ this . renderGatewayInput ( 'dcrouterGatewayApiToken' , 'API Token' , settings ? . dcrouterGatewayApiToken || '' , 'dcrouter API token' , 'Requires workhosters and certificates scopes.' , 'password' ) }
${ this . renderGatewayInput ( 'dcrouterWorkHosterId' , 'WorkHoster ID' , settings ? . dcrouterWorkHosterId || '' , 'optional stable owner ID' , 'Leave empty to let Onebox create a stable ID.' ) }
${ this . renderGatewayInput ( 'dcrouterTargetHost' , 'Target Host' , settings ? . dcrouterTargetHost || '' , 'public or private host/IP' , 'Defaults to the configured server IP when empty.' ) }
${ this . renderGatewayInput ( 'dcrouterTargetPort' , 'Target Port' , String ( settings ? . dcrouterTargetPort || 80 ) , '80' , 'Internal HTTP port dcrouter forwards to.' , 'number' ) }
</div>
<div class="gateway-footer">
<button class="save-button" @click= ${ ( ) = > this . saveExternalGatewaySettings ( ) } >Save Gateway Settings</button>
</div>
</section>
` ;
}
private renderGatewayInput (
key : keyof NonNullable < appstate.ISettingsState [ 'settings' ] > ,
label : string ,
value : string ,
placeholder : string ,
hint : string ,
type : 'text' | 'password' | 'number' = 'text' ,
) : TemplateResult {
return html `
<label class="gateway-field ${ key === 'dcrouterGatewayUrl' ? 'full' : '' } ">
<span class="field-label"> ${ label } </span>
<input
type= ${ type }
.value= ${ value }
placeholder= ${ placeholder }
@input= ${ ( event : Event ) = > this . updateGatewayDraft ( key , ( event . target as HTMLInputElement ) . value ) }
/>
<span class="field-hint"> ${ hint } </span>
</label>
` ;
}
private updateGatewayDraft (
key : keyof NonNullable < appstate.ISettingsState [ 'settings' ] > ,
value : string ,
) : void {
const currentSettings = this . settingsState . settings || { } as NonNullable < appstate.ISettingsState [ 'settings' ] > ;
const nextValue = key === 'dcrouterTargetPort' ? Number ( value ) || 0 : value ;
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 : {
dcrouterGatewayUrl : settings.dcrouterGatewayUrl || '' ,
dcrouterGatewayApiToken : settings.dcrouterGatewayApiToken || '' ,
dcrouterWorkHosterId : settings.dcrouterWorkHosterId || '' ,
dcrouterTargetHost : settings.dcrouterTargetHost || '' ,
dcrouterTargetPort : Number ( settings . dcrouterTargetPort ) || 80 ,
} ,
} ) ;
}
2026-02-24 18:15:44 +00:00
}