@serve.zone/dcrouter 8.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist_serve/bundle.js +2420 -1227
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -0
  4. package/dist_ts/classes.dcrouter.js +27 -1
  5. package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
  6. package/dist_ts/config/classes.api-token-manager.js +134 -0
  7. package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
  8. package/dist_ts/config/classes.route-config-manager.js +231 -0
  9. package/dist_ts/config/index.d.ts +2 -0
  10. package/dist_ts/config/index.js +3 -1
  11. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  12. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  13. package/dist_ts/opsserver/handlers/{config.handler.d.ts → api-token.handler.d.ts} +5 -2
  14. package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
  15. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  16. package/dist_ts/opsserver/handlers/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/route-management.handler.d.ts +13 -0
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/route-management.d.ts +68 -0
  22. package/dist_ts_interfaces/data/route-management.js +2 -0
  23. package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
  24. package/dist_ts_interfaces/requests/api-tokens.js +2 -0
  25. package/dist_ts_interfaces/requests/config.d.ts +77 -1
  26. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  27. package/dist_ts_interfaces/requests/index.js +3 -1
  28. package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
  29. package/dist_ts_interfaces/requests/route-management.js +2 -0
  30. package/dist_ts_web/00_commitinfo_data.js +1 -1
  31. package/dist_ts_web/appstate.d.ts +37 -1
  32. package/dist_ts_web/appstate.js +220 -2
  33. package/dist_ts_web/elements/index.d.ts +2 -0
  34. package/dist_ts_web/elements/index.js +3 -1
  35. package/dist_ts_web/elements/ops-dashboard.js +23 -3
  36. package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
  37. package/dist_ts_web/elements/ops-view-apitokens.js +310 -0
  38. package/dist_ts_web/elements/ops-view-config.d.ts +10 -8
  39. package/dist_ts_web/elements/ops-view-config.js +215 -297
  40. package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
  41. package/dist_ts_web/elements/ops-view-routes.js +404 -0
  42. package/dist_ts_web/router.d.ts +1 -1
  43. package/dist_ts_web/router.js +2 -2
  44. package/package.json +2 -2
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dcrouter.ts +37 -1
  47. package/ts/config/classes.api-token-manager.ts +155 -0
  48. package/ts/config/classes.route-config-manager.ts +271 -0
  49. package/ts/config/index.ts +3 -1
  50. package/ts/opsserver/classes.opsserver.ts +4 -0
  51. package/ts/opsserver/handlers/api-token.handler.ts +96 -0
  52. package/ts/opsserver/handlers/config.handler.ts +154 -72
  53. package/ts/opsserver/handlers/index.ts +3 -1
  54. package/ts/opsserver/handlers/route-management.handler.ts +163 -0
  55. package/ts_web/00_commitinfo_data.ts +1 -1
  56. package/ts_web/appstate.ts +309 -2
  57. package/ts_web/elements/index.ts +2 -0
  58. package/ts_web/elements/ops-dashboard.ts +22 -2
  59. package/ts_web/elements/ops-view-apitokens.ts +285 -0
  60. package/ts_web/elements/ops-view-config.ts +237 -299
  61. package/ts_web/elements/ops-view-routes.ts +389 -0
  62. package/ts_web/router.ts +1 -1
  63. package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
  64. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  65. package/dist_ts/cache/classes.cached.document.d.ts +0 -76
  66. package/dist_ts/cache/classes.cached.document.js +0 -100
  67. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  68. package/dist_ts/cache/classes.cachedb.js +0 -126
  69. package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
  70. package/dist_ts/cache/documents/classes.cached.email.js +0 -337
  71. package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
  72. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
  73. package/dist_ts/cache/documents/index.d.ts +0 -2
  74. package/dist_ts/cache/documents/index.js +0 -3
  75. package/dist_ts/cache/index.d.ts +0 -4
  76. package/dist_ts/cache/index.js +0 -7
  77. package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
  78. package/dist_ts/monitoring/classes.metricscache.js +0 -63
  79. package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
  80. package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
  81. package/dist_ts/opsserver/handlers/config.handler.js +0 -67
  82. package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
  83. package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
  84. package/dist_ts/security/classes.securitylogger.js +0 -235
  85. package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
  86. package/dist_ts/storage/classes.storagemanager.js +0 -344
  87. package/dist_ts/storage/index.d.ts +0 -1
  88. package/dist_ts/storage/index.js +0 -3
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import * as shared from './shared/index.js';
3
3
  import * as appstate from '../appstate.js';
4
+ import { appRouter } from '../router.js';
4
5
 
5
6
  import {
6
7
  DeesElement,
@@ -12,6 +13,8 @@ import {
12
13
  type TemplateResult,
13
14
  } from '@design.estate/dees-element';
14
15
 
16
+ import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
17
+
15
18
  @customElement('ops-view-config')
16
19
  export class OpsViewConfig extends DeesElement {
17
20
  @state()
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
35
38
  cssManager.defaultStyles,
36
39
  shared.viewHostCss,
37
40
  css`
38
- .configSection {
39
- background: ${cssManager.bdTheme('#fff', '#222')};
40
- border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
41
- border-radius: 8px;
42
- margin-bottom: 24px;
43
- overflow: hidden;
44
- }
45
-
46
- .sectionHeader {
47
- background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
48
- padding: 16px 24px;
49
- border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
50
- display: flex;
51
- justify-content: space-between;
52
- align-items: center;
53
- }
54
-
55
- .sectionTitle {
56
- font-size: 18px;
57
- font-weight: 600;
58
- color: ${cssManager.bdTheme('#333', '#ccc')};
59
- display: flex;
60
- align-items: center;
61
- gap: 12px;
62
- }
63
-
64
- .sectionTitle dees-icon {
65
- font-size: 20px;
66
- color: ${cssManager.bdTheme('#666', '#888')};
67
- }
68
-
69
- .sectionContent {
70
- padding: 24px;
71
- }
72
-
73
- .configField {
74
- margin-bottom: 20px;
75
- }
76
-
77
- .configField:last-child {
78
- margin-bottom: 0;
79
- }
80
-
81
- .fieldLabel {
82
- font-size: 13px;
83
- font-weight: 600;
84
- color: ${cssManager.bdTheme('#666', '#999')};
85
- margin-bottom: 8px;
86
- display: block;
87
- text-transform: uppercase;
88
- letter-spacing: 0.5px;
89
- }
90
-
91
- .fieldValue {
92
- font-family: 'Consolas', 'Monaco', monospace;
93
- font-size: 14px;
94
- color: ${cssManager.bdTheme('#333', '#ccc')};
95
- background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
96
- padding: 10px 14px;
97
- border-radius: 6px;
98
- border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
99
- }
100
-
101
- .fieldValue.empty {
102
- color: ${cssManager.bdTheme('#999', '#666')};
103
- font-style: italic;
104
- }
105
-
106
- .nestedFields {
107
- margin-left: 16px;
108
- padding-left: 16px;
109
- border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
110
- }
111
-
112
- /* Status badge styles */
113
- .statusBadge {
114
- display: inline-flex;
115
- align-items: center;
116
- gap: 6px;
117
- padding: 4px 12px;
118
- border-radius: 20px;
119
- font-size: 13px;
120
- font-weight: 600;
121
- }
122
-
123
- .statusBadge.enabled {
124
- background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')};
125
- color: ${cssManager.bdTheme('#155724', '#66cc66')};
126
- }
127
-
128
- .statusBadge.disabled {
129
- background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')};
130
- color: ${cssManager.bdTheme('#721c24', '#cc6666')};
131
- }
132
-
133
- .statusBadge dees-icon {
134
- font-size: 14px;
135
- }
136
-
137
- /* Array/list display */
138
- .arrayItems {
139
- display: flex;
140
- flex-wrap: wrap;
141
- gap: 8px;
142
- }
143
-
144
- .arrayItem {
145
- display: inline-flex;
146
- align-items: center;
147
- background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
148
- color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
149
- padding: 4px 12px;
150
- border-radius: 16px;
151
- font-size: 13px;
152
- font-family: 'Consolas', 'Monaco', monospace;
153
- }
154
-
155
- .arrayCount {
156
- font-size: 12px;
157
- color: ${cssManager.bdTheme('#999', '#666')};
158
- margin-bottom: 8px;
159
- }
160
-
161
- /* Numeric value formatting */
162
- .numericValue {
163
- font-weight: 600;
164
- color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
165
- }
166
-
167
- .errorMessage {
168
- background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
169
- border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
170
- border-radius: 4px;
171
- padding: 16px;
172
- color: ${cssManager.bdTheme('#c00', '#ff6666')};
173
- margin: 16px 0;
174
- }
175
-
176
41
  .loadingMessage {
177
42
  text-align: center;
178
43
  padding: 40px;
179
- color: ${cssManager.bdTheme('#666', '#999')};
44
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
180
45
  }
181
46
 
182
- .infoNote {
183
- background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
184
- border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')};
47
+ .errorMessage {
48
+ background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
49
+ border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
185
50
  border-radius: 8px;
186
51
  padding: 16px;
187
- margin-bottom: 24px;
188
- color: ${cssManager.bdTheme('#004085', '#88ccff')};
189
- display: flex;
190
- align-items: center;
191
- gap: 12px;
192
- }
193
-
194
- .infoNote dees-icon {
195
- font-size: 20px;
196
- flex-shrink: 0;
52
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
53
+ margin: 16px 0;
197
54
  }
198
55
  `,
199
56
  ];
@@ -202,185 +59,266 @@ export class OpsViewConfig extends DeesElement {
202
59
  return html`
203
60
  <ops-sectionheading>Configuration</ops-sectionheading>
204
61
 
205
- ${this.configState.isLoading ? html`
206
- <div class="loadingMessage">
207
- <dees-spinner></dees-spinner>
208
- <p>Loading configuration...</p>
209
- </div>
210
- ` : this.configState.error ? html`
211
- <div class="errorMessage">
212
- Error loading configuration: ${this.configState.error}
213
- </div>
214
- ` : this.configState.config ? html`
215
- <div class="infoNote">
216
- <dees-icon icon="lucide:info"></dees-icon>
217
- <span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
218
- </div>
219
-
220
- ${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)}
221
- ${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)}
222
- ${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)}
223
- ${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)}
224
- ` : html`
225
- <div class="errorMessage">No configuration loaded</div>
226
- `}
62
+ ${this.configState.isLoading
63
+ ? html`
64
+ <div class="loadingMessage">
65
+ <dees-spinner></dees-spinner>
66
+ <p>Loading configuration...</p>
67
+ </div>
68
+ `
69
+ : this.configState.error
70
+ ? html`
71
+ <div class="errorMessage">
72
+ Error loading configuration: ${this.configState.error}
73
+ </div>
74
+ `
75
+ : this.configState.config
76
+ ? this.renderConfig()
77
+ : html`<div class="errorMessage">No configuration loaded</div>`}
227
78
  `;
228
79
  }
229
80
 
230
- private renderConfigSection(key: string, title: string, icon: string, config: any) {
231
- const isEnabled = config?.enabled ?? false;
81
+ private renderConfig(): TemplateResult {
82
+ const cfg = this.configState.config!;
232
83
 
233
84
  return html`
234
- <div class="configSection">
235
- <div class="sectionHeader">
236
- <h3 class="sectionTitle">
237
- <dees-icon icon="${icon}"></dees-icon>
238
- ${title}
239
- </h3>
240
- ${this.renderStatusBadge(isEnabled)}
241
- </div>
242
- <div class="sectionContent">
243
- ${config ? this.renderConfigFields(config) : html`
244
- <div class="fieldValue empty">Not configured</div>
245
- `}
246
- </div>
247
- </div>
85
+ <sz-config-overview
86
+ infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
87
+ @navigate=${(e: CustomEvent) => {
88
+ if (e.detail?.view) {
89
+ appRouter.navigateToView(e.detail.view);
90
+ }
91
+ }}
92
+ >
93
+ ${this.renderSystemSection(cfg.system)}
94
+ ${this.renderSmartProxySection(cfg.smartProxy)}
95
+ ${this.renderEmailSection(cfg.email)}
96
+ ${this.renderDnsSection(cfg.dns)}
97
+ ${this.renderTlsSection(cfg.tls)}
98
+ ${this.renderCacheSection(cfg.cache)}
99
+ ${this.renderRadiusSection(cfg.radius)}
100
+ ${this.renderRemoteIngressSection(cfg.remoteIngress)}
101
+ </sz-config-overview>
248
102
  `;
249
103
  }
250
104
 
251
- private renderStatusBadge(enabled: boolean): TemplateResult {
252
- return enabled
253
- ? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
254
- : html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
105
+ private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
106
+ const fields: IConfigField[] = [
107
+ { key: 'Base Directory', value: sys.baseDir },
108
+ { key: 'Data Directory', value: sys.dataDir },
109
+ { key: 'Public IP', value: sys.publicIp },
110
+ { key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' },
111
+ { key: 'Uptime', value: this.formatUptime(sys.uptime) },
112
+ { key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
113
+ { key: 'Storage Path', value: sys.storagePath },
114
+ ];
115
+
116
+ return html`
117
+ <sz-config-section
118
+ title="System"
119
+ subtitle="Base paths and infrastructure"
120
+ icon="lucide:server"
121
+ status="enabled"
122
+ .fields=${fields}
123
+ ></sz-config-section>
124
+ `;
255
125
  }
256
126
 
257
- private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
258
- if (!config || typeof config !== 'object') {
259
- return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
127
+ private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
128
+ const fields: IConfigField[] = [
129
+ { key: 'Route Count', value: proxy.routeCount },
130
+ ];
131
+
132
+ if (proxy.acme) {
133
+ fields.push(
134
+ { key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
135
+ { key: 'Account Email', value: proxy.acme.accountEmail || null },
136
+ { key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
137
+ { key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
138
+ { key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
139
+ );
260
140
  }
261
141
 
262
- return Object.entries(config).map(([key, value]) => {
263
- const fieldName = prefix ? `${prefix}.${key}` : key;
264
- const displayName = this.formatFieldName(key);
265
-
266
- // Handle boolean values with badges
267
- if (typeof value === 'boolean') {
268
- return html`
269
- <div class="configField">
270
- <label class="fieldLabel">${displayName}</label>
271
- ${this.renderStatusBadge(value)}
272
- </div>
273
- `;
274
- }
275
-
276
- // Handle arrays
277
- if (Array.isArray(value)) {
278
- return html`
279
- <div class="configField">
280
- <label class="fieldLabel">${displayName}</label>
281
- ${this.renderArrayValue(value, key)}
282
- </div>
283
- `;
284
- }
285
-
286
- // Handle nested objects
287
- if (typeof value === 'object' && value !== null) {
288
- return html`
289
- <div class="configField">
290
- <label class="fieldLabel">${displayName}</label>
291
- <div class="nestedFields">
292
- ${this.renderConfigFields(value, fieldName)}
293
- </div>
294
- </div>
295
- `;
296
- }
142
+ const actions: IConfigSectionAction[] = [
143
+ { label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
144
+ ];
297
145
 
298
- // Handle primitive values
299
- return html`
300
- <div class="configField">
301
- <label class="fieldLabel">${displayName}</label>
302
- <div class="fieldValue">${this.formatValue(value, key)}</div>
303
- </div>
304
- `;
305
- });
146
+ return html`
147
+ <sz-config-section
148
+ title="SmartProxy"
149
+ subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
150
+ icon="lucide:network"
151
+ .status=${proxy.enabled ? 'enabled' : 'disabled'}
152
+ .fields=${fields}
153
+ .actions=${actions}
154
+ ></sz-config-section>
155
+ `;
306
156
  }
307
157
 
308
- private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
309
- if (arr.length === 0) {
310
- return html`<div class="fieldValue empty">None configured</div>`;
158
+ private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
159
+ const fields: IConfigField[] = [
160
+ { key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
161
+ { key: 'Hostname', value: email.hostname },
162
+ { key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
163
+ { key: 'Email Routes', value: email.emailRouteCount },
164
+ { key: 'Received Emails Path', value: email.receivedEmailsPath },
165
+ ];
166
+
167
+ if (email.portMapping) {
168
+ const mappingStr = Object.entries(email.portMapping)
169
+ .map(([ext, int]) => `${ext} → ${int}`)
170
+ .join(', ');
171
+ fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
311
172
  }
312
173
 
313
- // Determine if we should show as pills/tags
314
- const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number');
315
-
316
- if (showAsPills) {
317
- const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
318
- return html`
319
- <div class="arrayCount">${arr.length} ${itemLabel}</div>
320
- <div class="arrayItems">
321
- ${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
322
- </div>
323
- `;
324
- }
174
+ const actions: IConfigSectionAction[] = [
175
+ { label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
176
+ ];
325
177
 
326
- // For complex arrays, show as JSON
327
178
  return html`
328
- <div class="fieldValue">
329
- ${arr.length} items configured
330
- </div>
179
+ <sz-config-section
180
+ title="Email Server"
181
+ subtitle="SMTP email handling with smartmta"
182
+ icon="lucide:mail"
183
+ .status=${email.enabled ? 'enabled' : 'disabled'}
184
+ .fields=${fields}
185
+ .actions=${actions}
186
+ ></sz-config-section>
331
187
  `;
332
188
  }
333
189
 
334
- private getArrayItemLabel(fieldKey: string, count: number): string {
335
- const labels: Record<string, [string, string]> = {
336
- ports: ['port', 'ports'],
337
- domains: ['domain', 'domains'],
338
- nameservers: ['nameserver', 'nameservers'],
339
- blockList: ['IP', 'IPs'],
340
- };
190
+ private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
191
+ const fields: IConfigField[] = [
192
+ { key: 'Port', value: dns.port },
193
+ { key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
194
+ { key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
195
+ { key: 'Record Count', value: dns.recordCount },
196
+ { key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
197
+ ];
341
198
 
342
- const label = labels[fieldKey] || ['item', 'items'];
343
- return count === 1 ? label[0] : label[1];
199
+ return html`
200
+ <sz-config-section
201
+ title="DNS Server"
202
+ subtitle="Authoritative DNS with smartdns"
203
+ icon="lucide:globe"
204
+ .status=${dns.enabled ? 'enabled' : 'disabled'}
205
+ .fields=${fields}
206
+ ></sz-config-section>
207
+ `;
344
208
  }
345
209
 
346
- private formatFieldName(key: string): string {
347
- // Convert camelCase to readable format
348
- return key
349
- .replace(/([A-Z])/g, ' $1')
350
- .replace(/^./, str => str.toUpperCase())
351
- .trim();
210
+ private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
211
+ const fields: IConfigField[] = [
212
+ { key: 'Contact Email', value: tls.contactEmail },
213
+ { key: 'Domain', value: tls.domain },
214
+ { key: 'Source', value: tls.source, type: 'badge' },
215
+ { key: 'Certificate Path', value: tls.certPath },
216
+ { key: 'Key Path', value: tls.keyPath },
217
+ ];
218
+
219
+ const status = tls.source === 'none' ? 'not-configured' : 'enabled';
220
+ const actions: IConfigSectionAction[] = [
221
+ { label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
222
+ ];
223
+
224
+ return html`
225
+ <sz-config-section
226
+ title="TLS / Certificates"
227
+ subtitle="Certificate management and ACME"
228
+ icon="lucide:shield-check"
229
+ .status=${status as any}
230
+ .fields=${fields}
231
+ .actions=${actions}
232
+ ></sz-config-section>
233
+ `;
352
234
  }
353
235
 
354
- private formatValue(value: any, fieldKey?: string): string | TemplateResult {
355
- if (value === null || value === undefined) {
356
- return html`<span class="empty">Not set</span>`;
236
+ private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
237
+ const fields: IConfigField[] = [
238
+ { key: 'Storage Path', value: cache.storagePath },
239
+ { key: 'DB Name', value: cache.dbName },
240
+ { key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
241
+ { key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
242
+ ];
243
+
244
+ if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
245
+ for (const [key, val] of Object.entries(cache.ttlConfig)) {
246
+ fields.push({ key: `TTL: ${key}`, value: `${val} days` });
247
+ }
357
248
  }
358
249
 
359
- if (typeof value === 'number') {
360
- // Format bytes
361
- if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) {
362
- return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
363
- }
364
- // Format time values
365
- if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
366
- return html`<span class="numericValue">${value} seconds</span>`;
367
- }
368
- // Format port numbers
369
- if (fieldKey?.toLowerCase().includes('port')) {
370
- return html`<span class="numericValue">${value}</span>`;
371
- }
372
- // Format counts with separators
373
- return html`<span class="numericValue">${value.toLocaleString()}</span>`;
250
+ return html`
251
+ <sz-config-section
252
+ title="Cache Database"
253
+ subtitle="Persistent caching with smartdata"
254
+ icon="lucide:database"
255
+ .status=${cache.enabled ? 'enabled' : 'disabled'}
256
+ .fields=${fields}
257
+ ></sz-config-section>
258
+ `;
259
+ }
260
+
261
+ private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
262
+ const fields: IConfigField[] = [
263
+ { key: 'Auth Port', value: radius.authPort },
264
+ { key: 'Accounting Port', value: radius.acctPort },
265
+ { key: 'Bind Address', value: radius.bindAddress },
266
+ { key: 'Client Count', value: radius.clientCount },
267
+ ];
268
+
269
+ if (radius.vlanDefaultVlan !== null) {
270
+ fields.push(
271
+ { key: 'Default VLAN', value: radius.vlanDefaultVlan },
272
+ { key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
273
+ { key: 'VLAN Mappings', value: radius.vlanMappingCount },
274
+ );
374
275
  }
375
276
 
376
- return String(value);
277
+ const status = radius.enabled ? 'enabled' : 'not-configured';
278
+
279
+ return html`
280
+ <sz-config-section
281
+ title="RADIUS Server"
282
+ subtitle="Network authentication and VLAN assignment"
283
+ icon="lucide:wifi"
284
+ .status=${status as any}
285
+ .fields=${fields}
286
+ ></sz-config-section>
287
+ `;
288
+ }
289
+
290
+ private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
291
+ const fields: IConfigField[] = [
292
+ { key: 'Tunnel Port', value: ri.tunnelPort },
293
+ { key: 'Hub Domain', value: ri.hubDomain },
294
+ { key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
295
+ ];
296
+
297
+ const actions: IConfigSectionAction[] = [
298
+ { label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
299
+ ];
300
+
301
+ return html`
302
+ <sz-config-section
303
+ title="Remote Ingress"
304
+ subtitle="Edge tunnel nodes"
305
+ icon="lucide:cloud"
306
+ .status=${ri.enabled ? 'enabled' : 'disabled'}
307
+ .fields=${fields}
308
+ .actions=${actions}
309
+ ></sz-config-section>
310
+ `;
377
311
  }
378
312
 
379
- private formatBytes(bytes: number): string {
380
- if (bytes === 0) return '0 B';
381
- const k = 1024;
382
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
383
- const i = Math.floor(Math.log(bytes) / Math.log(k));
384
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
313
+ private formatUptime(seconds: number): string {
314
+ const days = Math.floor(seconds / 86400);
315
+ const hours = Math.floor((seconds % 86400) / 3600);
316
+ const mins = Math.floor((seconds % 3600) / 60);
317
+
318
+ const parts: string[] = [];
319
+ if (days > 0) parts.push(`${days}d`);
320
+ if (hours > 0) parts.push(`${hours}h`);
321
+ parts.push(`${mins}m`);
322
+ return parts.join(' ');
385
323
  }
386
324
  }