325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as shared from './shared/index.js';
|
|
import * as appstate from '../appstate.js';
|
|
import { appRouter } from '../router.js';
|
|
|
|
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
state,
|
|
css,
|
|
cssManager,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
|
|
|
|
@customElement('ops-view-config')
|
|
export class OpsViewConfig extends DeesElement {
|
|
@state()
|
|
accessor configState: appstate.IConfigState = {
|
|
config: null,
|
|
isLoading: false,
|
|
error: null,
|
|
};
|
|
|
|
constructor() {
|
|
super();
|
|
const subscription = appstate.configStatePart
|
|
.select((stateArg) => stateArg)
|
|
.subscribe((configState) => {
|
|
this.configState = configState;
|
|
});
|
|
this.rxSubscriptions.push(subscription);
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
shared.viewHostCss,
|
|
css`
|
|
.loadingMessage {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
}
|
|
|
|
.errorMessage {
|
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
|
|
border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
margin: 16px 0;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render() {
|
|
return html`
|
|
<ops-sectionheading>Configuration</ops-sectionheading>
|
|
|
|
${this.configState.isLoading
|
|
? html`
|
|
<div class="loadingMessage">
|
|
<dees-spinner></dees-spinner>
|
|
<p>Loading configuration...</p>
|
|
</div>
|
|
`
|
|
: this.configState.error
|
|
? html`
|
|
<div class="errorMessage">
|
|
Error loading configuration: ${this.configState.error}
|
|
</div>
|
|
`
|
|
: this.configState.config
|
|
? this.renderConfig()
|
|
: html`<div class="errorMessage">No configuration loaded</div>`}
|
|
`;
|
|
}
|
|
|
|
private renderConfig(): TemplateResult {
|
|
const cfg = this.configState.config!;
|
|
|
|
return html`
|
|
<sz-config-overview
|
|
infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
|
|
@navigate=${(e: CustomEvent) => {
|
|
if (e.detail?.view) {
|
|
appRouter.navigateToView(e.detail.view);
|
|
}
|
|
}}
|
|
>
|
|
${this.renderSystemSection(cfg.system)}
|
|
${this.renderSmartProxySection(cfg.smartProxy)}
|
|
${this.renderEmailSection(cfg.email)}
|
|
${this.renderDnsSection(cfg.dns)}
|
|
${this.renderTlsSection(cfg.tls)}
|
|
${this.renderCacheSection(cfg.cache)}
|
|
${this.renderRadiusSection(cfg.radius)}
|
|
${this.renderRemoteIngressSection(cfg.remoteIngress)}
|
|
</sz-config-overview>
|
|
`;
|
|
}
|
|
|
|
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Base Directory', value: sys.baseDir },
|
|
{ key: 'Data Directory', value: sys.dataDir },
|
|
{ key: 'Public IP', value: sys.publicIp },
|
|
{ key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' },
|
|
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
|
|
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
|
|
{ key: 'Storage Path', value: sys.storagePath },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="System"
|
|
subtitle="Base paths and infrastructure"
|
|
icon="lucide:server"
|
|
status="enabled"
|
|
.fields=${fields}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Route Count', value: proxy.routeCount },
|
|
];
|
|
|
|
if (proxy.acme) {
|
|
fields.push(
|
|
{ key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
|
|
{ key: 'Account Email', value: proxy.acme.accountEmail || null },
|
|
{ key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
|
|
{ key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
|
|
{ key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
|
|
);
|
|
}
|
|
|
|
const actions: IConfigSectionAction[] = [
|
|
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="SmartProxy"
|
|
subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
|
|
icon="lucide:network"
|
|
.status=${proxy.enabled ? 'enabled' : 'disabled'}
|
|
.fields=${fields}
|
|
.actions=${actions}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
|
|
{ key: 'Hostname', value: email.hostname },
|
|
{ key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
|
|
{ key: 'Email Routes', value: email.emailRouteCount },
|
|
{ key: 'Received Emails Path', value: email.receivedEmailsPath },
|
|
];
|
|
|
|
if (email.portMapping) {
|
|
const mappingStr = Object.entries(email.portMapping)
|
|
.map(([ext, int]) => `${ext} → ${int}`)
|
|
.join(', ');
|
|
fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
|
|
}
|
|
|
|
const actions: IConfigSectionAction[] = [
|
|
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="Email Server"
|
|
subtitle="SMTP email handling with smartmta"
|
|
icon="lucide:mail"
|
|
.status=${email.enabled ? 'enabled' : 'disabled'}
|
|
.fields=${fields}
|
|
.actions=${actions}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Port', value: dns.port },
|
|
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
|
|
{ key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
|
|
{ key: 'Record Count', value: dns.recordCount },
|
|
{ key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="DNS Server"
|
|
subtitle="Authoritative DNS with smartdns"
|
|
icon="lucide:globe"
|
|
.status=${dns.enabled ? 'enabled' : 'disabled'}
|
|
.fields=${fields}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Contact Email', value: tls.contactEmail },
|
|
{ key: 'Domain', value: tls.domain },
|
|
{ key: 'Source', value: tls.source, type: 'badge' },
|
|
{ key: 'Certificate Path', value: tls.certPath },
|
|
{ key: 'Key Path', value: tls.keyPath },
|
|
];
|
|
|
|
const status = tls.source === 'none' ? 'not-configured' : 'enabled';
|
|
const actions: IConfigSectionAction[] = [
|
|
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="TLS / Certificates"
|
|
subtitle="Certificate management and ACME"
|
|
icon="lucide:shield-check"
|
|
.status=${status as any}
|
|
.fields=${fields}
|
|
.actions=${actions}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Storage Path', value: cache.storagePath },
|
|
{ key: 'DB Name', value: cache.dbName },
|
|
{ key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
|
|
{ key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
|
|
];
|
|
|
|
if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
|
|
for (const [key, val] of Object.entries(cache.ttlConfig)) {
|
|
fields.push({ key: `TTL: ${key}`, value: `${val} days` });
|
|
}
|
|
}
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="Cache Database"
|
|
subtitle="Persistent caching with smartdata"
|
|
icon="lucide:database"
|
|
.status=${cache.enabled ? 'enabled' : 'disabled'}
|
|
.fields=${fields}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Auth Port', value: radius.authPort },
|
|
{ key: 'Accounting Port', value: radius.acctPort },
|
|
{ key: 'Bind Address', value: radius.bindAddress },
|
|
{ key: 'Client Count', value: radius.clientCount },
|
|
];
|
|
|
|
if (radius.vlanDefaultVlan !== null) {
|
|
fields.push(
|
|
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
|
|
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
|
|
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
|
|
);
|
|
}
|
|
|
|
const status = radius.enabled ? 'enabled' : 'not-configured';
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="RADIUS Server"
|
|
subtitle="Network authentication and VLAN assignment"
|
|
icon="lucide:wifi"
|
|
.status=${status as any}
|
|
.fields=${fields}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
|
|
const fields: IConfigField[] = [
|
|
{ key: 'Tunnel Port', value: ri.tunnelPort },
|
|
{ key: 'Hub Domain', value: ri.hubDomain },
|
|
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
|
|
];
|
|
|
|
const actions: IConfigSectionAction[] = [
|
|
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
|
|
];
|
|
|
|
return html`
|
|
<sz-config-section
|
|
title="Remote Ingress"
|
|
subtitle="Edge tunnel nodes"
|
|
icon="lucide:cloud"
|
|
.status=${ri.enabled ? 'enabled' : 'disabled'}
|
|
.fields=${fields}
|
|
.actions=${actions}
|
|
></sz-config-section>
|
|
`;
|
|
}
|
|
|
|
private formatUptime(seconds: number): string {
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
|
|
const parts: string[] = [];
|
|
if (days > 0) parts.push(`${days}d`);
|
|
if (hours > 0) parts.push(`${hours}h`);
|
|
parts.push(`${mins}m`);
|
|
return parts.join(' ');
|
|
}
|
|
}
|