BREAKING CHANGE(opsserver): Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
This commit is contained in:
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
@@ -0,0 +1,31 @@
|
||||
[ 75ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
||||
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
||||
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
||||
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
||||
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
||||
[ 763ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
[ 22315ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 22315ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 22316ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 22316ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 22321ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 22322ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 22322ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 22322ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 65371ms] [ERROR] method: >>createApiToken<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 65371ms] [ERROR] Failed to create token: zs @ http://localhost:3000/bundle.js:38142
|
||||
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
@@ -0,0 +1,25 @@
|
||||
[ 642ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
[ 114916ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 179731ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 179732ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 179737ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 179738ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 179738ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 179738ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
1
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
1
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
@@ -0,0 +1 @@
|
||||
[ 603ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
@@ -0,0 +1,24 @@
|
||||
[ 308ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 309ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 309ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 310ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 349ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 350ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 350ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 351ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 500ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/apitokens:0
|
||||
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
@@ -0,0 +1,30 @@
|
||||
[ 427ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
[ 44124ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
||||
[ 59106ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 59106ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 59107ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 59107ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 59116ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 59116ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
[ 89192ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||
[ 89192ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-23 - 9.0.0 - BREAKING CHANGE(opsserver)
|
||||
Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
|
||||
|
||||
- Introduce IConfigData interface with typed sections: system, smartProxy, email, dns, tls, cache, radius, remoteIngress.
|
||||
- Replace ConfigHandler.getConfiguration implementation to assemble and return IConfigData (changes API response shape for getConfiguration).
|
||||
- Refactor frontend: update appstate types and ops-view-config to render the new config sections, use @serve.zone/catalog IConfigField/IConfigSectionAction, add uptime formatting and remote ingress UI.
|
||||
- Fix ops-view-apitokens form handling to correctly read dees-input-tags values.
|
||||
- Update tests to expect new configuration fields.
|
||||
- Bump dependency @serve.zone/catalog to ^2.5.0.
|
||||
|
||||
## 2026-02-23 - 8.1.0 - feat(route-management)
|
||||
add programmatic route management API with API tokens and admin UI
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.0.30",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@serve.zone/catalog": "^2.3.0",
|
||||
"@serve.zone/catalog": "^2.5.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@serve.zone/remoteingress": "^4.0.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -93,8 +93,8 @@ importers:
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@serve.zone/catalog':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0(@tiptap/pm@2.27.2)
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -1333,8 +1333,8 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@serve.zone/catalog@2.3.0':
|
||||
resolution: {integrity: sha512-KCIQZXBO5A93VIsRkI/UzApNImEHzuA7P3Wx33+mDVUZ8/I5hafuCLgPzNu1q/TgQUte+q6I6e5Erezc9Hn74Q==}
|
||||
'@serve.zone/catalog@2.5.0':
|
||||
resolution: {integrity: sha512-bRwk7pbDxUB471wUAS7p22MTOOBCHlMWijsE43K9tDAPcxlRarhtf2Dgx0Y25s/dFXqj2JHwe6jjE84S80jFzg==}
|
||||
|
||||
'@serve.zone/interfaces@5.3.0':
|
||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||
@@ -6785,7 +6785,7 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@serve.zone/catalog@2.3.0(@tiptap/pm@2.27.2)':
|
||||
'@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
|
||||
@@ -55,10 +55,14 @@ tap.test('should respond to configuration request', async () => {
|
||||
const response = await configRequest.fire({});
|
||||
|
||||
expect(response).toHaveProperty('config');
|
||||
expect(response.config).toHaveProperty('system');
|
||||
expect(response.config).toHaveProperty('smartProxy');
|
||||
expect(response.config).toHaveProperty('email');
|
||||
expect(response.config).toHaveProperty('dns');
|
||||
expect(response.config).toHaveProperty('proxy');
|
||||
expect(response.config).toHaveProperty('security');
|
||||
expect(response.config).toHaveProperty('tls');
|
||||
expect(response.config).toHaveProperty('cache');
|
||||
expect(response.config).toHaveProperty('radius');
|
||||
expect(response.config).toHaveProperty('remoteIngress');
|
||||
});
|
||||
|
||||
tap.test('should handle log retrieval request', async () => {
|
||||
|
||||
@@ -106,10 +106,14 @@ tap.test('should allow read-only config access', async () => {
|
||||
const response = await configRequest.fire({});
|
||||
|
||||
expect(response).toHaveProperty('config');
|
||||
expect(response.config).toHaveProperty('system');
|
||||
expect(response.config).toHaveProperty('smartProxy');
|
||||
expect(response.config).toHaveProperty('email');
|
||||
expect(response.config).toHaveProperty('dns');
|
||||
expect(response.config).toHaveProperty('proxy');
|
||||
expect(response.config).toHaveProperty('security');
|
||||
expect(response.config).toHaveProperty('tls');
|
||||
expect(response.config).toHaveProperty('cache');
|
||||
expect(response.config).toHaveProperty('radius');
|
||||
expect(response.config).toHaveProperty('remoteIngress');
|
||||
console.log('Configuration read successfully');
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '8.1.0',
|
||||
version: '9.0.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
@@ -17,7 +18,7 @@ export class ConfigHandler {
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
||||
'getConfiguration',
|
||||
async (dataArg, toolsArg) => {
|
||||
const config = await this.getConfiguration(dataArg.section);
|
||||
const config = await this.getConfiguration();
|
||||
return {
|
||||
config,
|
||||
section: dataArg.section,
|
||||
@@ -27,82 +28,163 @@ export class ConfigHandler {
|
||||
);
|
||||
}
|
||||
|
||||
private async getConfiguration(section?: string): Promise<{
|
||||
email: {
|
||||
enabled: boolean;
|
||||
ports: number[];
|
||||
maxMessageSize: number;
|
||||
rateLimits: {
|
||||
perMinute: number;
|
||||
perHour: number;
|
||||
perDay: number;
|
||||
};
|
||||
domains?: string[];
|
||||
};
|
||||
dns: {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
nameservers: string[];
|
||||
caching: boolean;
|
||||
ttl: number;
|
||||
};
|
||||
proxy: {
|
||||
enabled: boolean;
|
||||
httpPort: number;
|
||||
httpsPort: number;
|
||||
maxConnections: number;
|
||||
};
|
||||
security: {
|
||||
blockList: string[];
|
||||
rateLimit: boolean;
|
||||
spamDetection: boolean;
|
||||
tlsRequired: boolean;
|
||||
};
|
||||
}> {
|
||||
private async getConfiguration(): Promise<interfaces.requests.IConfigData> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const opts = dcRouter.options;
|
||||
const resolvedPaths = dcRouter.resolvedPaths;
|
||||
|
||||
// Get email domains if email server is configured
|
||||
// --- System ---
|
||||
const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.storage?.readFunction
|
||||
? 'custom'
|
||||
: opts.storage?.fsPath
|
||||
? 'filesystem'
|
||||
: 'memory';
|
||||
|
||||
const system: interfaces.requests.IConfigData['system'] = {
|
||||
baseDir: resolvedPaths.dcrouterHomeDir,
|
||||
dataDir: resolvedPaths.dataDir,
|
||||
publicIp: opts.publicIp || null,
|
||||
proxyIps: opts.proxyIps || [],
|
||||
uptime: Math.floor(process.uptime()),
|
||||
storageBackend,
|
||||
storagePath: opts.storage?.fsPath || null,
|
||||
};
|
||||
|
||||
// --- SmartProxy ---
|
||||
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
|
||||
if (opts.smartProxyConfig?.acme) {
|
||||
const acme = opts.smartProxyConfig.acme;
|
||||
acmeInfo = {
|
||||
enabled: acme.enabled !== false,
|
||||
accountEmail: acme.accountEmail || '',
|
||||
useProduction: acme.useProduction !== false,
|
||||
autoRenew: acme.autoRenew !== false,
|
||||
renewThresholdDays: acme.renewThresholdDays || 30,
|
||||
};
|
||||
}
|
||||
|
||||
let routeCount = 0;
|
||||
if (dcRouter.routeConfigManager) {
|
||||
try {
|
||||
const merged = await dcRouter.routeConfigManager.getMergedRoutes();
|
||||
routeCount = merged.routes.length;
|
||||
} catch {
|
||||
routeCount = opts.smartProxyConfig?.routes?.length || 0;
|
||||
}
|
||||
} else if (opts.smartProxyConfig?.routes) {
|
||||
routeCount = opts.smartProxyConfig.routes.length;
|
||||
}
|
||||
|
||||
const smartProxy: interfaces.requests.IConfigData['smartProxy'] = {
|
||||
enabled: !!dcRouter.smartProxy,
|
||||
routeCount,
|
||||
acme: acmeInfo,
|
||||
};
|
||||
|
||||
// --- Email ---
|
||||
let emailDomains: string[] = [];
|
||||
if (dcRouter.emailServer && dcRouter.emailServer.domainRegistry) {
|
||||
emailDomains = dcRouter.emailServer.domainRegistry.getAllDomains();
|
||||
} else if (dcRouter.options.emailConfig?.domains) {
|
||||
// Fallback: get domains from email config options
|
||||
emailDomains = dcRouter.options.emailConfig.domains.map(d =>
|
||||
if (dcRouter.emailServer && (dcRouter.emailServer as any).domainRegistry) {
|
||||
emailDomains = (dcRouter.emailServer as any).domainRegistry.getAllDomains();
|
||||
} else if (opts.emailConfig?.domains) {
|
||||
emailDomains = opts.emailConfig.domains.map((d: any) =>
|
||||
typeof d === 'string' ? d : d.domain
|
||||
);
|
||||
}
|
||||
|
||||
let portMapping: Record<string, number> | null = null;
|
||||
if (opts.emailPortConfig?.portMapping) {
|
||||
portMapping = {};
|
||||
for (const [ext, int] of Object.entries(opts.emailPortConfig.portMapping)) {
|
||||
portMapping[String(ext)] = int as number;
|
||||
}
|
||||
}
|
||||
|
||||
const email: interfaces.requests.IConfigData['email'] = {
|
||||
enabled: !!dcRouter.emailServer,
|
||||
ports: opts.emailConfig?.ports || [],
|
||||
portMapping,
|
||||
hostname: opts.emailConfig?.hostname || null,
|
||||
domains: emailDomains,
|
||||
emailRouteCount: opts.emailConfig?.routes?.length || 0,
|
||||
receivedEmailsPath: opts.emailPortConfig?.receivedEmailsPath || null,
|
||||
};
|
||||
|
||||
// --- DNS ---
|
||||
const dnsRecords = (opts.dnsRecords || []).map(r => ({
|
||||
name: r.name,
|
||||
type: r.type,
|
||||
value: r.value,
|
||||
ttl: r.ttl,
|
||||
}));
|
||||
|
||||
const dns: interfaces.requests.IConfigData['dns'] = {
|
||||
enabled: !!dcRouter.dnsServer,
|
||||
port: 53,
|
||||
nsDomains: opts.dnsNsDomains || [],
|
||||
scopes: opts.dnsScopes || [],
|
||||
recordCount: dnsRecords.length,
|
||||
records: dnsRecords,
|
||||
dnsChallenge: !!opts.dnsChallenge?.cloudflareApiKey,
|
||||
};
|
||||
|
||||
// --- TLS ---
|
||||
let tlsSource: 'acme' | 'static' | 'none' = 'none';
|
||||
if (opts.tls?.certPath && opts.tls?.keyPath) {
|
||||
tlsSource = 'static';
|
||||
} else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
|
||||
tlsSource = 'acme';
|
||||
}
|
||||
|
||||
const tls: interfaces.requests.IConfigData['tls'] = {
|
||||
contactEmail: opts.tls?.contactEmail || opts.smartProxyConfig?.acme?.accountEmail || null,
|
||||
domain: opts.tls?.domain || null,
|
||||
source: tlsSource,
|
||||
certPath: opts.tls?.certPath || null,
|
||||
keyPath: opts.tls?.keyPath || null,
|
||||
};
|
||||
|
||||
// --- Cache ---
|
||||
const cacheConfig = opts.cacheConfig;
|
||||
const cache: interfaces.requests.IConfigData['cache'] = {
|
||||
enabled: cacheConfig?.enabled !== false,
|
||||
storagePath: cacheConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
|
||||
dbName: cacheConfig?.dbName || 'dcrouter',
|
||||
defaultTTLDays: cacheConfig?.defaultTTLDays || 30,
|
||||
cleanupIntervalHours: cacheConfig?.cleanupIntervalHours || 1,
|
||||
ttlConfig: cacheConfig?.ttlConfig ? { ...cacheConfig.ttlConfig } as Record<string, number> : {},
|
||||
};
|
||||
|
||||
// --- RADIUS ---
|
||||
const radiusCfg = opts.radiusConfig;
|
||||
const radius: interfaces.requests.IConfigData['radius'] = {
|
||||
enabled: !!dcRouter.radiusServer,
|
||||
authPort: radiusCfg?.authPort || null,
|
||||
acctPort: radiusCfg?.acctPort || null,
|
||||
bindAddress: radiusCfg?.bindAddress || null,
|
||||
clientCount: radiusCfg?.clients?.length || 0,
|
||||
vlanDefaultVlan: radiusCfg?.vlanAssignment?.defaultVlan ?? null,
|
||||
vlanAllowUnknownMacs: radiusCfg?.vlanAssignment?.allowUnknownMacs ?? null,
|
||||
vlanMappingCount: radiusCfg?.vlanAssignment?.mappings?.length || 0,
|
||||
};
|
||||
|
||||
// --- Remote Ingress ---
|
||||
const riCfg = opts.remoteIngressConfig;
|
||||
const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
|
||||
enabled: !!dcRouter.remoteIngressManager,
|
||||
tunnelPort: riCfg?.tunnelPort || null,
|
||||
hubDomain: riCfg?.hubDomain || null,
|
||||
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath),
|
||||
};
|
||||
|
||||
return {
|
||||
email: {
|
||||
enabled: !!dcRouter.emailServer,
|
||||
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [],
|
||||
maxMessageSize: 10 * 1024 * 1024, // 10MB default
|
||||
rateLimits: {
|
||||
perMinute: 10,
|
||||
perHour: 100,
|
||||
perDay: 1000,
|
||||
},
|
||||
domains: emailDomains,
|
||||
},
|
||||
dns: {
|
||||
enabled: !!dcRouter.dnsServer,
|
||||
port: 53,
|
||||
nameservers: dcRouter.options.dnsNsDomains || [],
|
||||
caching: true,
|
||||
ttl: 300,
|
||||
},
|
||||
proxy: {
|
||||
enabled: !!dcRouter.smartProxy,
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
maxConnections: 1000,
|
||||
},
|
||||
security: {
|
||||
blockList: [],
|
||||
rateLimit: true,
|
||||
spamDetection: true,
|
||||
tlsRequired: false,
|
||||
},
|
||||
system,
|
||||
smartProxy,
|
||||
email,
|
||||
dns,
|
||||
tls,
|
||||
cache,
|
||||
radius,
|
||||
remoteIngress,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,78 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
|
||||
export interface IConfigData {
|
||||
system: {
|
||||
baseDir: string;
|
||||
dataDir: string;
|
||||
publicIp: string | null;
|
||||
proxyIps: string[];
|
||||
uptime: number;
|
||||
storageBackend: 'filesystem' | 'custom' | 'memory';
|
||||
storagePath: string | null;
|
||||
};
|
||||
smartProxy: {
|
||||
enabled: boolean;
|
||||
routeCount: number;
|
||||
acme: {
|
||||
enabled: boolean;
|
||||
accountEmail: string;
|
||||
useProduction: boolean;
|
||||
autoRenew: boolean;
|
||||
renewThresholdDays: number;
|
||||
} | null;
|
||||
};
|
||||
email: {
|
||||
enabled: boolean;
|
||||
ports: number[];
|
||||
portMapping: Record<string, number> | null;
|
||||
hostname: string | null;
|
||||
domains: string[];
|
||||
emailRouteCount: number;
|
||||
receivedEmailsPath: string | null;
|
||||
};
|
||||
dns: {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
nsDomains: string[];
|
||||
scopes: string[];
|
||||
recordCount: number;
|
||||
records: Array<{ name: string; type: string; value: string; ttl?: number }>;
|
||||
dnsChallenge: boolean;
|
||||
};
|
||||
tls: {
|
||||
contactEmail: string | null;
|
||||
domain: string | null;
|
||||
source: 'acme' | 'static' | 'none';
|
||||
certPath: string | null;
|
||||
keyPath: string | null;
|
||||
};
|
||||
cache: {
|
||||
enabled: boolean;
|
||||
storagePath: string | null;
|
||||
dbName: string | null;
|
||||
defaultTTLDays: number;
|
||||
cleanupIntervalHours: number;
|
||||
ttlConfig: Record<string, number>;
|
||||
};
|
||||
radius: {
|
||||
enabled: boolean;
|
||||
authPort: number | null;
|
||||
acctPort: number | null;
|
||||
bindAddress: string | null;
|
||||
clientCount: number;
|
||||
vlanDefaultVlan: number | null;
|
||||
vlanAllowUnknownMacs: boolean | null;
|
||||
vlanMappingCount: number;
|
||||
};
|
||||
remoteIngress: {
|
||||
enabled: boolean;
|
||||
tunnelPort: number | null;
|
||||
hubDomain: string | null;
|
||||
tlsConfigured: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Get Configuration (read-only)
|
||||
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
@@ -12,7 +84,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
|
||||
section?: string;
|
||||
};
|
||||
response: {
|
||||
config: any;
|
||||
config: IConfigData;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '8.1.0',
|
||||
version: '9.0.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface IStatsState {
|
||||
}
|
||||
|
||||
export interface IConfigState {
|
||||
config: any | null;
|
||||
config: interfaces.requests.IConfigData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ export class OpsDashboard extends DeesElement {
|
||||
name: 'Overview',
|
||||
element: OpsViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
element: OpsViewConfig,
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
element: OpsViewNetwork,
|
||||
@@ -65,10 +69,6 @@ export class OpsDashboard extends DeesElement {
|
||||
name: 'ApiTokens',
|
||||
element: OpsViewApiTokens,
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
element: OpsViewConfig,
|
||||
},
|
||||
{
|
||||
name: 'Security',
|
||||
element: OpsViewSecurity,
|
||||
|
||||
@@ -225,13 +225,17 @@ export class OpsViewApiTokens extends DeesElement {
|
||||
name: 'Create',
|
||||
iconName: 'lucide:key',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
const contentEl = modalArg.shadowRoot?.querySelector('.content');
|
||||
const form = contentEl?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
if (!formData.name) return;
|
||||
|
||||
// dees-input-tags returns string[] directly
|
||||
const scopes = (formData.scopes || [])
|
||||
// dees-input-tags is not in dees-form's FORM_INPUT_TYPES, so collectFormData() won't
|
||||
// include it. Query the tags input directly and call getValue().
|
||||
const tagsInput = form.querySelector('dees-input-tags') as any;
|
||||
const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
|
||||
const scopes = rawScopes
|
||||
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
|
||||
|
||||
const expiresInDays = formData.expiresInDays
|
||||
|
||||
@@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user