@qwickapps/server 1.0.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.
- package/LICENSE +45 -0
- package/README.md +321 -0
- package/dist/core/control-panel.d.ts +21 -0
- package/dist/core/control-panel.d.ts.map +1 -0
- package/dist/core/control-panel.js +416 -0
- package/dist/core/control-panel.js.map +1 -0
- package/dist/core/gateway.d.ts +133 -0
- package/dist/core/gateway.d.ts.map +1 -0
- package/dist/core/gateway.js +270 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health-manager.d.ts +52 -0
- package/dist/core/health-manager.d.ts.map +1 -0
- package/dist/core/health-manager.js +192 -0
- package/dist/core/health-manager.js.map +1 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logging.d.ts +83 -0
- package/dist/core/logging.d.ts.map +1 -0
- package/dist/core/logging.js +191 -0
- package/dist/core/logging.js.map +1 -0
- package/dist/core/types.d.ts +195 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/config-plugin.d.ts +15 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -0
- package/dist/plugins/config-plugin.js +96 -0
- package/dist/plugins/config-plugin.js.map +1 -0
- package/dist/plugins/diagnostics-plugin.d.ts +29 -0
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
- package/dist/plugins/diagnostics-plugin.js +142 -0
- package/dist/plugins/diagnostics-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.d.ts +17 -0
- package/dist/plugins/health-plugin.d.ts.map +1 -0
- package/dist/plugins/health-plugin.js +25 -0
- package/dist/plugins/health-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/logs-plugin.d.ts +22 -0
- package/dist/plugins/logs-plugin.d.ts.map +1 -0
- package/dist/plugins/logs-plugin.js +242 -0
- package/dist/plugins/logs-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
- package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
- package/dist-ui/assets/index-CiizQQnb.css +1 -0
- package/dist-ui/index.html +13 -0
- package/package.json +98 -0
- package/src/core/control-panel.ts +493 -0
- package/src/core/gateway.ts +421 -0
- package/src/core/health-manager.ts +227 -0
- package/src/core/index.ts +25 -0
- package/src/core/logging.ts +234 -0
- package/src/core/types.ts +218 -0
- package/src/index.ts +55 -0
- package/src/plugins/config-plugin.ts +117 -0
- package/src/plugins/diagnostics-plugin.ts +178 -0
- package/src/plugins/health-plugin.ts +35 -0
- package/src/plugins/index.ts +17 -0
- package/src/plugins/logs-plugin.ts +314 -0
- package/ui/index.html +12 -0
- package/ui/src/App.tsx +65 -0
- package/ui/src/api/controlPanelApi.ts +148 -0
- package/ui/src/config/AppConfig.ts +18 -0
- package/ui/src/index.css +29 -0
- package/ui/src/index.tsx +11 -0
- package/ui/src/pages/ConfigPage.tsx +199 -0
- package/ui/src/pages/DashboardPage.tsx +264 -0
- package/ui/src/pages/DiagnosticsPage.tsx +315 -0
- package/ui/src/pages/HealthPage.tsx +204 -0
- package/ui/src/pages/LogsPage.tsx +267 -0
- package/ui/src/pages/NotFoundPage.tsx +41 -0
- package/ui/tsconfig.json +19 -0
- package/ui/vite.config.ts +21 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Panel Core
|
|
3
|
+
*
|
|
4
|
+
* Creates and manages the control panel Express application
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import express, { type Application, type Router, type Request, type Response } from 'express';
|
|
10
|
+
import helmet from 'helmet';
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import compression from 'compression';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { HealthManager } from './health-manager.js';
|
|
17
|
+
import { initializeLogging, getControlPanelLogger, getLoggingSubsystem, type LoggingConfig } from './logging.js';
|
|
18
|
+
import type {
|
|
19
|
+
ControlPanelConfig,
|
|
20
|
+
ControlPanelPlugin,
|
|
21
|
+
ControlPanelInstance,
|
|
22
|
+
PluginContext,
|
|
23
|
+
DiagnosticsReport,
|
|
24
|
+
HealthCheck,
|
|
25
|
+
Logger,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
// Get the package root directory for serving UI assets
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = dirname(__filename);
|
|
31
|
+
// Handle both src/core and dist/core paths - go up to find package root
|
|
32
|
+
const packageRoot = __dirname.includes('/src/')
|
|
33
|
+
? join(__dirname, '..', '..')
|
|
34
|
+
: join(__dirname, '..', '..');
|
|
35
|
+
const uiDistPath = join(packageRoot, 'dist-ui');
|
|
36
|
+
|
|
37
|
+
export interface CreateControlPanelOptions {
|
|
38
|
+
config: ControlPanelConfig;
|
|
39
|
+
plugins?: ControlPanelPlugin[];
|
|
40
|
+
logger?: Logger;
|
|
41
|
+
/** Logging configuration */
|
|
42
|
+
logging?: LoggingConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a control panel instance
|
|
47
|
+
*/
|
|
48
|
+
export function createControlPanel(options: CreateControlPanelOptions): ControlPanelInstance {
|
|
49
|
+
const { config, plugins = [], logging: loggingConfig } = options;
|
|
50
|
+
|
|
51
|
+
// Initialize logging subsystem
|
|
52
|
+
const loggingSubsystem = initializeLogging({
|
|
53
|
+
namespace: config.productName,
|
|
54
|
+
...loggingConfig,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Use provided logger or get one from the logging subsystem
|
|
58
|
+
const logger = options.logger || loggingSubsystem.getRootLogger();
|
|
59
|
+
|
|
60
|
+
const app: Application = express();
|
|
61
|
+
const router: Router = express.Router();
|
|
62
|
+
const healthManager = new HealthManager(logger);
|
|
63
|
+
const registeredPlugins: ControlPanelPlugin[] = [];
|
|
64
|
+
let server: ReturnType<typeof app.listen> | null = null;
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
|
|
67
|
+
// Security middleware
|
|
68
|
+
app.use(
|
|
69
|
+
helmet({
|
|
70
|
+
contentSecurityPolicy: false, // Allow inline scripts for simple UI
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// CORS
|
|
75
|
+
app.use(
|
|
76
|
+
cors({
|
|
77
|
+
origin: config.cors?.origins || '*',
|
|
78
|
+
credentials: true,
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Body parsing (skip for specified paths to allow proxy middleware to work)
|
|
83
|
+
const skipBodyParserPaths = config.skipBodyParserPaths || [];
|
|
84
|
+
if (skipBodyParserPaths.length > 0) {
|
|
85
|
+
app.use((req, res, next) => {
|
|
86
|
+
// Skip body parsing for specified paths (useful for proxy middleware)
|
|
87
|
+
if (skipBodyParserPaths.some(path => req.path.startsWith(path))) {
|
|
88
|
+
return next();
|
|
89
|
+
}
|
|
90
|
+
express.json()(req, res, next);
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
app.use(express.json());
|
|
94
|
+
}
|
|
95
|
+
app.use(compression());
|
|
96
|
+
|
|
97
|
+
// Basic auth middleware if configured
|
|
98
|
+
if (config.auth?.enabled && config.auth.provider === 'basic' && config.auth.users) {
|
|
99
|
+
app.use((req, res, next) => {
|
|
100
|
+
const authHeader = req.headers.authorization;
|
|
101
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
102
|
+
res.set('WWW-Authenticate', 'Basic realm="Control Panel"');
|
|
103
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const base64Credentials = authHeader.substring(6);
|
|
107
|
+
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
108
|
+
const [username, password] = credentials.split(':');
|
|
109
|
+
|
|
110
|
+
const validUser = config.auth!.users!.find(
|
|
111
|
+
(u) => u.username === username && u.password === password
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!validUser) {
|
|
115
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
next();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Custom auth middleware
|
|
123
|
+
if (config.auth?.enabled && config.auth.provider === 'custom' && config.auth.customMiddleware) {
|
|
124
|
+
app.use(config.auth.customMiddleware);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Request logging
|
|
128
|
+
app.use((req, _res, next) => {
|
|
129
|
+
logger.debug(`${req.method} ${req.path}`);
|
|
130
|
+
next();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Mount router
|
|
134
|
+
app.use('/api', router);
|
|
135
|
+
|
|
136
|
+
// Built-in routes
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* GET /api/health - Aggregated health status
|
|
140
|
+
*/
|
|
141
|
+
router.get('/health', (_req: Request, res: Response) => {
|
|
142
|
+
const results = healthManager.getResults();
|
|
143
|
+
const status = healthManager.getAggregatedStatus();
|
|
144
|
+
const uptime = Date.now() - startTime;
|
|
145
|
+
|
|
146
|
+
res.status(status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503).json({
|
|
147
|
+
status,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
uptime,
|
|
150
|
+
checks: results,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* GET /api/info - Product information
|
|
156
|
+
*/
|
|
157
|
+
router.get('/info', (_req: Request, res: Response) => {
|
|
158
|
+
res.json({
|
|
159
|
+
product: config.productName,
|
|
160
|
+
version: config.version || 'unknown',
|
|
161
|
+
uptime: Date.now() - startTime,
|
|
162
|
+
links: config.links || [],
|
|
163
|
+
branding: config.branding || {},
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* GET /api/diagnostics - Full diagnostics for AI agents
|
|
169
|
+
*/
|
|
170
|
+
router.get('/diagnostics', (_req: Request, res: Response) => {
|
|
171
|
+
const report = getDiagnostics();
|
|
172
|
+
res.json(report);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Serve dashboard UI
|
|
177
|
+
*
|
|
178
|
+
* Priority:
|
|
179
|
+
* 1. If useRichUI is true and dist-ui exists, serve React SPA
|
|
180
|
+
* 2. Otherwise serve simple static HTML dashboard
|
|
181
|
+
*/
|
|
182
|
+
if (!config.disableDashboard) {
|
|
183
|
+
// Use customUiPath if provided, otherwise fall back to package's dist-ui
|
|
184
|
+
const effectiveUiPath = config.customUiPath || uiDistPath;
|
|
185
|
+
const hasRichUI = existsSync(effectiveUiPath);
|
|
186
|
+
const useRichUI = config.useRichUI !== false && hasRichUI;
|
|
187
|
+
|
|
188
|
+
logger.debug(`Dashboard config: __dirname=${__dirname}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
|
|
189
|
+
|
|
190
|
+
if (useRichUI) {
|
|
191
|
+
logger.info(`Serving rich React UI from ${effectiveUiPath}`);
|
|
192
|
+
// Serve static assets from dist-ui
|
|
193
|
+
app.use(express.static(effectiveUiPath));
|
|
194
|
+
|
|
195
|
+
// SPA fallback - serve index.html for all non-API routes
|
|
196
|
+
app.get('*', (req: Request, res: Response, next) => {
|
|
197
|
+
// Skip API routes (must check for /api/ with trailing slash to avoid matching /api-keys etc)
|
|
198
|
+
if (req.path.startsWith('/api/') || req.path === '/api') {
|
|
199
|
+
return next();
|
|
200
|
+
}
|
|
201
|
+
res.sendFile(join(effectiveUiPath, 'index.html'));
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
logger.info('Serving basic HTML dashboard');
|
|
205
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
206
|
+
const html = generateDashboardHtml(config, healthManager.getResults());
|
|
207
|
+
res.type('html').send(html);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Plugin context factory - creates context with plugin-specific logger
|
|
213
|
+
const createPluginContext = (pluginName: string): PluginContext => ({
|
|
214
|
+
config,
|
|
215
|
+
app,
|
|
216
|
+
router,
|
|
217
|
+
logger: getControlPanelLogger(pluginName),
|
|
218
|
+
registerHealthCheck: (check: HealthCheck) => healthManager.register(check),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Register plugin
|
|
222
|
+
const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
|
|
223
|
+
logger.info(`Registering plugin: ${plugin.name}`);
|
|
224
|
+
|
|
225
|
+
// Register routes
|
|
226
|
+
if (plugin.routes) {
|
|
227
|
+
for (const route of plugin.routes) {
|
|
228
|
+
switch (route.method) {
|
|
229
|
+
case 'get':
|
|
230
|
+
router.get(route.path, route.handler);
|
|
231
|
+
break;
|
|
232
|
+
case 'post':
|
|
233
|
+
router.post(route.path, route.handler);
|
|
234
|
+
break;
|
|
235
|
+
case 'put':
|
|
236
|
+
router.put(route.path, route.handler);
|
|
237
|
+
break;
|
|
238
|
+
case 'delete':
|
|
239
|
+
router.delete(route.path, route.handler);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
logger.debug(`Registered route: ${route.method.toUpperCase()} ${route.path}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Initialize plugin with plugin-specific logger
|
|
247
|
+
if (plugin.onInit) {
|
|
248
|
+
await plugin.onInit(createPluginContext(plugin.name));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
registeredPlugins.push(plugin);
|
|
252
|
+
logger.info(`Plugin registered: ${plugin.name}`);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Get diagnostics report
|
|
256
|
+
const getDiagnostics = (): DiagnosticsReport => {
|
|
257
|
+
const memUsage = process.memoryUsage();
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
product: config.productName,
|
|
262
|
+
version: config.version,
|
|
263
|
+
uptime: Date.now() - startTime,
|
|
264
|
+
health: healthManager.getResults(),
|
|
265
|
+
system: {
|
|
266
|
+
nodeVersion: process.version,
|
|
267
|
+
platform: process.platform,
|
|
268
|
+
arch: process.arch,
|
|
269
|
+
memory: {
|
|
270
|
+
total: memUsage.heapTotal,
|
|
271
|
+
used: memUsage.heapUsed,
|
|
272
|
+
free: memUsage.heapTotal - memUsage.heapUsed,
|
|
273
|
+
},
|
|
274
|
+
cpu: {
|
|
275
|
+
usage: 0, // Would need os.cpus() for real measurement
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Start server
|
|
282
|
+
const start = async (): Promise<void> => {
|
|
283
|
+
// Register initial plugins
|
|
284
|
+
for (const plugin of plugins) {
|
|
285
|
+
await registerPlugin(plugin);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
server = app.listen(config.port, () => {
|
|
290
|
+
logger.info(`Control panel started on port ${config.port}`);
|
|
291
|
+
logger.info(`Dashboard: http://localhost:${config.port}`);
|
|
292
|
+
logger.info(`Health: http://localhost:${config.port}/api/health`);
|
|
293
|
+
logger.info(`Diagnostics: http://localhost:${config.port}/api/diagnostics`);
|
|
294
|
+
resolve();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Stop server
|
|
300
|
+
const stop = async (): Promise<void> => {
|
|
301
|
+
// Shutdown plugins
|
|
302
|
+
for (const plugin of registeredPlugins) {
|
|
303
|
+
if (plugin.onShutdown) {
|
|
304
|
+
await plugin.onShutdown();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Shutdown health manager
|
|
309
|
+
healthManager.shutdown();
|
|
310
|
+
|
|
311
|
+
// Close server
|
|
312
|
+
if (server) {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
server!.close(() => {
|
|
315
|
+
logger.info('Control panel stopped');
|
|
316
|
+
resolve();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
app,
|
|
324
|
+
start,
|
|
325
|
+
stop,
|
|
326
|
+
registerPlugin,
|
|
327
|
+
getHealthStatus: () => healthManager.getResults(),
|
|
328
|
+
getDiagnostics,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate simple dashboard HTML
|
|
334
|
+
*/
|
|
335
|
+
function generateDashboardHtml(
|
|
336
|
+
config: ControlPanelConfig,
|
|
337
|
+
health: Record<string, { status: string; latency?: number; lastChecked: Date }>
|
|
338
|
+
): string {
|
|
339
|
+
const healthEntries = Object.entries(health);
|
|
340
|
+
const overallStatus = healthEntries.every((e) => e[1].status === 'healthy')
|
|
341
|
+
? 'healthy'
|
|
342
|
+
: healthEntries.some((e) => e[1].status === 'unhealthy')
|
|
343
|
+
? 'unhealthy'
|
|
344
|
+
: 'degraded';
|
|
345
|
+
|
|
346
|
+
const statusColor =
|
|
347
|
+
overallStatus === 'healthy' ? '#22c55e' : overallStatus === 'degraded' ? '#f59e0b' : '#ef4444';
|
|
348
|
+
|
|
349
|
+
const linksHtml = (config.links || [])
|
|
350
|
+
.map(
|
|
351
|
+
(link) =>
|
|
352
|
+
`<a href="${link.url}" ${link.external ? 'target="_blank"' : ''} class="link">${link.label}</a>`
|
|
353
|
+
)
|
|
354
|
+
.join('');
|
|
355
|
+
|
|
356
|
+
const healthHtml = healthEntries
|
|
357
|
+
.map(
|
|
358
|
+
([name, result]) => `
|
|
359
|
+
<div class="health-item">
|
|
360
|
+
<span class="status-dot" style="background-color: ${
|
|
361
|
+
result.status === 'healthy' ? '#22c55e' : result.status === 'degraded' ? '#f59e0b' : '#ef4444'
|
|
362
|
+
}"></span>
|
|
363
|
+
<span class="name">${name}</span>
|
|
364
|
+
<span class="latency">${result.latency ? `${result.latency}ms` : '-'}</span>
|
|
365
|
+
</div>
|
|
366
|
+
`
|
|
367
|
+
)
|
|
368
|
+
.join('');
|
|
369
|
+
|
|
370
|
+
return `<!DOCTYPE html>
|
|
371
|
+
<html lang="en">
|
|
372
|
+
<head>
|
|
373
|
+
<meta charset="UTF-8">
|
|
374
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
375
|
+
<title>${config.productName} - Control Panel</title>
|
|
376
|
+
<style>
|
|
377
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
378
|
+
body {
|
|
379
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
380
|
+
background: #0f172a;
|
|
381
|
+
color: #e2e8f0;
|
|
382
|
+
min-height: 100vh;
|
|
383
|
+
padding: 2rem;
|
|
384
|
+
}
|
|
385
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
386
|
+
header {
|
|
387
|
+
display: flex;
|
|
388
|
+
justify-content: space-between;
|
|
389
|
+
align-items: center;
|
|
390
|
+
margin-bottom: 2rem;
|
|
391
|
+
padding-bottom: 1rem;
|
|
392
|
+
border-bottom: 1px solid #334155;
|
|
393
|
+
}
|
|
394
|
+
h1 { font-size: 1.5rem; color: ${config.branding?.primaryColor || '#6366f1'}; }
|
|
395
|
+
.status-badge {
|
|
396
|
+
display: inline-flex;
|
|
397
|
+
align-items: center;
|
|
398
|
+
gap: 0.5rem;
|
|
399
|
+
padding: 0.5rem 1rem;
|
|
400
|
+
border-radius: 9999px;
|
|
401
|
+
background: ${statusColor}20;
|
|
402
|
+
color: ${statusColor};
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
}
|
|
405
|
+
.status-dot {
|
|
406
|
+
width: 8px;
|
|
407
|
+
height: 8px;
|
|
408
|
+
border-radius: 50%;
|
|
409
|
+
background: currentColor;
|
|
410
|
+
}
|
|
411
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
|
|
412
|
+
.card {
|
|
413
|
+
background: #1e293b;
|
|
414
|
+
border-radius: 0.75rem;
|
|
415
|
+
padding: 1.5rem;
|
|
416
|
+
border: 1px solid #334155;
|
|
417
|
+
}
|
|
418
|
+
.card h2 {
|
|
419
|
+
font-size: 0.875rem;
|
|
420
|
+
text-transform: uppercase;
|
|
421
|
+
letter-spacing: 0.05em;
|
|
422
|
+
color: #94a3b8;
|
|
423
|
+
margin-bottom: 1rem;
|
|
424
|
+
}
|
|
425
|
+
.health-item {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
gap: 0.75rem;
|
|
429
|
+
padding: 0.75rem 0;
|
|
430
|
+
border-bottom: 1px solid #334155;
|
|
431
|
+
}
|
|
432
|
+
.health-item:last-child { border-bottom: none; }
|
|
433
|
+
.health-item .name { flex: 1; }
|
|
434
|
+
.health-item .latency { color: #64748b; font-size: 0.875rem; }
|
|
435
|
+
.links { display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
|
436
|
+
.link {
|
|
437
|
+
display: inline-block;
|
|
438
|
+
padding: 0.75rem 1.5rem;
|
|
439
|
+
background: #334155;
|
|
440
|
+
color: #e2e8f0;
|
|
441
|
+
text-decoration: none;
|
|
442
|
+
border-radius: 0.5rem;
|
|
443
|
+
transition: background 0.2s;
|
|
444
|
+
}
|
|
445
|
+
.link:hover { background: #475569; }
|
|
446
|
+
.api-links { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #334155; }
|
|
447
|
+
.api-links a { color: #6366f1; margin-right: 1.5rem; text-decoration: none; }
|
|
448
|
+
.api-links a:hover { text-decoration: underline; }
|
|
449
|
+
</style>
|
|
450
|
+
</head>
|
|
451
|
+
<body>
|
|
452
|
+
<div class="container">
|
|
453
|
+
<header>
|
|
454
|
+
<h1>${config.productName} Control Panel</h1>
|
|
455
|
+
<div class="status-badge">
|
|
456
|
+
<span class="status-dot"></span>
|
|
457
|
+
${overallStatus.charAt(0).toUpperCase() + overallStatus.slice(1)}
|
|
458
|
+
</div>
|
|
459
|
+
</header>
|
|
460
|
+
|
|
461
|
+
<div class="cards">
|
|
462
|
+
<div class="card">
|
|
463
|
+
<h2>Health Checks</h2>
|
|
464
|
+
${healthHtml || '<p style="color: #64748b;">No health checks configured</p>'}
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
${
|
|
468
|
+
(config.links || []).length > 0
|
|
469
|
+
? `
|
|
470
|
+
<div class="card">
|
|
471
|
+
<h2>Quick Links</h2>
|
|
472
|
+
<div class="links">${linksHtml}</div>
|
|
473
|
+
</div>
|
|
474
|
+
`
|
|
475
|
+
: ''
|
|
476
|
+
}
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="api-links">
|
|
480
|
+
<strong>API:</strong>
|
|
481
|
+
<a href="/api/health">Health</a>
|
|
482
|
+
<a href="/api/info">Info</a>
|
|
483
|
+
<a href="/api/diagnostics">Diagnostics</a>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<script>
|
|
488
|
+
// Auto-refresh health status every 10 seconds
|
|
489
|
+
setTimeout(() => location.reload(), 10000);
|
|
490
|
+
</script>
|
|
491
|
+
</body>
|
|
492
|
+
</html>`;
|
|
493
|
+
}
|