@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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides AI-friendly diagnostic API for troubleshooting
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
import type { Request, Response } from 'express';
|
|
12
|
+
import type { ControlPanelPlugin, PluginContext } from '../core/types.js';
|
|
13
|
+
|
|
14
|
+
export interface DiagnosticsPluginConfig {
|
|
15
|
+
include?: {
|
|
16
|
+
logs?: {
|
|
17
|
+
startup?: number; // Last N lines from startup log
|
|
18
|
+
app?: number; // Last N lines from app log
|
|
19
|
+
};
|
|
20
|
+
health?: boolean;
|
|
21
|
+
config?: boolean;
|
|
22
|
+
system?: boolean;
|
|
23
|
+
};
|
|
24
|
+
logPaths?: {
|
|
25
|
+
startup?: string;
|
|
26
|
+
app?: string;
|
|
27
|
+
};
|
|
28
|
+
endpoint?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a diagnostics plugin for AI agents
|
|
33
|
+
*/
|
|
34
|
+
export function createDiagnosticsPlugin(config: DiagnosticsPluginConfig = {}): ControlPanelPlugin {
|
|
35
|
+
const {
|
|
36
|
+
include = { logs: { startup: 100, app: 200 }, health: true, config: true, system: true },
|
|
37
|
+
logPaths = { startup: './logs/startup.log', app: './logs/app.log' },
|
|
38
|
+
endpoint = '/diagnostics/full',
|
|
39
|
+
} = config;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: 'diagnostics',
|
|
43
|
+
order: 40,
|
|
44
|
+
|
|
45
|
+
routes: [
|
|
46
|
+
{
|
|
47
|
+
method: 'get',
|
|
48
|
+
path: endpoint,
|
|
49
|
+
handler: (_req: Request, res: Response) => {
|
|
50
|
+
try {
|
|
51
|
+
const report: Record<string, unknown> = {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
generated_for: 'AI Agent Diagnostics',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// System info
|
|
57
|
+
if (include.system) {
|
|
58
|
+
const memUsage = process.memoryUsage();
|
|
59
|
+
report.system = {
|
|
60
|
+
nodeVersion: process.version,
|
|
61
|
+
platform: process.platform,
|
|
62
|
+
arch: process.arch,
|
|
63
|
+
pid: process.pid,
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
uptime: process.uptime(),
|
|
66
|
+
memory: {
|
|
67
|
+
rss: formatBytes(memUsage.rss),
|
|
68
|
+
heapTotal: formatBytes(memUsage.heapTotal),
|
|
69
|
+
heapUsed: formatBytes(memUsage.heapUsed),
|
|
70
|
+
external: formatBytes(memUsage.external),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Environment check (not values, just presence)
|
|
76
|
+
if (include.config) {
|
|
77
|
+
const envCheck: Record<string, boolean> = {
|
|
78
|
+
NODE_ENV: !!process.env.NODE_ENV,
|
|
79
|
+
DATABASE_URI: !!process.env.DATABASE_URI,
|
|
80
|
+
PAYLOAD_SECRET: !!process.env.PAYLOAD_SECRET,
|
|
81
|
+
LOGFIRE_TOKEN: !!process.env.LOGFIRE_TOKEN,
|
|
82
|
+
};
|
|
83
|
+
report.envCheck = envCheck;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Logs
|
|
87
|
+
if (include.logs) {
|
|
88
|
+
const logs: Record<string, string[]> = {};
|
|
89
|
+
|
|
90
|
+
if (include.logs.startup && logPaths.startup) {
|
|
91
|
+
logs.startup = readLastNLines(logPaths.startup, include.logs.startup);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (include.logs.app && logPaths.app) {
|
|
95
|
+
logs.app = readLastNLines(logPaths.app, include.logs.app);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract errors
|
|
99
|
+
const allLogs = [...(logs.startup || []), ...(logs.app || [])];
|
|
100
|
+
logs.errors = allLogs.filter((line) => {
|
|
101
|
+
const lower = line.toLowerCase();
|
|
102
|
+
return lower.includes('error') || lower.includes('fatal') || lower.includes('exception');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
report.logs = logs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.json(report);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
res.status(500).json({
|
|
111
|
+
error: 'Failed to generate diagnostics',
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
method: 'get',
|
|
119
|
+
path: '/diagnostics/summary',
|
|
120
|
+
handler: (_req: Request, res: Response) => {
|
|
121
|
+
try {
|
|
122
|
+
const memUsage = process.memoryUsage();
|
|
123
|
+
|
|
124
|
+
res.json({
|
|
125
|
+
status: 'ok',
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
uptime: process.uptime(),
|
|
128
|
+
memory: {
|
|
129
|
+
heapUsed: formatBytes(memUsage.heapUsed),
|
|
130
|
+
heapTotal: formatBytes(memUsage.heapTotal),
|
|
131
|
+
},
|
|
132
|
+
env: process.env.NODE_ENV || 'development',
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
res.status(500).json({
|
|
136
|
+
status: 'error',
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
async onInit(context: PluginContext): Promise<void> {
|
|
145
|
+
context.logger.info('[DiagnosticsPlugin] Initialized');
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read last N lines from a file
|
|
152
|
+
*/
|
|
153
|
+
function readLastNLines(filePath: string, n: number): string[] {
|
|
154
|
+
const resolvedPath = resolve(filePath);
|
|
155
|
+
|
|
156
|
+
if (!existsSync(resolvedPath)) {
|
|
157
|
+
return [`File not found: ${filePath}`];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
162
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
163
|
+
return lines.slice(-n);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return [`Error reading file: ${error instanceof Error ? error.message : String(error)}`];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Format bytes to human readable
|
|
171
|
+
*/
|
|
172
|
+
function formatBytes(bytes: number): string {
|
|
173
|
+
if (bytes === 0) return '0 B';
|
|
174
|
+
const k = 1024;
|
|
175
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
176
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
177
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
178
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides health check monitoring capabilities
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ControlPanelPlugin, HealthCheck, PluginContext } from '../core/types.js';
|
|
10
|
+
|
|
11
|
+
export interface HealthPluginConfig {
|
|
12
|
+
checks: HealthCheck[];
|
|
13
|
+
aggregateEndpoint?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a health check plugin
|
|
18
|
+
*/
|
|
19
|
+
export function createHealthPlugin(config: HealthPluginConfig): ControlPanelPlugin {
|
|
20
|
+
return {
|
|
21
|
+
name: 'health',
|
|
22
|
+
order: 10,
|
|
23
|
+
|
|
24
|
+
async onInit(context: PluginContext): Promise<void> {
|
|
25
|
+
const { registerHealthCheck, logger } = context;
|
|
26
|
+
|
|
27
|
+
// Register all health checks
|
|
28
|
+
for (const check of config.checks) {
|
|
29
|
+
registerHealthCheck(check);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.info(`[HealthPlugin] Registered ${config.checks.length} health checks`);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in plugins for @qwickapps/server
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { createHealthPlugin } from './health-plugin.js';
|
|
8
|
+
export type { HealthPluginConfig } from './health-plugin.js';
|
|
9
|
+
|
|
10
|
+
export { createLogsPlugin } from './logs-plugin.js';
|
|
11
|
+
export type { LogsPluginConfig } from './logs-plugin.js';
|
|
12
|
+
|
|
13
|
+
export { createConfigPlugin } from './config-plugin.js';
|
|
14
|
+
export type { ConfigPluginConfig } from './config-plugin.js';
|
|
15
|
+
|
|
16
|
+
export { createDiagnosticsPlugin } from './diagnostics-plugin.js';
|
|
17
|
+
export type { DiagnosticsPluginConfig } from './diagnostics-plugin.js';
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides log viewing capabilities from various sources.
|
|
5
|
+
* If no sources are configured, automatically uses the logging subsystem's log paths.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import type { Request, Response } from 'express';
|
|
13
|
+
import type { ControlPanelPlugin, LogSource, PluginContext } from '../core/types.js';
|
|
14
|
+
import { getLoggingSubsystem } from '../core/logging.js';
|
|
15
|
+
|
|
16
|
+
export interface LogsPluginConfig {
|
|
17
|
+
/** Log sources to display. If empty, uses default sources from logging subsystem */
|
|
18
|
+
sources?: LogSource[];
|
|
19
|
+
retention?: {
|
|
20
|
+
maxLines?: number;
|
|
21
|
+
autoRefresh?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LogEntry {
|
|
26
|
+
id: number;
|
|
27
|
+
level: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
namespace: string;
|
|
30
|
+
message: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface LogStats {
|
|
35
|
+
totalLogs: number;
|
|
36
|
+
byLevel: {
|
|
37
|
+
debug: number;
|
|
38
|
+
info: number;
|
|
39
|
+
warn: number;
|
|
40
|
+
error: number;
|
|
41
|
+
};
|
|
42
|
+
fileSize: number;
|
|
43
|
+
fileSizeFormatted: string;
|
|
44
|
+
oldestLog: string | null;
|
|
45
|
+
newestLog: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get default log sources from the logging subsystem
|
|
50
|
+
*/
|
|
51
|
+
function getDefaultSources(): LogSource[] {
|
|
52
|
+
const loggingSubsystem = getLoggingSubsystem();
|
|
53
|
+
const logPaths = loggingSubsystem.getLogPaths();
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
{ name: 'app', type: 'file', path: logPaths.appLog },
|
|
57
|
+
{ name: 'error', type: 'file', path: logPaths.errorLog },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a logs plugin
|
|
63
|
+
*/
|
|
64
|
+
export function createLogsPlugin(config: LogsPluginConfig = {}): ControlPanelPlugin {
|
|
65
|
+
const maxLines = config.retention?.maxLines || 10000;
|
|
66
|
+
|
|
67
|
+
// Use provided sources or default to logging subsystem paths
|
|
68
|
+
const getSources = (): LogSource[] => {
|
|
69
|
+
if (config.sources && config.sources.length > 0) {
|
|
70
|
+
return config.sources;
|
|
71
|
+
}
|
|
72
|
+
return getDefaultSources();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: 'logs',
|
|
77
|
+
order: 20,
|
|
78
|
+
|
|
79
|
+
routes: [
|
|
80
|
+
{
|
|
81
|
+
method: 'get',
|
|
82
|
+
path: '/logs/sources',
|
|
83
|
+
handler: (_req: Request, res: Response) => {
|
|
84
|
+
const sources = getSources();
|
|
85
|
+
res.json({
|
|
86
|
+
sources: sources.map((s) => ({
|
|
87
|
+
name: s.name,
|
|
88
|
+
type: s.type,
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
method: 'get',
|
|
95
|
+
path: '/logs',
|
|
96
|
+
handler: (req: Request, res: Response) => {
|
|
97
|
+
try {
|
|
98
|
+
const sources = getSources();
|
|
99
|
+
const sourceName = (req.query.source as string) || sources[0]?.name;
|
|
100
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 100, maxLines);
|
|
101
|
+
const offset = parseInt(req.query.offset as string) || 0;
|
|
102
|
+
const level = req.query.level as string;
|
|
103
|
+
const search = req.query.search as string;
|
|
104
|
+
const order = (req.query.order as 'asc' | 'desc') || 'desc';
|
|
105
|
+
|
|
106
|
+
const source = sources.find((s) => s.name === sourceName);
|
|
107
|
+
if (!source) {
|
|
108
|
+
return res.status(404).json({ error: `Source "${sourceName}" not found` });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (source.type === 'file' && source.path) {
|
|
112
|
+
const logs = readLogsFromFile(source.path, { limit, offset, level, search, order });
|
|
113
|
+
return res.json(logs);
|
|
114
|
+
} else if (source.type === 'api' && source.url) {
|
|
115
|
+
// Proxy to remote API
|
|
116
|
+
return res.status(501).json({ error: 'API source not yet implemented' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return res.status(400).json({ error: 'Invalid source configuration' });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return res.status(500).json({
|
|
122
|
+
error: 'Failed to read logs',
|
|
123
|
+
message: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
method: 'get',
|
|
130
|
+
path: '/logs/stats',
|
|
131
|
+
handler: (req: Request, res: Response) => {
|
|
132
|
+
try {
|
|
133
|
+
const sources = getSources();
|
|
134
|
+
const sourceName = (req.query.source as string) || sources[0]?.name;
|
|
135
|
+
const source = sources.find((s) => s.name === sourceName);
|
|
136
|
+
|
|
137
|
+
if (!source) {
|
|
138
|
+
return res.status(404).json({ error: `Source "${sourceName}" not found` });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (source.type === 'file' && source.path) {
|
|
142
|
+
const stats = getLogStats(source.path);
|
|
143
|
+
return res.json(stats);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return res.status(400).json({ error: 'Stats only available for file sources' });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return res.status(500).json({
|
|
149
|
+
error: 'Failed to get log stats',
|
|
150
|
+
message: error instanceof Error ? error.message : String(error),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
|
|
157
|
+
async onInit(context: PluginContext): Promise<void> {
|
|
158
|
+
const sources = getSources();
|
|
159
|
+
context.logger.info(`Initialized with ${sources.length} sources`, {
|
|
160
|
+
sources: sources.map((s) => ({ name: s.name, type: s.type, path: s.path })),
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read logs from a file
|
|
168
|
+
*/
|
|
169
|
+
function readLogsFromFile(
|
|
170
|
+
filePath: string,
|
|
171
|
+
options: {
|
|
172
|
+
limit: number;
|
|
173
|
+
offset: number;
|
|
174
|
+
level?: string;
|
|
175
|
+
search?: string;
|
|
176
|
+
order: 'asc' | 'desc';
|
|
177
|
+
}
|
|
178
|
+
): { logs: LogEntry[]; total: number } {
|
|
179
|
+
const resolvedPath = resolve(filePath);
|
|
180
|
+
|
|
181
|
+
if (!existsSync(resolvedPath)) {
|
|
182
|
+
return { logs: [], total: 0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
186
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
187
|
+
|
|
188
|
+
let entries: LogEntry[] = [];
|
|
189
|
+
let id = 0;
|
|
190
|
+
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
// Try to parse as JSON log entry
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(line);
|
|
195
|
+
if (parsed.msg || parsed.message) {
|
|
196
|
+
entries.push({
|
|
197
|
+
id: id++,
|
|
198
|
+
level: parsed.level || 'info',
|
|
199
|
+
timestamp: parsed.timestamp || parsed.time || new Date().toISOString(),
|
|
200
|
+
namespace: parsed.ns || parsed.name || 'unknown',
|
|
201
|
+
message: parsed.msg || parsed.message,
|
|
202
|
+
...parsed,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Try to parse as simple log format: "LEVEL [namespace] message"
|
|
207
|
+
const match = line.match(/^(\d{2}:\d{2}:\d{2})\s+\[([^\]]+)\]\s+(.*)$/);
|
|
208
|
+
if (match) {
|
|
209
|
+
entries.push({
|
|
210
|
+
id: id++,
|
|
211
|
+
level: 'info',
|
|
212
|
+
timestamp: match[1],
|
|
213
|
+
namespace: match[2],
|
|
214
|
+
message: match[3],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Filter by level
|
|
221
|
+
if (options.level && options.level !== 'all') {
|
|
222
|
+
entries = entries.filter((e) => e.level === options.level);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Filter by search
|
|
226
|
+
if (options.search) {
|
|
227
|
+
const searchLower = options.search.toLowerCase();
|
|
228
|
+
entries = entries.filter(
|
|
229
|
+
(e) =>
|
|
230
|
+
e.message.toLowerCase().includes(searchLower) ||
|
|
231
|
+
e.namespace.toLowerCase().includes(searchLower)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const total = entries.length;
|
|
236
|
+
|
|
237
|
+
// Sort
|
|
238
|
+
if (options.order === 'desc') {
|
|
239
|
+
entries.reverse();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Paginate
|
|
243
|
+
entries = entries.slice(options.offset, options.offset + options.limit);
|
|
244
|
+
|
|
245
|
+
return { logs: entries, total };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get log statistics from a file
|
|
250
|
+
*/
|
|
251
|
+
function getLogStats(filePath: string): LogStats {
|
|
252
|
+
const resolvedPath = resolve(filePath);
|
|
253
|
+
|
|
254
|
+
if (!existsSync(resolvedPath)) {
|
|
255
|
+
return {
|
|
256
|
+
totalLogs: 0,
|
|
257
|
+
byLevel: { debug: 0, info: 0, warn: 0, error: 0 },
|
|
258
|
+
fileSize: 0,
|
|
259
|
+
fileSizeFormatted: '0 B',
|
|
260
|
+
oldestLog: null,
|
|
261
|
+
newestLog: null,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const stats = statSync(resolvedPath);
|
|
266
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
267
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
268
|
+
|
|
269
|
+
const byLevel = { debug: 0, info: 0, warn: 0, error: 0 };
|
|
270
|
+
let oldestLog: string | null = null;
|
|
271
|
+
let newestLog: string | null = null;
|
|
272
|
+
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(line);
|
|
276
|
+
const level = (parsed.level || 'info').toLowerCase();
|
|
277
|
+
if (level in byLevel) {
|
|
278
|
+
byLevel[level as keyof typeof byLevel]++;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const timestamp = parsed.timestamp || parsed.time;
|
|
282
|
+
if (timestamp) {
|
|
283
|
+
if (!oldestLog || timestamp < oldestLog) {
|
|
284
|
+
oldestLog = timestamp;
|
|
285
|
+
}
|
|
286
|
+
if (!newestLog || timestamp > newestLog) {
|
|
287
|
+
newestLog = timestamp;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
byLevel.info++; // Count non-JSON lines as info
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
totalLogs: lines.length,
|
|
297
|
+
byLevel,
|
|
298
|
+
fileSize: stats.size,
|
|
299
|
+
fileSizeFormatted: formatBytes(stats.size),
|
|
300
|
+
oldestLog,
|
|
301
|
+
newestLog,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Format bytes to human readable
|
|
307
|
+
*/
|
|
308
|
+
function formatBytes(bytes: number): string {
|
|
309
|
+
if (bytes === 0) return '0 B';
|
|
310
|
+
const k = 1024;
|
|
311
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
312
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
313
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
314
|
+
}
|
package/ui/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Control Panel</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/ui/src/App.tsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
2
|
+
import { QwickApp, ProductLogo, Text } from '@qwickapps/react-framework';
|
|
3
|
+
import { Link, Box } from '@mui/material';
|
|
4
|
+
import { defaultConfig } from './config/AppConfig';
|
|
5
|
+
import { DashboardPage } from './pages/DashboardPage';
|
|
6
|
+
import { HealthPage } from './pages/HealthPage';
|
|
7
|
+
import { LogsPage } from './pages/LogsPage';
|
|
8
|
+
import { ConfigPage } from './pages/ConfigPage';
|
|
9
|
+
import { DiagnosticsPage } from './pages/DiagnosticsPage';
|
|
10
|
+
import { NotFoundPage } from './pages/NotFoundPage';
|
|
11
|
+
|
|
12
|
+
// Package version - injected at build time or fallback
|
|
13
|
+
const SERVER_VERSION = '1.0.0';
|
|
14
|
+
|
|
15
|
+
// Default logo - consumers can customize
|
|
16
|
+
const logo = <ProductLogo name="Control Panel" />;
|
|
17
|
+
|
|
18
|
+
// Default footer content with QwickApps Server branding
|
|
19
|
+
const footerContent = (
|
|
20
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, py: 2 }}>
|
|
21
|
+
<Text variant="caption" customColor="var(--theme-text-secondary)">
|
|
22
|
+
Built with{' '}
|
|
23
|
+
<Link
|
|
24
|
+
href="https://qwickapps.com/products/qwickapps-server"
|
|
25
|
+
target="_blank"
|
|
26
|
+
rel="noopener noreferrer"
|
|
27
|
+
sx={{ color: 'primary.main' }}
|
|
28
|
+
>
|
|
29
|
+
QwickApps Server
|
|
30
|
+
</Link>
|
|
31
|
+
{' '}v{SERVER_VERSION}
|
|
32
|
+
</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export function App() {
|
|
37
|
+
return (
|
|
38
|
+
<BrowserRouter>
|
|
39
|
+
<QwickApp
|
|
40
|
+
config={defaultConfig}
|
|
41
|
+
logo={logo}
|
|
42
|
+
footerContent={footerContent}
|
|
43
|
+
enableScaffolding={true}
|
|
44
|
+
navigationItems={[
|
|
45
|
+
{ id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
|
|
46
|
+
{ id: 'health', label: 'Health', route: '/health', icon: 'favorite' },
|
|
47
|
+
{ id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
|
|
48
|
+
{ id: 'config', label: 'Config', route: '/config', icon: 'settings' },
|
|
49
|
+
{ id: 'diagnostics', label: 'Diagnostics', route: '/diagnostics', icon: 'bug_report' },
|
|
50
|
+
]}
|
|
51
|
+
showThemeSwitcher={true}
|
|
52
|
+
showPaletteSwitcher={true}
|
|
53
|
+
>
|
|
54
|
+
<Routes>
|
|
55
|
+
<Route path="/" element={<DashboardPage />} />
|
|
56
|
+
<Route path="/health" element={<HealthPage />} />
|
|
57
|
+
<Route path="/logs" element={<LogsPage />} />
|
|
58
|
+
<Route path="/config" element={<ConfigPage />} />
|
|
59
|
+
<Route path="/diagnostics" element={<DiagnosticsPage />} />
|
|
60
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
61
|
+
</Routes>
|
|
62
|
+
</QwickApp>
|
|
63
|
+
</BrowserRouter>
|
|
64
|
+
);
|
|
65
|
+
}
|