@geekmidas/studio 0.1.0 → 0.4.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/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +14 -6
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
package/src/server/hono.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
1
|
import type { Context } from 'hono';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
3
|
import type { DataBrowser } from '../data/DataBrowser';
|
|
4
|
+
import type { Studio } from '../Studio';
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Direction,
|
|
7
|
+
type FilterCondition,
|
|
8
|
+
FilterOperator,
|
|
9
|
+
type SortConfig,
|
|
9
10
|
} from '../types';
|
|
10
11
|
import { getAsset, getIndexHtml } from '../ui-assets';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Interface for the Studio instance used by the Hono adapter.
|
|
14
|
-
* Only requires the data browser - monitoring routes are separate.
|
|
15
15
|
*/
|
|
16
16
|
export interface StudioLike {
|
|
17
|
-
|
|
17
|
+
data: DataBrowser<unknown>;
|
|
18
|
+
// Monitoring methods
|
|
19
|
+
getRequests: Studio<unknown>['getRequests'];
|
|
20
|
+
getRequest: Studio<unknown>['getRequest'];
|
|
21
|
+
getExceptions: Studio<unknown>['getExceptions'];
|
|
22
|
+
getException: Studio<unknown>['getException'];
|
|
23
|
+
getLogs: Studio<unknown>['getLogs'];
|
|
24
|
+
getStats: Studio<unknown>['getStats'];
|
|
25
|
+
// Metrics methods
|
|
26
|
+
getMetrics: Studio<unknown>['getMetrics'];
|
|
27
|
+
getEndpointMetrics: Studio<unknown>['getEndpointMetrics'];
|
|
28
|
+
getEndpointDetails: Studio<unknown>['getEndpointDetails'];
|
|
29
|
+
getStatusDistribution: Studio<unknown>['getStatusDistribution'];
|
|
30
|
+
resetMetrics: Studio<unknown>['resetMetrics'];
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
/**
|
|
@@ -23,42 +36,45 @@ export interface StudioLike {
|
|
|
23
36
|
* Example: filter[name][eq]=John&filter[age][gt]=18
|
|
24
37
|
*/
|
|
25
38
|
function parseFilters(c: Context): FilterCondition[] {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
const filters: FilterCondition[] = [];
|
|
40
|
+
const url = new URL(c.req.url);
|
|
41
|
+
|
|
42
|
+
url.searchParams.forEach((value, key) => {
|
|
43
|
+
const match = key.match(/^filter\[(\w+)\]\[(\w+)\]$/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const column = match[1];
|
|
46
|
+
const operator = match[2];
|
|
47
|
+
if (!column || !operator) return;
|
|
48
|
+
|
|
49
|
+
const op = operator as FilterOperator;
|
|
50
|
+
|
|
51
|
+
// Validate operator
|
|
52
|
+
if (!Object.values(FilterOperator).includes(op)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle special cases
|
|
57
|
+
if (op === FilterOperator.In || op === FilterOperator.Nin) {
|
|
58
|
+
filters.push({ column, operator: op, value: value.split(',') });
|
|
59
|
+
} else if (
|
|
60
|
+
op === FilterOperator.IsNull ||
|
|
61
|
+
op === FilterOperator.IsNotNull
|
|
62
|
+
) {
|
|
63
|
+
filters.push({ column, operator: op });
|
|
64
|
+
} else {
|
|
65
|
+
// Try to parse as number or boolean
|
|
66
|
+
let parsedValue: unknown = value;
|
|
67
|
+
if (value === 'true') parsedValue = true;
|
|
68
|
+
else if (value === 'false') parsedValue = false;
|
|
69
|
+
else if (!Number.isNaN(Number(value)) && value !== '')
|
|
70
|
+
parsedValue = Number(value);
|
|
71
|
+
|
|
72
|
+
filters.push({ column, operator: op, value: parsedValue });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return filters;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/**
|
|
@@ -67,159 +83,345 @@ function parseFilters(c: Context): FilterCondition[] {
|
|
|
67
83
|
* Example: sort=name:asc,created_at:desc
|
|
68
84
|
*/
|
|
69
85
|
function parseSort(c: Context): SortConfig[] {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const sortParam = c.req.query('sort');
|
|
87
|
+
if (!sortParam) return [];
|
|
88
|
+
|
|
89
|
+
return sortParam
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((part) => {
|
|
92
|
+
const parts = part.split(':');
|
|
93
|
+
const column = parts[0];
|
|
94
|
+
const dir = parts[1];
|
|
95
|
+
if (!column) return null;
|
|
96
|
+
return {
|
|
97
|
+
column,
|
|
98
|
+
direction: dir === 'desc' ? Direction.Desc : Direction.Asc,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.filter((s): s is SortConfig => s !== null);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse query options for monitoring endpoints.
|
|
106
|
+
*/
|
|
107
|
+
function parseQueryOptions(c: Context) {
|
|
108
|
+
const limit = parseInt(c.req.query('limit') || '50', 10);
|
|
109
|
+
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
110
|
+
const search = c.req.query('search');
|
|
111
|
+
const before = c.req.query('before');
|
|
112
|
+
const after = c.req.query('after');
|
|
113
|
+
const tags = c.req.query('tags')?.split(',').filter(Boolean);
|
|
114
|
+
const method = c.req.query('method');
|
|
115
|
+
const status = c.req.query('status');
|
|
116
|
+
const level = c.req.query('level') as
|
|
117
|
+
| 'debug'
|
|
118
|
+
| 'info'
|
|
119
|
+
| 'warn'
|
|
120
|
+
| 'error'
|
|
121
|
+
| undefined;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
limit: Math.min(limit, 100),
|
|
125
|
+
offset,
|
|
126
|
+
search,
|
|
127
|
+
before: before ? new Date(before) : undefined,
|
|
128
|
+
after: after ? new Date(after) : undefined,
|
|
129
|
+
tags,
|
|
130
|
+
method: method || undefined,
|
|
131
|
+
status: status || undefined,
|
|
132
|
+
level: level || undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse metrics query options from query parameters.
|
|
138
|
+
*/
|
|
139
|
+
function parseMetricsQueryOptions(c: Context) {
|
|
140
|
+
const start = c.req.query('start');
|
|
141
|
+
const end = c.req.query('end');
|
|
142
|
+
const bucketSize = c.req.query('bucketSize');
|
|
143
|
+
const limit = c.req.query('limit');
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
range:
|
|
147
|
+
start && end ? { start: new Date(start), end: new Date(end) } : undefined,
|
|
148
|
+
bucketSize: bucketSize ? parseInt(bucketSize, 10) : undefined,
|
|
149
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
150
|
+
};
|
|
80
151
|
}
|
|
81
152
|
|
|
82
153
|
/**
|
|
83
154
|
* Create Hono app with Studio API routes and dashboard UI.
|
|
84
155
|
*/
|
|
85
156
|
export function createStudioApp(studio: StudioLike): Hono {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
157
|
+
const app = new Hono();
|
|
158
|
+
|
|
159
|
+
// ============================================
|
|
160
|
+
// Database API
|
|
161
|
+
// ============================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* GET /api/schema
|
|
165
|
+
* Get the complete database schema
|
|
166
|
+
*/
|
|
167
|
+
app.get('/api/schema', async (c) => {
|
|
168
|
+
const forceRefresh = c.req.query('refresh') === 'true';
|
|
169
|
+
const schema = await studio.data.getSchema(forceRefresh);
|
|
170
|
+
return c.json(schema);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* GET /api/tables
|
|
175
|
+
* List all tables with basic info
|
|
176
|
+
*/
|
|
177
|
+
app.get('/api/tables', async (c) => {
|
|
178
|
+
const schema = await studio.data.getSchema();
|
|
179
|
+
const tables = schema.tables.map((t) => ({
|
|
180
|
+
name: t.name,
|
|
181
|
+
schema: t.schema,
|
|
182
|
+
columnCount: t.columns.length,
|
|
183
|
+
primaryKey: t.primaryKey,
|
|
184
|
+
estimatedRowCount: t.estimatedRowCount,
|
|
185
|
+
}));
|
|
186
|
+
return c.json({ tables });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* GET /api/tables/:name
|
|
191
|
+
* Get detailed information about a specific table
|
|
192
|
+
*/
|
|
193
|
+
app.get('/api/tables/:name', async (c) => {
|
|
194
|
+
const tableName = c.req.param('name');
|
|
195
|
+
const tableInfo = await studio.data.getTableInfo(tableName);
|
|
196
|
+
|
|
197
|
+
if (!tableInfo) {
|
|
198
|
+
return c.json({ error: `Table '${tableName}' not found` }, 404);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return c.json(tableInfo);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* GET /api/tables/:name/rows
|
|
206
|
+
* Query table data with pagination, filtering, and sorting
|
|
207
|
+
*/
|
|
208
|
+
app.get('/api/tables/:name/rows', async (c) => {
|
|
209
|
+
const tableName = c.req.param('name');
|
|
210
|
+
const pageSize = Math.min(
|
|
211
|
+
parseInt(c.req.query('pageSize') || '50', 10),
|
|
212
|
+
100,
|
|
213
|
+
);
|
|
214
|
+
const cursor = c.req.query('cursor') || undefined;
|
|
215
|
+
const filters = parseFilters(c);
|
|
216
|
+
const sort = parseSort(c);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const result = await studio.data.query({
|
|
220
|
+
table: tableName,
|
|
221
|
+
pageSize,
|
|
222
|
+
cursor,
|
|
223
|
+
filters: filters.length > 0 ? filters : undefined,
|
|
224
|
+
sort: sort.length > 0 ? sort : undefined,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return c.json(result);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
230
|
+
return c.json({ error: error.message }, 404);
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ============================================
|
|
237
|
+
// Monitoring API
|
|
238
|
+
// ============================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* GET /api/stats
|
|
242
|
+
* Get storage statistics
|
|
243
|
+
*/
|
|
244
|
+
app.get('/api/stats', async (c) => {
|
|
245
|
+
const stats = await studio.getStats();
|
|
246
|
+
return c.json(stats);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* GET /api/requests
|
|
251
|
+
* Get request entries
|
|
252
|
+
*/
|
|
253
|
+
app.get('/api/requests', async (c) => {
|
|
254
|
+
const options = parseQueryOptions(c);
|
|
255
|
+
const requests = await studio.getRequests(options);
|
|
256
|
+
return c.json(requests);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* GET /api/requests/:id
|
|
261
|
+
* Get a single request by ID
|
|
262
|
+
*/
|
|
263
|
+
app.get('/api/requests/:id', async (c) => {
|
|
264
|
+
const request = await studio.getRequest(c.req.param('id'));
|
|
265
|
+
if (!request) {
|
|
266
|
+
return c.json({ error: 'Request not found' }, 404);
|
|
267
|
+
}
|
|
268
|
+
return c.json(request);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* GET /api/exceptions
|
|
273
|
+
* Get exception entries
|
|
274
|
+
*/
|
|
275
|
+
app.get('/api/exceptions', async (c) => {
|
|
276
|
+
const options = parseQueryOptions(c);
|
|
277
|
+
const exceptions = await studio.getExceptions(options);
|
|
278
|
+
return c.json(exceptions);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* GET /api/exceptions/:id
|
|
283
|
+
* Get a single exception by ID
|
|
284
|
+
*/
|
|
285
|
+
app.get('/api/exceptions/:id', async (c) => {
|
|
286
|
+
const exception = await studio.getException(c.req.param('id'));
|
|
287
|
+
if (!exception) {
|
|
288
|
+
return c.json({ error: 'Exception not found' }, 404);
|
|
289
|
+
}
|
|
290
|
+
return c.json(exception);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* GET /api/logs
|
|
295
|
+
* Get log entries
|
|
296
|
+
*/
|
|
297
|
+
app.get('/api/logs', async (c) => {
|
|
298
|
+
const options = parseQueryOptions(c);
|
|
299
|
+
const logs = await studio.getLogs(options);
|
|
300
|
+
return c.json(logs);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ============================================
|
|
304
|
+
// Metrics API
|
|
305
|
+
// ============================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* GET /api/metrics
|
|
309
|
+
* Get aggregated request metrics
|
|
310
|
+
*/
|
|
311
|
+
app.get('/api/metrics', (c) => {
|
|
312
|
+
const options = parseMetricsQueryOptions(c);
|
|
313
|
+
const metrics = studio.getMetrics(options);
|
|
314
|
+
return c.json(metrics);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* GET /api/metrics/endpoints
|
|
319
|
+
* Get metrics grouped by endpoint
|
|
320
|
+
*/
|
|
321
|
+
app.get('/api/metrics/endpoints', (c) => {
|
|
322
|
+
const options = parseMetricsQueryOptions(c);
|
|
323
|
+
const endpoints = studio.getEndpointMetrics(options);
|
|
324
|
+
return c.json(endpoints);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* GET /api/metrics/endpoint
|
|
329
|
+
* Get detailed metrics for a specific endpoint
|
|
330
|
+
*/
|
|
331
|
+
app.get('/api/metrics/endpoint', (c) => {
|
|
332
|
+
const method = c.req.query('method');
|
|
333
|
+
const path = c.req.query('path');
|
|
334
|
+
|
|
335
|
+
if (!method || !path) {
|
|
336
|
+
return c.json({ error: 'method and path are required' }, 400);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const options = parseMetricsQueryOptions(c);
|
|
340
|
+
const details = studio.getEndpointDetails(method, path, options);
|
|
341
|
+
|
|
342
|
+
if (!details) {
|
|
343
|
+
return c.json({ error: 'Endpoint not found' }, 404);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return c.json(details);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* GET /api/metrics/status
|
|
351
|
+
* Get HTTP status code distribution
|
|
352
|
+
*/
|
|
353
|
+
app.get('/api/metrics/status', (c) => {
|
|
354
|
+
const options = parseMetricsQueryOptions(c);
|
|
355
|
+
const distribution = studio.getStatusDistribution(options);
|
|
356
|
+
return c.json(distribution);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* DELETE /api/metrics
|
|
361
|
+
* Reset all metrics
|
|
362
|
+
*/
|
|
363
|
+
app.delete('/api/metrics', (c) => {
|
|
364
|
+
studio.resetMetrics();
|
|
365
|
+
return c.json({ success: true });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ============================================
|
|
369
|
+
// Static Assets & Dashboard UI
|
|
370
|
+
// ============================================
|
|
371
|
+
|
|
372
|
+
// Static assets
|
|
373
|
+
app.get('/assets/:filename', (c) => {
|
|
374
|
+
const filename = c.req.param('filename');
|
|
375
|
+
const assetPath = `assets/${filename}`;
|
|
376
|
+
const asset = getAsset(assetPath);
|
|
377
|
+
if (asset) {
|
|
378
|
+
return c.body(asset.content, 200, {
|
|
379
|
+
'Content-Type': asset.contentType,
|
|
380
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return c.notFound();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Dashboard UI - serve React app
|
|
387
|
+
app.get('/', (c) => {
|
|
388
|
+
const html = getIndexHtml();
|
|
389
|
+
if (!html) {
|
|
390
|
+
return c.json({
|
|
391
|
+
message: 'Studio API is running',
|
|
392
|
+
note: 'UI not available. Run "pnpm build:ui" first.',
|
|
393
|
+
endpoints: {
|
|
394
|
+
schema: '/api/schema',
|
|
395
|
+
tables: '/api/tables',
|
|
396
|
+
tableInfo: '/api/tables/:name',
|
|
397
|
+
tableRows: '/api/tables/:name/rows',
|
|
398
|
+
stats: '/api/stats',
|
|
399
|
+
requests: '/api/requests',
|
|
400
|
+
exceptions: '/api/exceptions',
|
|
401
|
+
logs: '/api/logs',
|
|
402
|
+
metrics: '/api/metrics',
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return c.html(html);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// SPA fallback - serve index.html for client-side routing
|
|
410
|
+
app.get('/*', (c) => {
|
|
411
|
+
// Return 404 JSON for API routes
|
|
412
|
+
if (c.req.path.startsWith('/api/')) {
|
|
413
|
+
return c.json({ error: 'Not found' }, 404);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const html = getIndexHtml();
|
|
417
|
+
if (!html) {
|
|
418
|
+
return c.notFound();
|
|
419
|
+
}
|
|
420
|
+
return c.html(html);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return app;
|
|
222
424
|
}
|
|
223
425
|
|
|
224
426
|
// Re-export types
|
|
225
|
-
export type {
|
|
427
|
+
export type { DataBrowser };
|