|
|
|
|
@@ -1,6 +1,7 @@
|
|
|
|
|
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,
|
|
|
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
|
|
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()
|
|
|
|
|
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
shared.viewHostCss,
|
|
|
|
|
css`
|
|
|
|
|
.configSection {
|
|
|
|
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sectionHeader {
|
|
|
|
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sectionTitle {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sectionTitle dees-icon {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#888')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sectionContent {
|
|
|
|
|
padding: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.configField {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.configField:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fieldLabel {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
display: block;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fieldValue {
|
|
|
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
|
|
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fieldValue.empty {
|
|
|
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nestedFields {
|
|
|
|
|
margin-left: 16px;
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Status badge styles */
|
|
|
|
|
.statusBadge {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.statusBadge.enabled {
|
|
|
|
|
background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')};
|
|
|
|
|
color: ${cssManager.bdTheme('#155724', '#66cc66')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.statusBadge.disabled {
|
|
|
|
|
background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')};
|
|
|
|
|
color: ${cssManager.bdTheme('#721c24', '#cc6666')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.statusBadge dees-icon {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Array/list display */
|
|
|
|
|
.arrayItems {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arrayItem {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
|
|
|
|
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arrayCount {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Numeric value formatting */
|
|
|
|
|
.numericValue {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.errorMessage {
|
|
|
|
|
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loadingMessage {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.infoNote {
|
|
|
|
|
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')};
|
|
|
|
|
.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;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
color: ${cssManager.bdTheme('#004085', '#88ccff')};
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.infoNote dees-icon {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
@@ -202,185 +59,266 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
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 ? html`
|
|
|
|
|
<div class="infoNote">
|
|
|
|
|
<dees-icon icon="lucide:info"></dees-icon>
|
|
|
|
|
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)}
|
|
|
|
|
${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)}
|
|
|
|
|
${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)}
|
|
|
|
|
${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)}
|
|
|
|
|
` : html`
|
|
|
|
|
<div class="errorMessage">No configuration loaded</div>
|
|
|
|
|
`}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderConfigSection(key: string, title: string, icon: string, config: any) {
|
|
|
|
|
const isEnabled = config?.enabled ?? false;
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configSection">
|
|
|
|
|
<div class="sectionHeader">
|
|
|
|
|
<h3 class="sectionTitle">
|
|
|
|
|
<dees-icon icon="${icon}"></dees-icon>
|
|
|
|
|
${title}
|
|
|
|
|
</h3>
|
|
|
|
|
${this.renderStatusBadge(isEnabled)}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sectionContent">
|
|
|
|
|
${config ? this.renderConfigFields(config) : html`
|
|
|
|
|
<div class="fieldValue empty">Not configured</div>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderStatusBadge(enabled: boolean): TemplateResult {
|
|
|
|
|
return enabled
|
|
|
|
|
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
|
|
|
|
|
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
|
|
|
|
|
if (!config || typeof config !== 'object') {
|
|
|
|
|
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.entries(config).map(([key, value]) => {
|
|
|
|
|
const fieldName = prefix ? `${prefix}.${key}` : key;
|
|
|
|
|
const displayName = this.formatFieldName(key);
|
|
|
|
|
|
|
|
|
|
// Handle boolean values with badges
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${displayName}</label>
|
|
|
|
|
${this.renderStatusBadge(value)}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle arrays
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${displayName}</label>
|
|
|
|
|
${this.renderArrayValue(value, key)}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle nested objects
|
|
|
|
|
if (typeof value === 'object' && value !== null) {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${displayName}</label>
|
|
|
|
|
<div class="nestedFields">
|
|
|
|
|
${this.renderConfigFields(value, fieldName)}
|
|
|
|
|
${this.configState.isLoading
|
|
|
|
|
? html`
|
|
|
|
|
<div class="loadingMessage">
|
|
|
|
|
<dees-spinner></dees-spinner>
|
|
|
|
|
<p>Loading configuration...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle primitive values
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${displayName}</label>
|
|
|
|
|
<div class="fieldValue">${this.formatValue(value, key)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
|
|
|
|
|
if (arr.length === 0) {
|
|
|
|
|
return html`<div class="fieldValue empty">None configured</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine if we should show as pills/tags
|
|
|
|
|
const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number');
|
|
|
|
|
|
|
|
|
|
if (showAsPills) {
|
|
|
|
|
const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
|
|
|
|
|
return html`
|
|
|
|
|
<div class="arrayCount">${arr.length} ${itemLabel}</div>
|
|
|
|
|
<div class="arrayItems">
|
|
|
|
|
${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For complex arrays, show as JSON
|
|
|
|
|
return html`
|
|
|
|
|
<div class="fieldValue">
|
|
|
|
|
${arr.length} items configured
|
|
|
|
|
</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 getArrayItemLabel(fieldKey: string, count: number): string {
|
|
|
|
|
const labels: Record<string, [string, string]> = {
|
|
|
|
|
ports: ['port', 'ports'],
|
|
|
|
|
domains: ['domain', 'domains'],
|
|
|
|
|
nameservers: ['nameserver', 'nameservers'],
|
|
|
|
|
blockList: ['IP', 'IPs'],
|
|
|
|
|
};
|
|
|
|
|
private renderConfig(): TemplateResult {
|
|
|
|
|
const cfg = this.configState.config!;
|
|
|
|
|
|
|
|
|
|
const label = labels[fieldKey] || ['item', 'items'];
|
|
|
|
|
return count === 1 ? label[0] : label[1];
|
|
|
|
|
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 formatFieldName(key: string): string {
|
|
|
|
|
// Convert camelCase to readable format
|
|
|
|
|
return key
|
|
|
|
|
.replace(/([A-Z])/g, ' $1')
|
|
|
|
|
.replace(/^./, str => str.toUpperCase())
|
|
|
|
|
.trim();
|
|
|
|
|
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 formatValue(value: any, fieldKey?: string): string | TemplateResult {
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
|
return html`<span class="empty">Not set</span>`;
|
|
|
|
|
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` },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'number') {
|
|
|
|
|
// Format bytes
|
|
|
|
|
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) {
|
|
|
|
|
return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
|
|
|
|
|
}
|
|
|
|
|
// Format time values
|
|
|
|
|
if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
|
|
|
|
|
return html`<span class="numericValue">${value} seconds</span>`;
|
|
|
|
|
}
|
|
|
|
|
// Format port numbers
|
|
|
|
|
if (fieldKey?.toLowerCase().includes('port')) {
|
|
|
|
|
return html`<span class="numericValue">${value}</span>`;
|
|
|
|
|
}
|
|
|
|
|
// Format counts with separators
|
|
|
|
|
return html`<span class="numericValue">${value.toLocaleString()}</span>`;
|
|
|
|
|
}
|
|
|
|
|
const actions: IConfigSectionAction[] = [
|
|
|
|
|
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return String(value);
|
|
|
|
|
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 formatBytes(bytes: number): string {
|
|
|
|
|
if (bytes === 0) return '0 B';
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
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(' ');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|