@cinnabun/dev-dashboard 0.0.1
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/dist/dev-dashboard.module.d.ts +6 -0
- package/dist/dev-dashboard.module.js +158 -0
- package/dist/dev-dashboard.plugin.d.ts +5 -0
- package/dist/dev-dashboard.plugin.js +16 -0
- package/dist/guards/dashboard-auth.guard.d.ts +2 -0
- package/dist/guards/dashboard-auth.guard.js +23 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/interfaces/dashboard-options.d.ts +11 -0
- package/dist/interfaces/dashboard-options.js +1 -0
- package/dist/services/introspection.service.d.ts +75 -0
- package/dist/services/introspection.service.js +157 -0
- package/dist/services/metrics.service.d.ts +24 -0
- package/dist/services/metrics.service.js +35 -0
- package/dist/ui/dashboard-ui.d.ts +1 -0
- package/dist/ui/dashboard-ui.js +306 -0
- package/package.json +36 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Module } from "@cinnabun/core";
|
|
2
|
+
import type { DevDashboardOptions } from "./interfaces/dashboard-options.js";
|
|
3
|
+
export declare class DevDashboardModule {
|
|
4
|
+
static forRoot(options: DevDashboardOptions): ReturnType<typeof Module>;
|
|
5
|
+
static getOptions(): DevDashboardOptions;
|
|
6
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Module, RestController, GetMapping, UseGuard, } from "@cinnabun/core";
|
|
11
|
+
import { IntrospectionService } from "./services/introspection.service.js";
|
|
12
|
+
import { MetricsService } from "./services/metrics.service.js";
|
|
13
|
+
import { createDashboardAuthGuard } from "./guards/dashboard-auth.guard.js";
|
|
14
|
+
import { buildDashboardUIHtml } from "./ui/dashboard-ui.js";
|
|
15
|
+
let moduleOptions = null;
|
|
16
|
+
export class DevDashboardModule {
|
|
17
|
+
static forRoot(options) {
|
|
18
|
+
moduleOptions = options;
|
|
19
|
+
const enabled = options.enabled ?? process.env.NODE_ENV === "development";
|
|
20
|
+
const basePath = (options.path ?? "/__cinnabun").replace(/\/$/, "") || "/__cinnabun";
|
|
21
|
+
const authGuard = createDashboardAuthGuard(options.auth?.enabled && options.auth?.secret ? options.auth.secret : undefined);
|
|
22
|
+
if (!enabled) {
|
|
23
|
+
let EmptyDashboardModule = class EmptyDashboardModule {
|
|
24
|
+
};
|
|
25
|
+
EmptyDashboardModule = __decorate([
|
|
26
|
+
Module({
|
|
27
|
+
controllers: [],
|
|
28
|
+
providers: [],
|
|
29
|
+
exports: [],
|
|
30
|
+
})
|
|
31
|
+
], EmptyDashboardModule);
|
|
32
|
+
return EmptyDashboardModule;
|
|
33
|
+
}
|
|
34
|
+
let DynamicDashboardController = class DynamicDashboardController {
|
|
35
|
+
introspectionService;
|
|
36
|
+
metricsService;
|
|
37
|
+
constructor(introspectionService, metricsService) {
|
|
38
|
+
this.introspectionService = introspectionService;
|
|
39
|
+
this.metricsService = metricsService;
|
|
40
|
+
}
|
|
41
|
+
getUI() {
|
|
42
|
+
const html = buildDashboardUIHtml("Cinnabun Dev Dashboard", basePath);
|
|
43
|
+
return new Response(html, {
|
|
44
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
getRoutes() {
|
|
48
|
+
return this.introspectionService.getRoutes();
|
|
49
|
+
}
|
|
50
|
+
getModules() {
|
|
51
|
+
return this.introspectionService.getModules();
|
|
52
|
+
}
|
|
53
|
+
getWebSockets() {
|
|
54
|
+
return this.introspectionService.getWebSocketMappings();
|
|
55
|
+
}
|
|
56
|
+
async getQueue() {
|
|
57
|
+
return this.introspectionService.getQueueJobs();
|
|
58
|
+
}
|
|
59
|
+
async getCache() {
|
|
60
|
+
return this.introspectionService.getCacheKeys();
|
|
61
|
+
}
|
|
62
|
+
async getScheduler() {
|
|
63
|
+
return this.introspectionService.getScheduledTasks();
|
|
64
|
+
}
|
|
65
|
+
getProviders() {
|
|
66
|
+
return this.introspectionService.getProviders();
|
|
67
|
+
}
|
|
68
|
+
getPlugins() {
|
|
69
|
+
return this.introspectionService.getPlugins();
|
|
70
|
+
}
|
|
71
|
+
getMetrics() {
|
|
72
|
+
return this.metricsService.getMetrics() ?? { available: false };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
__decorate([
|
|
76
|
+
GetMapping("/"),
|
|
77
|
+
__metadata("design:type", Function),
|
|
78
|
+
__metadata("design:paramtypes", []),
|
|
79
|
+
__metadata("design:returntype", void 0)
|
|
80
|
+
], DynamicDashboardController.prototype, "getUI", null);
|
|
81
|
+
__decorate([
|
|
82
|
+
GetMapping("/api/routes"),
|
|
83
|
+
__metadata("design:type", Function),
|
|
84
|
+
__metadata("design:paramtypes", []),
|
|
85
|
+
__metadata("design:returntype", void 0)
|
|
86
|
+
], DynamicDashboardController.prototype, "getRoutes", null);
|
|
87
|
+
__decorate([
|
|
88
|
+
GetMapping("/api/modules"),
|
|
89
|
+
__metadata("design:type", Function),
|
|
90
|
+
__metadata("design:paramtypes", []),
|
|
91
|
+
__metadata("design:returntype", void 0)
|
|
92
|
+
], DynamicDashboardController.prototype, "getModules", null);
|
|
93
|
+
__decorate([
|
|
94
|
+
GetMapping("/api/websockets"),
|
|
95
|
+
__metadata("design:type", Function),
|
|
96
|
+
__metadata("design:paramtypes", []),
|
|
97
|
+
__metadata("design:returntype", void 0)
|
|
98
|
+
], DynamicDashboardController.prototype, "getWebSockets", null);
|
|
99
|
+
__decorate([
|
|
100
|
+
GetMapping("/api/queue"),
|
|
101
|
+
__metadata("design:type", Function),
|
|
102
|
+
__metadata("design:paramtypes", []),
|
|
103
|
+
__metadata("design:returntype", Promise)
|
|
104
|
+
], DynamicDashboardController.prototype, "getQueue", null);
|
|
105
|
+
__decorate([
|
|
106
|
+
GetMapping("/api/cache"),
|
|
107
|
+
__metadata("design:type", Function),
|
|
108
|
+
__metadata("design:paramtypes", []),
|
|
109
|
+
__metadata("design:returntype", Promise)
|
|
110
|
+
], DynamicDashboardController.prototype, "getCache", null);
|
|
111
|
+
__decorate([
|
|
112
|
+
GetMapping("/api/scheduler"),
|
|
113
|
+
__metadata("design:type", Function),
|
|
114
|
+
__metadata("design:paramtypes", []),
|
|
115
|
+
__metadata("design:returntype", Promise)
|
|
116
|
+
], DynamicDashboardController.prototype, "getScheduler", null);
|
|
117
|
+
__decorate([
|
|
118
|
+
GetMapping("/api/providers"),
|
|
119
|
+
__metadata("design:type", Function),
|
|
120
|
+
__metadata("design:paramtypes", []),
|
|
121
|
+
__metadata("design:returntype", void 0)
|
|
122
|
+
], DynamicDashboardController.prototype, "getProviders", null);
|
|
123
|
+
__decorate([
|
|
124
|
+
GetMapping("/api/plugins"),
|
|
125
|
+
__metadata("design:type", Function),
|
|
126
|
+
__metadata("design:paramtypes", []),
|
|
127
|
+
__metadata("design:returntype", void 0)
|
|
128
|
+
], DynamicDashboardController.prototype, "getPlugins", null);
|
|
129
|
+
__decorate([
|
|
130
|
+
GetMapping("/api/metrics"),
|
|
131
|
+
__metadata("design:type", Function),
|
|
132
|
+
__metadata("design:paramtypes", []),
|
|
133
|
+
__metadata("design:returntype", void 0)
|
|
134
|
+
], DynamicDashboardController.prototype, "getMetrics", null);
|
|
135
|
+
DynamicDashboardController = __decorate([
|
|
136
|
+
RestController(basePath),
|
|
137
|
+
UseGuard(authGuard),
|
|
138
|
+
__metadata("design:paramtypes", [IntrospectionService,
|
|
139
|
+
MetricsService])
|
|
140
|
+
], DynamicDashboardController);
|
|
141
|
+
let DevDashboardDynamicModule = class DevDashboardDynamicModule {
|
|
142
|
+
};
|
|
143
|
+
DevDashboardDynamicModule = __decorate([
|
|
144
|
+
Module({
|
|
145
|
+
controllers: [DynamicDashboardController],
|
|
146
|
+
providers: [IntrospectionService, MetricsService],
|
|
147
|
+
exports: [IntrospectionService, MetricsService],
|
|
148
|
+
})
|
|
149
|
+
], DevDashboardDynamicModule);
|
|
150
|
+
return DevDashboardDynamicModule;
|
|
151
|
+
}
|
|
152
|
+
static getOptions() {
|
|
153
|
+
if (!moduleOptions) {
|
|
154
|
+
throw new Error("DevDashboardModule not initialized. Call DevDashboardModule.forRoot() first.");
|
|
155
|
+
}
|
|
156
|
+
return moduleOptions;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Logger } from "@cinnabun/core";
|
|
2
|
+
import { IntrospectionService } from "./services/introspection.service.js";
|
|
3
|
+
export class DevDashboardPlugin {
|
|
4
|
+
name = "DevDashboardPlugin";
|
|
5
|
+
async onInit(context) {
|
|
6
|
+
const logger = new Logger("DevDashboardPlugin");
|
|
7
|
+
try {
|
|
8
|
+
const introspectionService = context.container.resolve(IntrospectionService);
|
|
9
|
+
introspectionService.setContainer(context.container);
|
|
10
|
+
logger.info("Dev dashboard initialized");
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
logger.warn("DevDashboardPlugin: IntrospectionService not found. Ensure DevDashboardModule.forRoot() is imported.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { UnauthorizedException } from "@cinnabun/core";
|
|
2
|
+
export function createDashboardAuthGuard(secret) {
|
|
3
|
+
if (!secret) {
|
|
4
|
+
return class NoOpGuard {
|
|
5
|
+
canActivate() {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return class DashboardSecretGuard {
|
|
11
|
+
async canActivate(req) {
|
|
12
|
+
const authHeader = req.headers.get("Authorization");
|
|
13
|
+
const secretHeader = req.headers.get("X-Dashboard-Secret");
|
|
14
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
15
|
+
? authHeader.slice(7)
|
|
16
|
+
: secretHeader;
|
|
17
|
+
if (token !== secret) {
|
|
18
|
+
throw new UnauthorizedException("Invalid or missing dashboard secret");
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DevDashboardModule } from "./dev-dashboard.module.js";
|
|
2
|
+
export { DevDashboardPlugin } from "./dev-dashboard.plugin.js";
|
|
3
|
+
export type { DevDashboardOptions } from "./interfaces/dashboard-options.js";
|
|
4
|
+
export { IntrospectionService } from "./services/introspection.service.js";
|
|
5
|
+
export { MetricsService } from "./services/metrics.service.js";
|
|
6
|
+
export type { RouteInfo, ModuleInfo, WebSocketInfo, QueueJobInfo, CacheKeyInfo, ScheduledTaskInfo, ProviderInfo, PluginInfo, } from "./services/introspection.service.js";
|
|
7
|
+
export type { RequestMetric, MetricsSummary } from "./services/metrics.service.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DevDashboardOptions {
|
|
2
|
+
/** Default: process.env.NODE_ENV === "development" */
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
/** Default: "/__cinnabun" */
|
|
5
|
+
path?: string;
|
|
6
|
+
auth?: {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
/** Simple secret key for dev access */
|
|
9
|
+
secret?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Container } from "@cinnabun/core";
|
|
2
|
+
export interface RouteInfo {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
controller: string;
|
|
6
|
+
handler: string;
|
|
7
|
+
guards: string[];
|
|
8
|
+
interceptors: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ModuleInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
imports: string[];
|
|
13
|
+
controllers: string[];
|
|
14
|
+
providers: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface WebSocketMappingInfo {
|
|
17
|
+
destination: string;
|
|
18
|
+
handler: string;
|
|
19
|
+
broadcastTo?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WebSocketSubscribeInfo {
|
|
22
|
+
destination: string;
|
|
23
|
+
subscriberCount?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface WebSocketInfo {
|
|
26
|
+
brokerPrefixes: string[];
|
|
27
|
+
appPrefixes: string[];
|
|
28
|
+
endpoints: string[];
|
|
29
|
+
messageMappings: WebSocketMappingInfo[];
|
|
30
|
+
subscriptions: WebSocketSubscribeInfo[];
|
|
31
|
+
}
|
|
32
|
+
export interface QueueJobInfo {
|
|
33
|
+
queueName: string;
|
|
34
|
+
jobName: string;
|
|
35
|
+
handler: string;
|
|
36
|
+
status: string;
|
|
37
|
+
}
|
|
38
|
+
export interface CacheKeyInfo {
|
|
39
|
+
key: string;
|
|
40
|
+
ttl?: string;
|
|
41
|
+
size?: string;
|
|
42
|
+
lastAccess?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ScheduledTaskInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
expression?: string;
|
|
47
|
+
nextRun?: string;
|
|
48
|
+
lastRun?: string;
|
|
49
|
+
status: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ProviderInfo {
|
|
52
|
+
name: string;
|
|
53
|
+
scope: string;
|
|
54
|
+
resolved: boolean;
|
|
55
|
+
dependencies?: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface PluginInfo {
|
|
58
|
+
name: string;
|
|
59
|
+
version?: string;
|
|
60
|
+
onInit: boolean;
|
|
61
|
+
onReady: boolean;
|
|
62
|
+
onShutdown: boolean;
|
|
63
|
+
}
|
|
64
|
+
export declare class IntrospectionService {
|
|
65
|
+
private container;
|
|
66
|
+
setContainer(container: Container): void;
|
|
67
|
+
getRoutes(): RouteInfo[];
|
|
68
|
+
getModules(): ModuleInfo[];
|
|
69
|
+
getWebSocketMappings(): WebSocketInfo;
|
|
70
|
+
getProviders(): ProviderInfo[];
|
|
71
|
+
getPlugins(): PluginInfo[];
|
|
72
|
+
getQueueJobs(): Promise<QueueJobInfo[]>;
|
|
73
|
+
getCacheKeys(): Promise<CacheKeyInfo[]>;
|
|
74
|
+
getScheduledTasks(): Promise<ScheduledTaskInfo[]>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { metadataStorage } from "@cinnabun/core";
|
|
2
|
+
export class IntrospectionService {
|
|
3
|
+
container = null;
|
|
4
|
+
setContainer(container) {
|
|
5
|
+
this.container = container;
|
|
6
|
+
}
|
|
7
|
+
getRoutes() {
|
|
8
|
+
const routes = metadataStorage.routes;
|
|
9
|
+
return routes.map((route) => {
|
|
10
|
+
const basePath = metadataStorage.getControllerPath(route.target);
|
|
11
|
+
const methodPath = route.path === "/" ? "" : route.path;
|
|
12
|
+
const fullPath = basePath + methodPath || "/";
|
|
13
|
+
const guards = metadataStorage.getGuardsFor(route.target, route.methodKey);
|
|
14
|
+
const interceptors = metadataStorage.getInterceptorsFor(route.target, route.methodKey);
|
|
15
|
+
return {
|
|
16
|
+
method: route.httpMethod,
|
|
17
|
+
path: fullPath,
|
|
18
|
+
controller: route.target.name,
|
|
19
|
+
handler: route.methodKey,
|
|
20
|
+
guards: guards.map((g) => g.name),
|
|
21
|
+
interceptors: interceptors.map((i) => i.name),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
getModules() {
|
|
26
|
+
const modules = metadataStorage.modules;
|
|
27
|
+
return Array.from(modules.entries()).map(([target, meta]) => ({
|
|
28
|
+
name: target.name,
|
|
29
|
+
imports: meta.imports.map((c) => c.name),
|
|
30
|
+
controllers: meta.controllers.map((c) => c.name),
|
|
31
|
+
providers: meta.providers.map((p) => p.name),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
getWebSocketMappings() {
|
|
35
|
+
const gateways = metadataStorage.getAllWsGateways();
|
|
36
|
+
let brokerPrefixes = [];
|
|
37
|
+
let appPrefixes = [];
|
|
38
|
+
const endpoints = [];
|
|
39
|
+
for (const [, path] of gateways) {
|
|
40
|
+
endpoints.push(path);
|
|
41
|
+
}
|
|
42
|
+
const firstBroker = Array.from(metadataStorage.wsBrokerMetadata.values())[0];
|
|
43
|
+
if (firstBroker) {
|
|
44
|
+
brokerPrefixes = firstBroker.brokerPrefixes;
|
|
45
|
+
appPrefixes = firstBroker.appDestinationPrefixes;
|
|
46
|
+
}
|
|
47
|
+
const messageMappings = [];
|
|
48
|
+
for (const mapping of metadataStorage.messageMappings) {
|
|
49
|
+
const sendTo = metadataStorage.getSendTo(mapping.target, mapping.methodKey);
|
|
50
|
+
messageMappings.push({
|
|
51
|
+
destination: mapping.destination,
|
|
52
|
+
handler: `${mapping.target.name}.${mapping.methodKey}`,
|
|
53
|
+
broadcastTo: sendTo,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const subscriptions = [];
|
|
57
|
+
for (const mapping of metadataStorage.subscribeMappings) {
|
|
58
|
+
subscriptions.push({
|
|
59
|
+
destination: mapping.destination,
|
|
60
|
+
subscriberCount: "—",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
brokerPrefixes,
|
|
65
|
+
appPrefixes,
|
|
66
|
+
endpoints,
|
|
67
|
+
messageMappings,
|
|
68
|
+
subscriptions,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
getProviders() {
|
|
72
|
+
if (!this.container)
|
|
73
|
+
return [];
|
|
74
|
+
const providers = this.container.getProviders();
|
|
75
|
+
return providers.map((p) => ({
|
|
76
|
+
name: p.name,
|
|
77
|
+
scope: "Singleton",
|
|
78
|
+
resolved: true,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
getPlugins() {
|
|
82
|
+
const appMetadata = metadataStorage.appMetadata;
|
|
83
|
+
const plugins = [];
|
|
84
|
+
for (const [, meta] of appMetadata) {
|
|
85
|
+
for (const plugin of meta.plugins || []) {
|
|
86
|
+
const p = plugin;
|
|
87
|
+
plugins.push({
|
|
88
|
+
name: p.name ?? "Unknown",
|
|
89
|
+
version: p.version,
|
|
90
|
+
onInit: "onInit" in plugin && typeof plugin.onInit === "function",
|
|
91
|
+
onReady: "onReady" in plugin && typeof plugin.onReady === "function",
|
|
92
|
+
onShutdown: "onShutdown" in plugin && typeof plugin.onShutdown === "function",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return plugins;
|
|
97
|
+
}
|
|
98
|
+
async getQueueJobs() {
|
|
99
|
+
try {
|
|
100
|
+
const { queueMetadataStorage } = await import("@cinnabun/queue");
|
|
101
|
+
const processors = queueMetadataStorage.getAllProcessors();
|
|
102
|
+
const jobs = [];
|
|
103
|
+
for (const proc of processors) {
|
|
104
|
+
const procJobs = queueMetadataStorage.getJobs(proc.target);
|
|
105
|
+
for (const j of procJobs) {
|
|
106
|
+
jobs.push({
|
|
107
|
+
queueName: proc.queueName,
|
|
108
|
+
jobName: j.jobName,
|
|
109
|
+
handler: `${proc.target.name}.${j.methodKey}`,
|
|
110
|
+
status: "Active",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return jobs;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async getCacheKeys() {
|
|
121
|
+
try {
|
|
122
|
+
const { getCacheService } = await import("@cinnabun/cache");
|
|
123
|
+
const cache = getCacheService();
|
|
124
|
+
if (!cache)
|
|
125
|
+
return [];
|
|
126
|
+
const keys = await cache.keys();
|
|
127
|
+
return keys.map((key) => ({
|
|
128
|
+
key,
|
|
129
|
+
ttl: "—",
|
|
130
|
+
size: "—",
|
|
131
|
+
lastAccess: "—",
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async getScheduledTasks() {
|
|
139
|
+
if (!this.container)
|
|
140
|
+
return [];
|
|
141
|
+
try {
|
|
142
|
+
const { SchedulerService } = await import("@cinnabun/scheduler");
|
|
143
|
+
const scheduler = this.container.resolve(SchedulerService);
|
|
144
|
+
const tasks = scheduler.getTasks();
|
|
145
|
+
return tasks.map((t) => ({
|
|
146
|
+
name: t.name,
|
|
147
|
+
expression: t.expression,
|
|
148
|
+
nextRun: t.nextRun ? String(t.nextRun) : undefined,
|
|
149
|
+
lastRun: t.lastRun ? String(t.lastRun) : undefined,
|
|
150
|
+
status: t.enabled ? "Active" : "Disabled",
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RequestMetric {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export interface MetricsSummary {
|
|
9
|
+
totalRequests: number;
|
|
10
|
+
successRate: number;
|
|
11
|
+
errorRate: number;
|
|
12
|
+
averageResponseTimeMs: number;
|
|
13
|
+
recentErrors: Array<{
|
|
14
|
+
status: number;
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare class MetricsService {
|
|
21
|
+
private requests;
|
|
22
|
+
record(method: string, path: string, statusCode: number, durationMs: number): void;
|
|
23
|
+
getMetrics(): MetricsSummary | null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const MAX_REQUESTS = 1000;
|
|
2
|
+
export class MetricsService {
|
|
3
|
+
requests = [];
|
|
4
|
+
record(method, path, statusCode, durationMs) {
|
|
5
|
+
this.requests.push({
|
|
6
|
+
method,
|
|
7
|
+
path,
|
|
8
|
+
statusCode,
|
|
9
|
+
durationMs,
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
});
|
|
12
|
+
if (this.requests.length > MAX_REQUESTS) {
|
|
13
|
+
this.requests = this.requests.slice(-MAX_REQUESTS);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
getMetrics() {
|
|
17
|
+
if (this.requests.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const total = this.requests.length;
|
|
20
|
+
const success = this.requests.filter((r) => r.statusCode >= 200 && r.statusCode < 400).length;
|
|
21
|
+
const errors = this.requests.filter((r) => r.statusCode >= 400);
|
|
22
|
+
const avg = this.requests.reduce((s, r) => s + r.durationMs, 0) / total;
|
|
23
|
+
return {
|
|
24
|
+
totalRequests: total,
|
|
25
|
+
successRate: (success / total) * 100,
|
|
26
|
+
errorRate: (errors.length / total) * 100,
|
|
27
|
+
averageResponseTimeMs: Math.round(avg),
|
|
28
|
+
recentErrors: errors.slice(-10).map((r) => ({
|
|
29
|
+
status: r.statusCode,
|
|
30
|
+
method: r.method,
|
|
31
|
+
path: r.path,
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildDashboardUIHtml(title: string, basePath: string): string;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
export function buildDashboardUIHtml(title, basePath) {
|
|
2
|
+
const apiBase = `${basePath}/api`;
|
|
3
|
+
const scriptContent = buildScriptContent(apiBase, basePath);
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
+
<title>${escapeHtml(title)}</title>
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; }
|
|
13
|
+
.layout { display: flex; min-height: 100vh; }
|
|
14
|
+
.sidebar { width: 220px; background: #1e293b; padding: 1rem 0; flex-shrink: 0; }
|
|
15
|
+
.sidebar h1 { margin: 0 1rem 1rem; font-size: 1rem; font-weight: 600; color: #f8fafc; }
|
|
16
|
+
.sidebar a { display: block; padding: 0.5rem 1rem; color: #94a3b8; text-decoration: none; cursor: pointer; }
|
|
17
|
+
.sidebar a:hover { background: #334155; color: #fff; }
|
|
18
|
+
.sidebar a.active { background: #334155; color: #38bdf8; }
|
|
19
|
+
.main { flex: 1; padding: 1.5rem; overflow: auto; }
|
|
20
|
+
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
21
|
+
.btn { padding: 0.4rem 0.8rem; background: #334155; color: #e2e8f0; border: 1px solid #475569; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
|
|
22
|
+
.btn:hover { background: #475569; }
|
|
23
|
+
.search { padding: 0.4rem 0.8rem; border: 1px solid #475569; border-radius: 6px; background: #1e293b; color: #e2e8f0; width: 200px; }
|
|
24
|
+
table { width: 100%; border-collapse: collapse; background: #1e293b; border-radius: 8px; overflow: hidden; }
|
|
25
|
+
th, td { padding: 0.6rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
|
|
26
|
+
th { background: #0f172a; font-weight: 600; font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; }
|
|
27
|
+
tr:hover { background: #334155; }
|
|
28
|
+
.empty { display: flex; min-height: 200px; align-items: center; justify-content: center; color: #64748b; }
|
|
29
|
+
.error { color: #f87171; font-size: 0.9rem; }
|
|
30
|
+
.loading { color: #94a3b8; }
|
|
31
|
+
.card { background: #1e293b; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
|
|
32
|
+
.card h3 { margin: 0 0 0.5rem; font-size: 0.9rem; color: #94a3b8; }
|
|
33
|
+
.copy-btn { padding: 0.2rem 0.5rem; font-size: 0.75rem; margin-left: 0.5rem; cursor: pointer; }
|
|
34
|
+
code { font-family: ui-monospace, monospace; font-size: 0.85em; background: #0f172a; padding: 0.1rem 0.3rem; border-radius: 4px; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="layout">
|
|
39
|
+
<nav class="sidebar">
|
|
40
|
+
<h1>${escapeHtml(title)}</h1>
|
|
41
|
+
<a data-section="routes" class="active">Routes</a>
|
|
42
|
+
<a data-section="modules">Modules</a>
|
|
43
|
+
<a data-section="websockets">WebSockets</a>
|
|
44
|
+
<a data-section="queue">Queue</a>
|
|
45
|
+
<a data-section="cache">Cache</a>
|
|
46
|
+
<a data-section="scheduler">Scheduler</a>
|
|
47
|
+
<a data-section="providers">Providers</a>
|
|
48
|
+
<a data-section="plugins">Plugins</a>
|
|
49
|
+
<a data-section="metrics">Metrics</a>
|
|
50
|
+
</nav>
|
|
51
|
+
<main class="main">
|
|
52
|
+
<div class="toolbar">
|
|
53
|
+
<h2 id="section-title">Routes</h2>
|
|
54
|
+
<div>
|
|
55
|
+
<input type="text" class="search" id="search" placeholder="Filter..." style="display:none">
|
|
56
|
+
<button class="btn" id="refresh-btn">Refresh</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="content"></div>
|
|
60
|
+
</main>
|
|
61
|
+
</div>
|
|
62
|
+
<script>${scriptContent}</script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>`;
|
|
65
|
+
}
|
|
66
|
+
function buildScriptContent(apiBase, basePath) {
|
|
67
|
+
return `
|
|
68
|
+
(function() {
|
|
69
|
+
const API_BASE = ${JSON.stringify(apiBase)};
|
|
70
|
+
let currentSection = "routes";
|
|
71
|
+
let searchTerm = "";
|
|
72
|
+
|
|
73
|
+
function escapeHtml(s) {
|
|
74
|
+
if (s == null) return "";
|
|
75
|
+
const div = document.createElement("div");
|
|
76
|
+
div.textContent = String(s);
|
|
77
|
+
return div.innerHTML;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copyToClipboard(text) {
|
|
81
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
82
|
+
const btn = event.target;
|
|
83
|
+
const orig = btn.textContent;
|
|
84
|
+
btn.textContent = "Copied!";
|
|
85
|
+
setTimeout(function() { btn.textContent = orig; }, 1000);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function attrEscape(s) {
|
|
90
|
+
if (s == null) return "";
|
|
91
|
+
return String(s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
document.addEventListener("click", function(e) {
|
|
95
|
+
const btn = e.target.closest("[data-copy]");
|
|
96
|
+
if (btn) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
copyToClipboard(btn.getAttribute("data-copy") || "");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function fetchSection(section) {
|
|
103
|
+
const res = await fetch(API_BASE + "/" + section);
|
|
104
|
+
if (!res.ok) throw new Error("Failed to load: " + res.status);
|
|
105
|
+
return res.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderRoutes(data) {
|
|
109
|
+
if (!data || !data.length) return "<div class='empty'>No routes registered</div>";
|
|
110
|
+
const filtered = data.filter(function(r) {
|
|
111
|
+
if (!searchTerm) return true;
|
|
112
|
+
const s = searchTerm.toLowerCase();
|
|
113
|
+
return (r.method + r.path + r.controller + r.handler).toLowerCase().includes(s);
|
|
114
|
+
});
|
|
115
|
+
let html = "<table><thead><tr><th>Method</th><th>Path</th><th>Controller</th><th>Handler</th><th>Guards</th><th>Interceptors</th></tr></thead><tbody>";
|
|
116
|
+
filtered.forEach(function(r) {
|
|
117
|
+
html += "<tr><td><code>" + escapeHtml(r.method) + "</code></td><td><code>" + escapeHtml(r.path) + "</code><button class='copy-btn btn' data-copy='" + attrEscape(r.path) + "'>Copy</button></td><td>" + escapeHtml(r.controller) + "</td><td>" + escapeHtml(r.handler) + "</td><td>" + (r.guards && r.guards.length ? r.guards.join(", ") : "—") + "</td><td>" + (r.interceptors && r.interceptors.length ? r.interceptors.join(", ") : "—") + "</td></tr>";
|
|
118
|
+
});
|
|
119
|
+
html += "</tbody></table>";
|
|
120
|
+
return html;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderModules(data) {
|
|
124
|
+
if (!data || !data.length) return "<div class='empty'>No modules registered</div>";
|
|
125
|
+
const filtered = data.filter(function(m) {
|
|
126
|
+
if (!searchTerm) return true;
|
|
127
|
+
return (m.name + (m.imports || []).join("") + (m.controllers || []).join("") + (m.providers || []).join("")).toLowerCase().includes(searchTerm.toLowerCase());
|
|
128
|
+
});
|
|
129
|
+
let html = "";
|
|
130
|
+
filtered.forEach(function(m) {
|
|
131
|
+
html += "<div class='card'><h3>" + escapeHtml(m.name) + "</h3>";
|
|
132
|
+
html += "<p><strong>Imports:</strong> " + (m.imports && m.imports.length ? m.imports.join(", ") : "—") + "</p>";
|
|
133
|
+
html += "<p><strong>Controllers:</strong> " + (m.controllers && m.controllers.length ? m.controllers.join(", ") : "—") + "</p>";
|
|
134
|
+
html += "<p><strong>Providers:</strong> " + (m.providers && m.providers.length ? m.providers.join(", ") : "—") + "</p></div>";
|
|
135
|
+
});
|
|
136
|
+
return html || "<div class='empty'>No modules match filter</div>";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderWebSockets(data) {
|
|
140
|
+
if (!data) return "<div class='empty'>No WebSocket configuration</div>";
|
|
141
|
+
let html = "<div class='card'><h3>Broker</h3><p>Prefixes: " + (data.brokerPrefixes && data.brokerPrefixes.length ? data.brokerPrefixes.join(", ") : "—") + "</p>";
|
|
142
|
+
html += "<p>App prefixes: " + (data.appPrefixes && data.appPrefixes.length ? data.appPrefixes.join(", ") : "—") + "</p>";
|
|
143
|
+
html += "<p>Endpoints: " + (data.endpoints && data.endpoints.length ? data.endpoints.join(", ") : "—") + "</p></div>";
|
|
144
|
+
if (data.messageMappings && data.messageMappings.length) {
|
|
145
|
+
html += "<h3 style='margin:1rem 0 0.5rem'>Message Mappings</h3><table><thead><tr><th>Destination</th><th>Handler</th><th>Broadcast To</th></tr></thead><tbody>";
|
|
146
|
+
data.messageMappings.forEach(function(m) {
|
|
147
|
+
html += "<tr><td><code>" + escapeHtml(m.destination) + "</code></td><td>" + escapeHtml(m.handler) + "</td><td>" + (m.broadcastTo ? "<code>" + escapeHtml(m.broadcastTo) + "</code>" : "—") + "</td></tr>";
|
|
148
|
+
});
|
|
149
|
+
html += "</tbody></table>";
|
|
150
|
+
}
|
|
151
|
+
if (data.subscriptions && data.subscriptions.length) {
|
|
152
|
+
html += "<h3 style='margin:1rem 0 0.5rem'>Subscriptions</h3><table><thead><tr><th>Destination</th><th>Subscribers</th></tr></thead><tbody>";
|
|
153
|
+
data.subscriptions.forEach(function(s) {
|
|
154
|
+
html += "<tr><td><code>" + escapeHtml(s.destination) + "</code></td><td>" + (s.subscriberCount || "—") + "</td></tr>";
|
|
155
|
+
});
|
|
156
|
+
html += "</tbody></table>";
|
|
157
|
+
}
|
|
158
|
+
return html || "<div class='empty'>No WebSocket mappings</div>";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderQueue(data) {
|
|
162
|
+
if (!data || !data.length) return "<div class='empty'>No queue jobs (or @cinnabun/queue not installed)</div>";
|
|
163
|
+
let html = "<table><thead><tr><th>Queue</th><th>Job Name</th><th>Handler</th><th>Status</th></tr></thead><tbody>";
|
|
164
|
+
data.forEach(function(j) {
|
|
165
|
+
html += "<tr><td>" + escapeHtml(j.queueName) + "</td><td>" + escapeHtml(j.jobName) + "</td><td><code>" + escapeHtml(j.handler) + "</code></td><td>" + escapeHtml(j.status) + "</td></tr>";
|
|
166
|
+
});
|
|
167
|
+
html += "</tbody></table>";
|
|
168
|
+
return html;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderCache(data) {
|
|
172
|
+
if (!data || !data.length) return "<div class='empty'>No cache keys (or @cinnabun/cache not installed)</div>";
|
|
173
|
+
let html = "<table><thead><tr><th>Key</th><th>TTL</th><th>Size</th><th>Last Access</th></tr></thead><tbody>";
|
|
174
|
+
data.forEach(function(k) {
|
|
175
|
+
html += "<tr><td><code>" + escapeHtml(k.key) + "</code><button class='copy-btn btn' data-copy='" + attrEscape(k.key) + "'>Copy</button></td><td>" + (k.ttl || "—") + "</td><td>" + (k.size || "—") + "</td><td>" + (k.lastAccess || "—") + "</td></tr>";
|
|
176
|
+
});
|
|
177
|
+
html += "</tbody></table>";
|
|
178
|
+
return html;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderScheduler(data) {
|
|
182
|
+
if (!data || !data.length) return "<div class='empty'>No scheduled tasks (or @cinnabun/scheduler not installed)</div>";
|
|
183
|
+
let html = "<table><thead><tr><th>Task</th><th>Expression</th><th>Next Run</th><th>Last Run</th><th>Status</th></tr></thead><tbody>";
|
|
184
|
+
data.forEach(function(t) {
|
|
185
|
+
html += "<tr><td>" + escapeHtml(t.name) + "</td><td><code>" + escapeHtml(t.expression || "—") + "</code></td><td>" + (t.nextRun || "—") + "</td><td>" + (t.lastRun || "—") + "</td><td>" + escapeHtml(t.status) + "</td></tr>";
|
|
186
|
+
});
|
|
187
|
+
html += "</tbody></table>";
|
|
188
|
+
return html;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderProviders(data) {
|
|
192
|
+
if (!data || !data.length) return "<div class='empty'>No providers resolved</div>";
|
|
193
|
+
let html = "<table><thead><tr><th>Provider</th><th>Scope</th><th>Resolved</th></tr></thead><tbody>";
|
|
194
|
+
data.forEach(function(p) {
|
|
195
|
+
html += "<tr><td>" + escapeHtml(p.name) + "</td><td>" + escapeHtml(p.scope) + "</td><td>" + (p.resolved ? "Yes" : "No") + "</td></tr>";
|
|
196
|
+
});
|
|
197
|
+
html += "</tbody></table>";
|
|
198
|
+
return html;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderPlugins(data) {
|
|
202
|
+
if (!data || !data.length) return "<div class='empty'>No plugins registered</div>";
|
|
203
|
+
let html = "<table><thead><tr><th>Plugin</th><th>Version</th><th>onInit</th><th>onReady</th><th>onShutdown</th></tr></thead><tbody>";
|
|
204
|
+
data.forEach(function(p) {
|
|
205
|
+
html += "<tr><td>" + escapeHtml(p.name) + "</td><td>" + (p.version || "—") + "</td><td>" + (p.onInit ? "✓" : "—") + "</td><td>" + (p.onReady ? "✓" : "—") + "</td><td>" + (p.onShutdown ? "✓" : "Pending") + "</td></tr>";
|
|
206
|
+
});
|
|
207
|
+
html += "</tbody></table>";
|
|
208
|
+
return html;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function renderMetrics(data) {
|
|
212
|
+
if (!data || data.available === false) return "<div class='empty'>Metrics not available. Add MetricsInterceptor to collect request metrics.</div>";
|
|
213
|
+
let html = "<div class='card'><h3>Summary</h3>";
|
|
214
|
+
html += "<p><strong>Total Requests:</strong> " + data.totalRequests + "</p>";
|
|
215
|
+
html += "<p><strong>Success Rate:</strong> " + data.successRate.toFixed(1) + "%</p>";
|
|
216
|
+
html += "<p><strong>Error Rate:</strong> " + data.errorRate.toFixed(1) + "%</p>";
|
|
217
|
+
html += "<p><strong>Avg Response Time:</strong> " + data.averageResponseTimeMs + "ms</p></div>";
|
|
218
|
+
if (data.recentErrors && data.recentErrors.length) {
|
|
219
|
+
html += "<h3 style='margin:1rem 0 0.5rem'>Recent Errors</h3><table><thead><tr><th>Status</th><th>Method</th><th>Path</th></tr></thead><tbody>";
|
|
220
|
+
data.recentErrors.forEach(function(e) {
|
|
221
|
+
html += "<tr><td>" + e.status + "</td><td>" + escapeHtml(e.method) + "</td><td>" + escapeHtml(e.path) + "</td></tr>";
|
|
222
|
+
});
|
|
223
|
+
html += "</tbody></table>";
|
|
224
|
+
}
|
|
225
|
+
return html;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const renderers = {
|
|
229
|
+
routes: renderRoutes,
|
|
230
|
+
modules: renderModules,
|
|
231
|
+
websockets: renderWebSockets,
|
|
232
|
+
queue: renderQueue,
|
|
233
|
+
cache: renderCache,
|
|
234
|
+
scheduler: renderScheduler,
|
|
235
|
+
providers: renderProviders,
|
|
236
|
+
plugins: renderPlugins,
|
|
237
|
+
metrics: renderMetrics
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const titles = {
|
|
241
|
+
routes: "Routes",
|
|
242
|
+
modules: "Modules",
|
|
243
|
+
websockets: "WebSockets",
|
|
244
|
+
queue: "Queue Jobs",
|
|
245
|
+
cache: "Cache Keys",
|
|
246
|
+
scheduler: "Scheduled Tasks",
|
|
247
|
+
providers: "DI Providers",
|
|
248
|
+
plugins: "Plugins",
|
|
249
|
+
metrics: "Request Metrics"
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
async function loadSection(section) {
|
|
253
|
+
currentSection = section;
|
|
254
|
+
document.getElementById("section-title").textContent = titles[section] || section;
|
|
255
|
+
document.querySelectorAll(".sidebar a").forEach(function(a) {
|
|
256
|
+
a.classList.toggle("active", a.dataset.section === section);
|
|
257
|
+
});
|
|
258
|
+
const content = document.getElementById("content");
|
|
259
|
+
content.innerHTML = "<div class='loading'>Loading...</div>";
|
|
260
|
+
const searchEl = document.getElementById("search");
|
|
261
|
+
searchEl.style.display = (section === "routes" || section === "modules") ? "inline-block" : "none";
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const data = await fetchSection(section);
|
|
265
|
+
const render = renderers[section];
|
|
266
|
+
content.innerHTML = render ? render(data) : JSON.stringify(data, null, 2);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
content.innerHTML = "<div class='error'>" + escapeHtml(err.message) + "</div>";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function applyFilter() {
|
|
273
|
+
searchTerm = (document.getElementById("search").value || "").trim();
|
|
274
|
+
loadSection(currentSection);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
document.querySelectorAll(".sidebar a").forEach(function(a) {
|
|
278
|
+
a.addEventListener("click", function(e) {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
loadSection(a.dataset.section);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
document.getElementById("refresh-btn").addEventListener("click", function() {
|
|
285
|
+
loadSection(currentSection);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
document.getElementById("search").addEventListener("input", function() {
|
|
289
|
+
clearTimeout(window.searchTimeout);
|
|
290
|
+
window.searchTimeout = setTimeout(applyFilter, 200);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
setInterval(function() { loadSection(currentSection); }, 10000);
|
|
294
|
+
|
|
295
|
+
loadSection("routes");
|
|
296
|
+
})();
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
function escapeHtml(s) {
|
|
300
|
+
return s
|
|
301
|
+
.replace(/&/g, "&")
|
|
302
|
+
.replace(/</g, "<")
|
|
303
|
+
.replace(/>/g, ">")
|
|
304
|
+
.replace(/"/g, """)
|
|
305
|
+
.replace(/'/g, "'");
|
|
306
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cinnabun/dev-dashboard",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Development-only dashboard for introspection and debugging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "bun run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["cinnabun", "dev", "dashboard", "introspection"],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@cinnabun/core": "^0.0.3",
|
|
20
|
+
"@cinnabun/queue": ">=0.0.1",
|
|
21
|
+
"@cinnabun/cache": ">=0.0.1",
|
|
22
|
+
"@cinnabun/scheduler": ">=0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"@cinnabun/queue": { "optional": true },
|
|
26
|
+
"@cinnabun/cache": { "optional": true },
|
|
27
|
+
"@cinnabun/scheduler": { "optional": true }
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@cinnabun/core": "workspace:*",
|
|
31
|
+
"@cinnabun/testing": "workspace:*",
|
|
32
|
+
"@types/bun": "latest",
|
|
33
|
+
"reflect-metadata": "^0.2.2",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|