tina4-nodejs 3.11.5 → 3.11.9
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/package.json +1 -1
- package/packages/cli/src/commands/serve.ts +0 -1
- package/packages/core/public/js/tina4-dev-admin.js +146 -92
- package/packages/core/public/js/tina4-dev-admin.min.js +145 -91
- package/packages/core/src/devAdmin.ts +46 -1636
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/response.ts +7 -0
- package/packages/core/src/types.ts +1 -0
- package/packages/core/src/websocketConnection.ts +2 -0
- package/packages/core/src/watcher.ts +0 -126
|
@@ -489,6 +489,32 @@ export class DevAdmin {
|
|
|
489
489
|
{ method: "GET", pattern: "/__dev/api/metrics", handler: (_req: any, res: any) => { res.json(quickMetrics()); } },
|
|
490
490
|
{ method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req: any, res: any) => { res.json(fullAnalysis()); } },
|
|
491
491
|
{ method: "GET", pattern: "/__dev/api/metrics/file", handler: (req: any, res: any) => { const url = new URL(req.url ?? "/", "http://localhost"); const p = (url.searchParams.get("path") || "").toString(); res.json(fileDetail(p)); } },
|
|
492
|
+
// GraphQL schema introspection (auto-discovers registered ORM models)
|
|
493
|
+
{ method: "GET", pattern: "/__dev/api/graphql/schema", handler: async (_req: any, res: any) => {
|
|
494
|
+
try {
|
|
495
|
+
const { GraphQL } = require("./graphql.js");
|
|
496
|
+
const gql = new GraphQL();
|
|
497
|
+
|
|
498
|
+
// Auto-discover ORM models from BaseModel registry
|
|
499
|
+
try {
|
|
500
|
+
const orm = await import("@tina4/orm");
|
|
501
|
+
const registry = (orm as any).BaseModel?._modelRegistry as Record<string, any> | undefined;
|
|
502
|
+
if (registry) {
|
|
503
|
+
for (const modelClass of Object.values(registry)) {
|
|
504
|
+
if (modelClass?.tableName && modelClass?.fields) {
|
|
505
|
+
gql.fromOrm(modelClass);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch (_ormErr: any) {
|
|
510
|
+
// ORM package not available — continue with empty schema
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
res.json({ schema: gql.introspect(), sdl: gql.schemaSdl() });
|
|
514
|
+
} catch (e: any) {
|
|
515
|
+
res.json({ error: e.message }, 400);
|
|
516
|
+
}
|
|
517
|
+
}},
|
|
492
518
|
// Version check (proxy to avoid CORS)
|
|
493
519
|
{ method: "GET", pattern: "/__dev/api/version-check", handler: handleVersionCheck },
|
|
494
520
|
// JS asset
|
|
@@ -590,13 +616,20 @@ function handleRoutes(router: Router): RouteHandler {
|
|
|
590
616
|
const allRoutes = router.getRoutes();
|
|
591
617
|
const result = allRoutes
|
|
592
618
|
.filter((r) => !internalPrefixes.some((prefix) => r.pattern.startsWith(prefix)))
|
|
593
|
-
.map((r) =>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
619
|
+
.map((r) => {
|
|
620
|
+
const filePath = r.filePath ?? null;
|
|
621
|
+
return {
|
|
622
|
+
method: r.method,
|
|
623
|
+
path: r.pattern,
|
|
624
|
+
pattern: r.pattern,
|
|
625
|
+
auth_required: (r.secure ?? false) && !(r.noAuth ?? false),
|
|
626
|
+
handler: filePath ? `${filePath.split("/").pop()}` : "Closure",
|
|
627
|
+
module: filePath ? filePath.substring(0, filePath.lastIndexOf("/")) : "",
|
|
628
|
+
filePath,
|
|
629
|
+
hasMiddleware: (r.middlewares?.length ?? 0) > 0,
|
|
630
|
+
meta: r.meta ?? null,
|
|
631
|
+
};
|
|
632
|
+
});
|
|
600
633
|
res.json({ routes: result, count: result.length });
|
|
601
634
|
};
|
|
602
635
|
}
|
|
@@ -1323,1641 +1356,18 @@ const handleDevAdminJs: RouteHandler = async (_req, res) => {
|
|
|
1323
1356
|
if (existsSync(jsPath)) {
|
|
1324
1357
|
try {
|
|
1325
1358
|
const content = readFileSync(jsPath, "utf-8");
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1359
|
+
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1360
|
+
res.raw.end(content);
|
|
1361
|
+
return;
|
|
1331
1362
|
} catch { /* try next */ }
|
|
1332
1363
|
}
|
|
1333
1364
|
}
|
|
1334
1365
|
|
|
1335
|
-
//
|
|
1336
|
-
res.raw.writeHead(
|
|
1337
|
-
res.raw.end(
|
|
1366
|
+
// File not found — no legacy fallback
|
|
1367
|
+
res.raw.writeHead(404, { "Content-Type": "text/plain" });
|
|
1368
|
+
res.raw.end("tina4-dev-admin.min.js not found");
|
|
1338
1369
|
};
|
|
1339
1370
|
|
|
1340
|
-
// ---------------------------------------------------------------------------
|
|
1341
|
-
// Shared Dev Admin JS — cross-language, vanilla JS, zero dependencies
|
|
1342
|
-
// ---------------------------------------------------------------------------
|
|
1343
|
-
|
|
1344
|
-
function renderAppShell(): string[] {
|
|
1345
|
-
return [
|
|
1346
|
-
"let currentTab = 'routes';",
|
|
1347
|
-
"let queueFilter = '';",
|
|
1348
|
-
"let mailboxFolder = '';",
|
|
1349
|
-
"",
|
|
1350
|
-
"function showTab(tab, e) {",
|
|
1351
|
-
" currentTab = tab;",
|
|
1352
|
-
" document.querySelectorAll('.dev-tab').forEach(function(t) { t.classList.remove('active'); });",
|
|
1353
|
-
" document.querySelectorAll('.dev-panel').forEach(function(p) { p.classList.add('hidden'); });",
|
|
1354
|
-
" if (e) e.target.closest('.dev-tab').classList.add('active');",
|
|
1355
|
-
" document.getElementById('panel-' + tab).classList.remove('hidden');",
|
|
1356
|
-
" var loaders = {routes:loadRoutes, queue:loadQueue, mailbox:loadMailbox, messages:loadMessages, database:loadTables, requests:loadRequests, errors:loadErrors, websockets:loadWebSockets, system:loadSystem, tools:function(){}};",
|
|
1357
|
-
" if (loaders[tab]) loaders[tab]();",
|
|
1358
|
-
"}",
|
|
1359
|
-
"",
|
|
1360
|
-
"function api(path, method, body) {",
|
|
1361
|
-
" var opts = { method: method || 'GET', headers: {} };",
|
|
1362
|
-
" if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }",
|
|
1363
|
-
" return fetch(path, opts).then(function(r) { return r.json(); });",
|
|
1364
|
-
"}",
|
|
1365
|
-
];
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
function renderRoutesTab(): string[] {
|
|
1369
|
-
return [
|
|
1370
|
-
"// -- Routes --",
|
|
1371
|
-
"function loadRoutes() {",
|
|
1372
|
-
" api('/__dev/api/routes').then(function(d) {",
|
|
1373
|
-
" document.getElementById('routes-count').textContent = d.count;",
|
|
1374
|
-
" document.getElementById('routes-body').innerHTML = d.routes.map(function(r) {",
|
|
1375
|
-
" return '<tr>' +",
|
|
1376
|
-
" '<td><span class=\"method method-' + r.method.toLowerCase() + '\">' + r.method + '</span></td>' +",
|
|
1377
|
-
" '<td class=\"path\"><a href=\"' + (r.path || r.pattern || '') + '\" target=\"_blank\" title=\"' + (r.method !== 'GET' ? r.method + ' route \\u2014 may not respond to browser GET' : 'Open in new tab') + '\" style=\"color:inherit;text-decoration:underline dotted;' + (r.method !== 'GET' ? 'opacity:0.7' : '') + '\">' + (r.path || r.pattern || '') + '</a></td>' +",
|
|
1378
|
-
" '<td>' + (r.auth_required || r.secure ? '<span class=\"badge-pill bg-reserved\">auth</span>' : '<span class=\"badge-pill bg-success\">open</span>') + '</td>' +",
|
|
1379
|
-
" '<td class=\"text-sm text-muted\">' + (r.handler || '') + (r.module ? ' <small>(' + r.module + ')</small>' : '') + '</td>' +",
|
|
1380
|
-
" '</tr>';",
|
|
1381
|
-
" }).join('');",
|
|
1382
|
-
" });",
|
|
1383
|
-
"}",
|
|
1384
|
-
];
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
function renderQueueTab(): string[] {
|
|
1388
|
-
return [
|
|
1389
|
-
"// -- Queue --",
|
|
1390
|
-
"function loadQueue() {",
|
|
1391
|
-
" var qs = queueFilter ? '?status=' + queueFilter : '';",
|
|
1392
|
-
" api('/__dev/api/queue' + qs).then(function(d) {",
|
|
1393
|
-
" ['pending','completed','failed','reserved'].forEach(function(s) {",
|
|
1394
|
-
" var el = document.getElementById('q-' + s);",
|
|
1395
|
-
" if (el) el.textContent = d.stats[s] || 0;",
|
|
1396
|
-
" });",
|
|
1397
|
-
" document.getElementById('queue-count').textContent = Object.values(d.stats).reduce(function(a,b){return a+b;}, 0);",
|
|
1398
|
-
" var tbody = document.getElementById('queue-body');",
|
|
1399
|
-
" var empty = document.getElementById('queue-empty');",
|
|
1400
|
-
" if (!d.jobs.length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }",
|
|
1401
|
-
" empty.classList.add('hidden');",
|
|
1402
|
-
" tbody.innerHTML = d.jobs.map(function(j) {",
|
|
1403
|
-
" return '<tr>' +",
|
|
1404
|
-
" '<td>' + j.id + '</td>' +",
|
|
1405
|
-
" '<td class=\"path\">' + j.topic + '</td>' +",
|
|
1406
|
-
" '<td><span class=\"badge-pill bg-' + j.status + '\">' + j.status + '</span></td>' +",
|
|
1407
|
-
" '<td>' + j.attempts + '</td>' +",
|
|
1408
|
-
" '<td class=\"text-sm text-muted\">' + (j.created_at || '') + '</td>' +",
|
|
1409
|
-
" '<td class=\"text-mono text-sm\" style=\"max-width:250px;overflow:hidden;text-overflow:ellipsis\">' + (typeof j.data === 'object' ? JSON.stringify(j.data) : j.data) + '</td>' +",
|
|
1410
|
-
" '<td><button class=\"btn btn-sm\" onclick=\"replayJob(\\'' + j.id + '\\',\\'' + j.topic + '\\')\">Replay</button></td>' +",
|
|
1411
|
-
" '</tr>';",
|
|
1412
|
-
" }).join('');",
|
|
1413
|
-
" });",
|
|
1414
|
-
"}",
|
|
1415
|
-
"function filterQueue(status, e) {",
|
|
1416
|
-
" queueFilter = status;",
|
|
1417
|
-
" document.querySelectorAll('#panel-queue .filter-btn').forEach(function(b) { b.classList.remove('active'); });",
|
|
1418
|
-
" if (e) e.target.classList.add('active');",
|
|
1419
|
-
" loadQueue();",
|
|
1420
|
-
"}",
|
|
1421
|
-
"function retryQueue() { api('/__dev/api/queue/retry', 'POST', {}).then(function() { loadQueue(); }); }",
|
|
1422
|
-
"function purgeQueue() { api('/__dev/api/queue/purge', 'POST', {}).then(function() { loadQueue(); }); }",
|
|
1423
|
-
"function replayJob(id, topic) { api('/__dev/api/queue/replay', 'POST', {job_id: id, topic: topic}).then(function() { loadQueue(); }); }",
|
|
1424
|
-
];
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
function renderMailboxTab(): string[] {
|
|
1428
|
-
return [
|
|
1429
|
-
"// -- Mailbox --",
|
|
1430
|
-
"function loadMailbox() {",
|
|
1431
|
-
" var qs = mailboxFolder ? '?folder=' + mailboxFolder : '';",
|
|
1432
|
-
" api('/__dev/api/mailbox' + qs).then(function(d) {",
|
|
1433
|
-
" document.getElementById('mailbox-count').textContent = d.unread;",
|
|
1434
|
-
" document.getElementById('mail-detail').classList.add('hidden');",
|
|
1435
|
-
" var list = document.getElementById('mailbox-list');",
|
|
1436
|
-
" if (!d.messages.length) { list.innerHTML = '<div class=\"empty\">No messages. Click \"Seed 5\" to generate test emails.</div>'; return; }",
|
|
1437
|
-
" list.innerHTML = d.messages.map(function(m) {",
|
|
1438
|
-
" return '<div class=\"mail-item ' + (m.read ? '' : 'unread') + '\" onclick=\"readMail(\\'' + m.id + '\\')\">'+",
|
|
1439
|
-
" '<span class=\"text-sm text-muted\" style=\"float:right\">' + (m.date||'').substring(0,16) + '</span>'+",
|
|
1440
|
-
" '<div class=\"text-sm text-muted\">' + m.from + ' → ' + (m.to||[]).join(', ') + '</div>'+",
|
|
1441
|
-
" '<div style=\"font-weight:600;font-size:0.8rem\">' + m.subject + '</div>'+",
|
|
1442
|
-
" '<span class=\"badge-pill bg-' + (m.type === 'inbox' ? 'success' : 'primary') + '\" style=\"margin-top:0.2rem\">' + m.type + '</span>'+",
|
|
1443
|
-
" '</div>';",
|
|
1444
|
-
" }).join('');",
|
|
1445
|
-
" });",
|
|
1446
|
-
"}",
|
|
1447
|
-
"function filterMailbox(folder, e) {",
|
|
1448
|
-
" mailboxFolder = folder;",
|
|
1449
|
-
" document.querySelectorAll('#panel-mailbox .filter-btn').forEach(function(b) { b.classList.remove('active'); });",
|
|
1450
|
-
" if (e) e.target.classList.add('active');",
|
|
1451
|
-
" loadMailbox();",
|
|
1452
|
-
"}",
|
|
1453
|
-
"function readMail(id) {",
|
|
1454
|
-
" api('/__dev/api/mailbox/read?id=' + id).then(function(m) {",
|
|
1455
|
-
" var det = document.getElementById('mail-detail');",
|
|
1456
|
-
" det.classList.remove('hidden');",
|
|
1457
|
-
" det.innerHTML = '<h3 style=\"font-size:0.9rem\">' + m.subject + '</h3>'+",
|
|
1458
|
-
" '<p class=\"text-sm text-muted\">From: ' + m.from + ' | To: ' + (m.to||[]).join(', ') + ' | ' + m.date + '</p>'+",
|
|
1459
|
-
" '<div style=\"background:var(--bg);padding:0.75rem;border-radius:var(--radius);margin-top:0.5rem;font-size:0.8rem\">' + (m.html ? m.body : '<pre>' + (m.body||'') + '</pre>') + '</div>';",
|
|
1460
|
-
" });",
|
|
1461
|
-
"}",
|
|
1462
|
-
"function seedMailbox() { api('/__dev/api/mailbox/seed', 'POST', {count:5}).then(function() { loadMailbox(); }); }",
|
|
1463
|
-
"function clearMailbox() { api('/__dev/api/mailbox/clear', 'POST', {}).then(function() { loadMailbox(); }); }",
|
|
1464
|
-
];
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
function renderMessagesTab(): string[] {
|
|
1468
|
-
return [
|
|
1469
|
-
"// -- Messages --",
|
|
1470
|
-
"function loadMessages() {",
|
|
1471
|
-
" api('/__dev/api/messages').then(function(d) {",
|
|
1472
|
-
" document.getElementById('messages-count').textContent = d.counts.total || 0;",
|
|
1473
|
-
" renderMessages(d.messages);",
|
|
1474
|
-
" });",
|
|
1475
|
-
"}",
|
|
1476
|
-
"function searchMessages() {",
|
|
1477
|
-
" var q = document.getElementById('msg-search').value.trim();",
|
|
1478
|
-
" if (!q) { loadMessages(); return; }",
|
|
1479
|
-
" api('/__dev/api/messages/search?q=' + encodeURIComponent(q)).then(function(d) { renderMessages(d.messages); });",
|
|
1480
|
-
"}",
|
|
1481
|
-
"function renderMessages(messages) {",
|
|
1482
|
-
" var list = document.getElementById('messages-list');",
|
|
1483
|
-
" var empty = document.getElementById('messages-empty');",
|
|
1484
|
-
" if (!messages.length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }",
|
|
1485
|
-
" empty.classList.add('hidden');",
|
|
1486
|
-
" list.innerHTML = messages.map(function(m) {",
|
|
1487
|
-
" return '<div class=\"msg-entry\">'+",
|
|
1488
|
-
" '<span class=\"time\">' + (m.timestamp||'').substring(11,19) + '</span> '+",
|
|
1489
|
-
" '<span class=\"cat\">' + m.category + '</span> '+",
|
|
1490
|
-
" '<span class=\"level-' + m.level + '\">[' + m.level + ']</span> '+",
|
|
1491
|
-
" esc(m.message) +",
|
|
1492
|
-
" (m.data ? ' <code class=\"text-sm text-muted\">' + JSON.stringify(m.data) + '</code>' : '') +",
|
|
1493
|
-
" '</div>';",
|
|
1494
|
-
" }).join('');",
|
|
1495
|
-
"}",
|
|
1496
|
-
"function clearMessages() { api('/__dev/api/messages/clear', 'POST', {}).then(function() { loadMessages(); }); }",
|
|
1497
|
-
];
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function renderDatabaseTab(): string[] {
|
|
1501
|
-
return [
|
|
1502
|
-
"// -- Database --",
|
|
1503
|
-
"var _currentTable = '';",
|
|
1504
|
-
"",
|
|
1505
|
-
"function _clipCopy(text, btn) {",
|
|
1506
|
-
" var orig = btn.textContent;",
|
|
1507
|
-
" var ta = document.createElement('textarea');",
|
|
1508
|
-
" ta.value = text;",
|
|
1509
|
-
" ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';",
|
|
1510
|
-
" document.body.appendChild(ta);",
|
|
1511
|
-
" ta.focus();",
|
|
1512
|
-
" ta.select();",
|
|
1513
|
-
" try { document.execCommand('copy'); btn.textContent = 'Copied!'; btn.style.color = 'var(--success)'; }",
|
|
1514
|
-
" catch(e) { btn.textContent = 'Failed'; btn.style.color = 'var(--danger)'; }",
|
|
1515
|
-
" document.body.removeChild(ta);",
|
|
1516
|
-
" setTimeout(function() { btn.textContent = orig; btn.style.color = ''; }, 1500);",
|
|
1517
|
-
"}",
|
|
1518
|
-
"",
|
|
1519
|
-
"function copyResults(fmt, btn) {",
|
|
1520
|
-
" var el = document.getElementById('query-results');",
|
|
1521
|
-
" var tbl = el ? el.querySelector('table') : null;",
|
|
1522
|
-
" if (!tbl) { btn.textContent = 'No data'; btn.style.color = 'var(--danger)'; setTimeout(function() { btn.textContent = fmt === 'json' ? 'Copy JSON' : 'Copy CSV'; btn.style.color = ''; }, 1500); return; }",
|
|
1523
|
-
" var headerCells = tbl.querySelectorAll('thead th');",
|
|
1524
|
-
" var headers = []; headerCells.forEach(function(h) { headers.push(h.textContent); });",
|
|
1525
|
-
" var bodyRows = tbl.querySelectorAll('tbody tr');",
|
|
1526
|
-
" var data = [];",
|
|
1527
|
-
" bodyRows.forEach(function(row) {",
|
|
1528
|
-
" var cells = row.querySelectorAll('td');",
|
|
1529
|
-
" var obj = {};",
|
|
1530
|
-
" cells.forEach(function(c, i) { obj[headers[i] || ('col' + i)] = c.textContent === 'null' ? null : c.textContent; });",
|
|
1531
|
-
" data.push(obj);",
|
|
1532
|
-
" });",
|
|
1533
|
-
" var text = '';",
|
|
1534
|
-
" if (fmt === 'json') {",
|
|
1535
|
-
" text = JSON.stringify(data, null, 2);",
|
|
1536
|
-
" } else {",
|
|
1537
|
-
" var lines = [headers.join(',')];",
|
|
1538
|
-
" data.forEach(function(r) {",
|
|
1539
|
-
" lines.push(headers.map(function(h) { var v = (r[h] !== null ? r[h] : ''); return v.indexOf(',') >= 0 || v.indexOf('\"') >= 0 ? '\"' + v.replace(/\"/g, '\"\"') + '\"' : v; }).join(','));",
|
|
1540
|
-
" });",
|
|
1541
|
-
" text = lines.join(String.fromCharCode(10));",
|
|
1542
|
-
" }",
|
|
1543
|
-
" _clipCopy(text, btn);",
|
|
1544
|
-
"}",
|
|
1545
|
-
"",
|
|
1546
|
-
"function pasteData() {",
|
|
1547
|
-
" var NL = String.fromCharCode(10);",
|
|
1548
|
-
" var TAB = String.fromCharCode(9);",
|
|
1549
|
-
" var overlay = document.createElement('div');",
|
|
1550
|
-
" overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:99999;display:flex;align-items:center;justify-content:center';",
|
|
1551
|
-
" var box = document.createElement('div');",
|
|
1552
|
-
" box.style.cssText = 'background:var(--bg,#1e293b);border:1px solid var(--border,#334155);border-radius:0.5rem;padding:1rem;width:500px;max-width:90vw';",
|
|
1553
|
-
" box.innerHTML = '<div style=\"font-weight:600;margin-bottom:0.5rem;color:var(--text,#e2e8f0)\">Paste Data</div><div style=\"font-size:0.75rem;color:var(--muted,#94a3b8);margin-bottom:0.5rem\">Paste JSON array or CSV/tab-separated data below</div>';",
|
|
1554
|
-
" var ta = document.createElement('textarea');",
|
|
1555
|
-
" ta.style.cssText = 'width:100%;height:150px;font-family:monospace;font-size:0.75rem;background:var(--bg-alt,#0f172a);color:var(--text,#e2e8f0);border:1px solid var(--border,#334155);border-radius:0.25rem;padding:0.5rem;resize:vertical';",
|
|
1556
|
-
" ta.placeholder = 'Paste here (Ctrl+V)...';",
|
|
1557
|
-
" var btns = document.createElement('div');",
|
|
1558
|
-
" btns.style.cssText = 'display:flex;gap:0.5rem;margin-top:0.5rem;justify-content:flex-end';",
|
|
1559
|
-
" var cancelBtn = document.createElement('button');",
|
|
1560
|
-
" cancelBtn.className = 'btn btn-sm'; cancelBtn.textContent = 'Cancel';",
|
|
1561
|
-
" cancelBtn.onclick = function() { document.body.removeChild(overlay); };",
|
|
1562
|
-
" var goBtn = document.createElement('button');",
|
|
1563
|
-
" goBtn.className = 'btn btn-sm btn-primary'; goBtn.textContent = 'Generate SQL';",
|
|
1564
|
-
" goBtn.onclick = function() {",
|
|
1565
|
-
" var text = ta.value.trim();",
|
|
1566
|
-
" if (!text) { alert('Paste some data first'); return; }",
|
|
1567
|
-
" var upper = text.substring(0, 50).toUpperCase();",
|
|
1568
|
-
" if (upper.indexOf('INSERT ') >= 0 || upper.indexOf('CREATE ') >= 0 || upper.indexOf('SELECT ') >= 0) {",
|
|
1569
|
-
" document.getElementById('query-input').value = text;",
|
|
1570
|
-
" document.body.removeChild(overlay);",
|
|
1571
|
-
" return;",
|
|
1572
|
-
" }",
|
|
1573
|
-
" var rows = [];",
|
|
1574
|
-
" var parsed = null;",
|
|
1575
|
-
" try { parsed = JSON.parse(text); } catch(e) {}",
|
|
1576
|
-
" if (parsed && Array.isArray(parsed) && parsed.length > 0) {",
|
|
1577
|
-
" rows = parsed;",
|
|
1578
|
-
" } else {",
|
|
1579
|
-
" var lines = text.split(NL);",
|
|
1580
|
-
" if (lines.length < 2) { alert('Need a header row + at least one data row'); return; }",
|
|
1581
|
-
" var sep = lines[0].indexOf(TAB) >= 0 ? TAB : ',';",
|
|
1582
|
-
" var hdrs = lines[0].split(sep);",
|
|
1583
|
-
" for (var i = 1; i < lines.length; i++) {",
|
|
1584
|
-
" var vals = lines[i].split(sep);",
|
|
1585
|
-
" if (!vals.length || vals.join('').trim() === '') continue;",
|
|
1586
|
-
" var row = {};",
|
|
1587
|
-
" hdrs.forEach(function(h, idx) { row[h.trim()] = vals[idx] !== undefined ? vals[idx].trim() : ''; });",
|
|
1588
|
-
" rows.push(row);",
|
|
1589
|
-
" }",
|
|
1590
|
-
" }",
|
|
1591
|
-
" if (!rows.length) { alert('No data rows found'); return; }",
|
|
1592
|
-
" var allCols = Object.keys(rows[0]);",
|
|
1593
|
-
" var table = _currentTable || '';",
|
|
1594
|
-
" var isNew = false;",
|
|
1595
|
-
" if (!table) {",
|
|
1596
|
-
" table = prompt('No table selected. Enter table name (creates if new):');",
|
|
1597
|
-
" if (!table) { return; }",
|
|
1598
|
-
" isNew = true;",
|
|
1599
|
-
" }",
|
|
1600
|
-
" var sql = '';",
|
|
1601
|
-
" if (isNew) {",
|
|
1602
|
-
" var hasId = allCols.some(function(c) { return c.toLowerCase() === 'id'; });",
|
|
1603
|
-
" var colDefs = allCols.map(function(h) {",
|
|
1604
|
-
" if (h.toLowerCase() === 'id') return h + ' INTEGER PRIMARY KEY AUTOINCREMENT';",
|
|
1605
|
-
" return h + ' TEXT';",
|
|
1606
|
-
" }).join(', ');",
|
|
1607
|
-
" if (!hasId) colDefs = 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + colDefs;",
|
|
1608
|
-
" sql = 'CREATE TABLE IF NOT EXISTS ' + table + ' (' + colDefs + ');' + NL;",
|
|
1609
|
-
" }",
|
|
1610
|
-
" sql += rows.map(function(r) {",
|
|
1611
|
-
" var keys = Object.keys(r);",
|
|
1612
|
-
" if (isNew) { keys = keys.filter(function(k) { return k.toLowerCase() !== 'id'; }); }",
|
|
1613
|
-
" var cols = keys.join(', ');",
|
|
1614
|
-
" var vs = keys.map(function(k) { var v = r[k]; return v === null ? 'NULL' : \"'\" + String(v).replace(/'/g, \"''\") + \"'\"; }).join(', ');",
|
|
1615
|
-
" return 'INSERT INTO ' + table + ' (' + cols + ') VALUES (' + vs + ')';",
|
|
1616
|
-
" }).join(';' + NL);",
|
|
1617
|
-
" document.getElementById('query-input').value = sql;",
|
|
1618
|
-
" document.body.removeChild(overlay);",
|
|
1619
|
-
" };",
|
|
1620
|
-
" btns.appendChild(cancelBtn); btns.appendChild(goBtn);",
|
|
1621
|
-
" box.appendChild(ta); box.appendChild(btns);",
|
|
1622
|
-
" overlay.appendChild(box); document.body.appendChild(overlay);",
|
|
1623
|
-
" ta.focus();",
|
|
1624
|
-
"}",
|
|
1625
|
-
"",
|
|
1626
|
-
"function loadTables() {",
|
|
1627
|
-
" api('/__dev/api/tables').then(function(d) {",
|
|
1628
|
-
" if (d.error) { document.getElementById('table-list').innerHTML = '<div style=\"color:var(--danger)\">' + d.error + '</div>'; return; }",
|
|
1629
|
-
" var tables = d.tables || [];",
|
|
1630
|
-
" document.getElementById('db-count').textContent = tables.length;",
|
|
1631
|
-
" var list = document.getElementById('table-list');",
|
|
1632
|
-
" if (!tables.length) { list.innerHTML = '<div class=\"text-muted\">No tables</div>'; return; }",
|
|
1633
|
-
" list.innerHTML = '';",
|
|
1634
|
-
" tables.forEach(function(t) {",
|
|
1635
|
-
" var div = document.createElement('div');",
|
|
1636
|
-
" div.style.cssText = 'cursor:pointer;padding:4px 6px;color:var(--primary);border-radius:4px;margin-bottom:1px';",
|
|
1637
|
-
" div.textContent = t;",
|
|
1638
|
-
" div.addEventListener('mouseenter', function() { if (t !== _currentTable) div.style.background = 'var(--bg-alt,rgba(255,255,255,0.05))'; });",
|
|
1639
|
-
" div.addEventListener('mouseleave', function() { if (t !== _currentTable) div.style.background = ''; });",
|
|
1640
|
-
" div.addEventListener('click', function() {",
|
|
1641
|
-
" list.querySelectorAll('div').forEach(function(d) { d.style.background = ''; d.style.fontWeight = ''; });",
|
|
1642
|
-
" div.style.background = 'var(--primary-bg,rgba(53,114,165,0.2))'; div.style.fontWeight = '600';",
|
|
1643
|
-
" browseTable(t);",
|
|
1644
|
-
" });",
|
|
1645
|
-
" list.appendChild(div);",
|
|
1646
|
-
" });",
|
|
1647
|
-
" var sel = document.getElementById('seed-table');",
|
|
1648
|
-
" sel.innerHTML = '<option value=\"\">Pick table...</option>' + tables.map(function(t) { return '<option value=\"' + t + '\">' + t + '</option>'; }).join('');",
|
|
1649
|
-
" }).catch(function(e) { document.getElementById('table-list').innerHTML = '<div style=\"color:var(--danger)\">' + e.message + '</div>'; });",
|
|
1650
|
-
"}",
|
|
1651
|
-
"",
|
|
1652
|
-
"function browseTable(name) {",
|
|
1653
|
-
" _currentTable = name;",
|
|
1654
|
-
" var lim = document.getElementById('query-limit').value;",
|
|
1655
|
-
" document.getElementById('query-input').value = 'SELECT * FROM ' + name + (lim !== '0' ? ' LIMIT ' + lim : '');",
|
|
1656
|
-
" runQuery();",
|
|
1657
|
-
"}",
|
|
1658
|
-
"",
|
|
1659
|
-
"function seedTable() {",
|
|
1660
|
-
" var table = document.getElementById('seed-table').value;",
|
|
1661
|
-
" var count = parseInt(document.getElementById('seed-count').value) || 10;",
|
|
1662
|
-
" if (!table) return;",
|
|
1663
|
-
" api('/__dev/api/seed', 'POST', {table:table, count:count}).then(function(d) {",
|
|
1664
|
-
" if (d.error) { alert(d.error); return; }",
|
|
1665
|
-
" browseTable(table);",
|
|
1666
|
-
" });",
|
|
1667
|
-
"}",
|
|
1668
|
-
"",
|
|
1669
|
-
"function runQuery() {",
|
|
1670
|
-
" var query = document.getElementById('query-input').value.trim();",
|
|
1671
|
-
" var type = document.getElementById('query-type').value;",
|
|
1672
|
-
" var errorEl = document.getElementById('query-error');",
|
|
1673
|
-
" var resultEl = document.getElementById('query-results');",
|
|
1674
|
-
" if (!query) { errorEl.textContent = 'Enter a query'; errorEl.classList.remove('hidden'); return; }",
|
|
1675
|
-
" errorEl.classList.add('hidden');",
|
|
1676
|
-
" resultEl.innerHTML = '<p class=\"text-muted\">Running...</p>';",
|
|
1677
|
-
" api('/__dev/api/query', 'POST', {query:query, type:type}).then(function(d) {",
|
|
1678
|
-
" if (d.error) { errorEl.textContent = d.error; errorEl.classList.remove('hidden'); resultEl.innerHTML = ''; return; }",
|
|
1679
|
-
" if (d.rows) {",
|
|
1680
|
-
" if (!d.rows.length) { resultEl.innerHTML = '<p class=\"text-muted\">No results</p>'; return; }",
|
|
1681
|
-
" var cols = d.columns || Object.keys(d.rows[0]);",
|
|
1682
|
-
" var html = '<div class=\"text-muted\" style=\"margin-bottom:0.25rem\">' + d.rows.length + ' row(s)</div><table class=\"table\"><thead><tr>' + cols.map(function(c) { return '<th>' + c + '</th>'; }).join('') + '</tr></thead><tbody>';",
|
|
1683
|
-
" d.rows.forEach(function(row) { html += '<tr>' + cols.map(function(c) { return '<td>' + (row[c] !== null ? row[c] : '<em>null</em>') + '</td>'; }).join('') + '</tr>'; });",
|
|
1684
|
-
" html += '</tbody></table>'; resultEl.innerHTML = html;",
|
|
1685
|
-
" } else if (d.data) {",
|
|
1686
|
-
" resultEl.innerHTML = '<pre class=\"p-md text-mono text-sm\">' + JSON.stringify(d.data, null, 2) + '</pre>';",
|
|
1687
|
-
" } else if (d.success) {",
|
|
1688
|
-
" resultEl.innerHTML = '<p style=\"color:var(--success)\">' + (d.affected || 0) + ' row(s) affected</p>';",
|
|
1689
|
-
" loadTables();",
|
|
1690
|
-
" } else {",
|
|
1691
|
-
" resultEl.innerHTML = '<div class=\"empty\">No results</div>';",
|
|
1692
|
-
" }",
|
|
1693
|
-
" }).catch(function(e) { errorEl.textContent = e.message; errorEl.classList.remove('hidden'); });",
|
|
1694
|
-
"}",
|
|
1695
|
-
];
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function renderRequestsTab(): string[] {
|
|
1699
|
-
return [
|
|
1700
|
-
"// -- Requests --",
|
|
1701
|
-
"function loadRequests() {",
|
|
1702
|
-
" api('/__dev/api/requests').then(function(d) {",
|
|
1703
|
-
" var stats = d.stats || {};",
|
|
1704
|
-
" document.getElementById('req-count').textContent = stats.total || 0;",
|
|
1705
|
-
" document.getElementById('req-stats').innerHTML = 'Total: ' + (stats.total||0) + ' | Avg: ' + (stats.avg_ms||0) + 'ms | Errors: ' + (stats.errors||0) + ' | Slowest: ' + (stats.slowest_ms||0) + 'ms';",
|
|
1706
|
-
" var tbody = document.getElementById('req-body');",
|
|
1707
|
-
" var empty = document.getElementById('req-empty');",
|
|
1708
|
-
" if (!(d.requests||[]).length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }",
|
|
1709
|
-
" empty.classList.add('hidden');",
|
|
1710
|
-
" tbody.innerHTML = d.requests.map(function(r) {",
|
|
1711
|
-
" var sc = r.status >= 500 ? 'status-err' : r.status >= 400 ? 'status-warn' : 'status-ok';",
|
|
1712
|
-
" return '<tr>'+",
|
|
1713
|
-
" '<td class=\"text-sm text-muted text-mono\">' + (r.timestamp||'').substring(11,19) + '</td>'+",
|
|
1714
|
-
" '<td><span class=\"method method-' + r.method.toLowerCase() + '\">' + r.method + '</span></td>'+",
|
|
1715
|
-
" '<td class=\"path\">' + r.path + '</td>'+",
|
|
1716
|
-
" '<td class=\"' + sc + '\" style=\"font-weight:600\">' + r.status + '</td>'+",
|
|
1717
|
-
" '<td class=\"text-mono text-sm\">' + r.duration_ms + 'ms</td>'+",
|
|
1718
|
-
" '<td class=\"text-sm text-muted\">' + (r.body_size ? r.body_size + 'B' : '') + '</td>'+",
|
|
1719
|
-
" '</tr>';",
|
|
1720
|
-
" }).join('');",
|
|
1721
|
-
" });",
|
|
1722
|
-
"}",
|
|
1723
|
-
"function clearRequests() { api('/__dev/api/requests/clear', 'POST', {}).then(function() { loadRequests(); }); }",
|
|
1724
|
-
];
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function renderErrorsTab(): string[] {
|
|
1728
|
-
return [
|
|
1729
|
-
"// -- Errors --",
|
|
1730
|
-
"function loadErrors() {",
|
|
1731
|
-
" api('/__dev/api/broken').then(function(d) {",
|
|
1732
|
-
" var health = d.health || {};",
|
|
1733
|
-
" document.getElementById('err-count').textContent = health.unresolved || 0;",
|
|
1734
|
-
" var list = document.getElementById('errors-list');",
|
|
1735
|
-
" var empty = document.getElementById('errors-empty');",
|
|
1736
|
-
" if (!(d.errors||[]).length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }",
|
|
1737
|
-
" empty.classList.add('hidden');",
|
|
1738
|
-
" list.innerHTML = d.errors.map(function(e) {",
|
|
1739
|
-
" return '<div style=\"padding:0.6rem 0.75rem;border-bottom:1px solid var(--border)\">'+",
|
|
1740
|
-
" '<div class=\"flex justify-between items-center\">'+",
|
|
1741
|
-
" '<span class=\"badge-pill ' + (e.resolved ? 'bg-success' : 'bg-danger') + '\">' + (e.resolved ? 'resolved' : 'unresolved') + '</span>'+",
|
|
1742
|
-
" '<span class=\"text-sm text-muted\">x' + e.count + ' | ' + (e.last_seen||'').substring(0,19) + '</span>'+",
|
|
1743
|
-
" '</div>'+",
|
|
1744
|
-
" '<div style=\"font-weight:600;font-size:0.8rem;margin-top:0.25rem\">' + esc(e.error_type) + ': ' + esc(e.message) + '</div>'+",
|
|
1745
|
-
" (e.traceback ? '<pre class=\"text-sm text-muted\" style=\"margin-top:0.25rem;max-height:100px;overflow:auto\">' + esc(e.traceback) + '</pre>' : '') +",
|
|
1746
|
-
" (!e.resolved ? '<button class=\"btn btn-sm btn-success\" style=\"margin-top:0.25rem\" onclick=\"resolveError(\\'' + e.id + '\\')\">Resolve</button>' : '') +",
|
|
1747
|
-
" '<button class=\"btn btn-sm btn-primary\" style=\"margin-top:0.25rem;margin-left:0.25rem\" data-err=\"' + btoa(e.error_type + ': ' + e.message) + '\" data-tb=\"' + btoa((e.traceback||'').substring(0,500)) + '\" onclick=\"askAboutError(this)\">Ask Tina4</button>'+",
|
|
1748
|
-
" '</div>';",
|
|
1749
|
-
" }).join('');",
|
|
1750
|
-
" });",
|
|
1751
|
-
"}",
|
|
1752
|
-
"function resolveError(id) { api('/__dev/api/broken/resolve', 'POST', {id:id}).then(function() { loadErrors(); }); }",
|
|
1753
|
-
"function clearResolvedErrors() { api('/__dev/api/broken/clear', 'POST', {}).then(function() { loadErrors(); }); }",
|
|
1754
|
-
];
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
function renderWebSocketTab(): string[] {
|
|
1758
|
-
return [
|
|
1759
|
-
"// -- WebSockets --",
|
|
1760
|
-
"function loadWebSockets() {",
|
|
1761
|
-
" api('/__dev/api/websockets').then(function(d) {",
|
|
1762
|
-
" document.getElementById('ws-count').textContent = d.count || 0;",
|
|
1763
|
-
" var tbody = document.getElementById('ws-body');",
|
|
1764
|
-
" var empty = document.getElementById('ws-empty');",
|
|
1765
|
-
" if (!(d.connections||[]).length) { tbody.innerHTML = ''; empty.classList.remove('hidden'); return; }",
|
|
1766
|
-
" empty.classList.add('hidden');",
|
|
1767
|
-
" tbody.innerHTML = d.connections.map(function(c) {",
|
|
1768
|
-
" return '<tr>'+",
|
|
1769
|
-
" '<td class=\"text-mono text-sm\">' + c.id + '</td>'+",
|
|
1770
|
-
" '<td class=\"path\">' + c.path + '</td>'+",
|
|
1771
|
-
" '<td class=\"text-sm text-muted\">' + c.ip + '</td>'+",
|
|
1772
|
-
" '<td class=\"text-sm text-muted\">' + (c.connected_at||'').substring(11,19) + '</td>'+",
|
|
1773
|
-
" '<td><span class=\"badge-pill ' + (c.closed ? 'bg-danger' : 'bg-success') + '\">' + (c.closed ? 'closed' : 'active') + '</span></td>'+",
|
|
1774
|
-
" '<td>' + (!c.closed ? '<button class=\"btn btn-sm btn-danger\" onclick=\"wsDisconnect(\\'' + c.id + '\\')\">Disconnect</button>' : '') + '</td>'+",
|
|
1775
|
-
" '</tr>';",
|
|
1776
|
-
" }).join('');",
|
|
1777
|
-
" });",
|
|
1778
|
-
"}",
|
|
1779
|
-
"function wsDisconnect(id) { api('/__dev/api/websockets/disconnect', 'POST', {id:id}).then(function() { loadWebSockets(); }); }",
|
|
1780
|
-
];
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
function renderSystemTab(): string[] {
|
|
1784
|
-
return [
|
|
1785
|
-
"// -- System --",
|
|
1786
|
-
"function loadSystem() {",
|
|
1787
|
-
" api('/__dev/api/system').then(function(d) {",
|
|
1788
|
-
" var nodeVersion = d.node_version || (d.node ? d.node.version : '') || 'N/A';",
|
|
1789
|
-
" var platform = d.os || d.platform || (d.node ? d.node.platform : '') || '';",
|
|
1790
|
-
" var arch = d.architecture || (d.node ? d.node.arch : '') || '';",
|
|
1791
|
-
" var memCurrent = d.memory ? (d.memory.current_mb ? d.memory.current_mb + ' MB' : d.memory.heapUsed || 'N/A') : 'N/A';",
|
|
1792
|
-
" var memPeak = d.memory ? (d.memory.peak_mb ? d.memory.peak_mb + ' MB' : d.memory.rss || 'N/A') : 'N/A';",
|
|
1793
|
-
" var memLimit = d.memory ? (d.memory.limit || 'N/A') : 'N/A';",
|
|
1794
|
-
" var fwName = d.framework ? (typeof d.framework === 'object' ? d.framework.name : d.framework) : '';",
|
|
1795
|
-
" var fwVersion = d.framework ? (typeof d.framework === 'object' ? d.framework.version : '') : '';",
|
|
1796
|
-
" var routeCount = d.framework ? (typeof d.framework === 'object' ? d.framework.route_count : '') : (d.route_count || '');",
|
|
1797
|
-
"",
|
|
1798
|
-
" var html = '<div class=\"sys-card\"><div class=\"label\">Node.js</div><div class=\"value text-sm\">' + nodeVersion + '</div></div>' +",
|
|
1799
|
-
" '<div class=\"sys-card\"><div class=\"label\">Platform</div><div class=\"value text-sm\">' + platform + '</div></div>' +",
|
|
1800
|
-
" '<div class=\"sys-card\"><div class=\"label\">Architecture</div><div class=\"value text-sm\">' + arch + '</div></div>' +",
|
|
1801
|
-
" '<div class=\"sys-card\"><div class=\"label\">Memory (Current)</div><div class=\"value\">' + memCurrent + '</div></div>' +",
|
|
1802
|
-
" '<div class=\"sys-card\"><div class=\"label\">Memory (Peak)</div><div class=\"value\">' + memPeak + '</div></div>' +",
|
|
1803
|
-
" '<div class=\"sys-card\"><div class=\"label\">Memory Limit</div><div class=\"value text-sm\">' + memLimit + '</div></div>';",
|
|
1804
|
-
"",
|
|
1805
|
-
" if (fwName) html += '<div class=\"sys-card\"><div class=\"label\">Framework</div><div class=\"value text-sm\">' + fwName + '</div></div>';",
|
|
1806
|
-
" if (fwVersion) html += '<div class=\"sys-card\"><div class=\"label\">Version</div><div class=\"value text-sm\">' + fwVersion + '</div></div>';",
|
|
1807
|
-
" if (routeCount !== '') html += '<div class=\"sys-card\"><div class=\"label\">Routes</div><div class=\"value\">' + routeCount + '</div></div>';",
|
|
1808
|
-
"",
|
|
1809
|
-
" html += '<div class=\"sys-card\"><div class=\"label\">DB Tables</div><div class=\"value\">' + (d.db_tables !== undefined ? d.db_tables : 'N/A') + '</div></div>';",
|
|
1810
|
-
" html += '<div class=\"sys-card\"><div class=\"label\">DB Connected</div><div class=\"value\">' + (d.db_connected ? '<span style=\"color:var(--success)\">Yes</span>' : '<span style=\"color:var(--danger)\">No</span>') + '</div></div>';",
|
|
1811
|
-
"",
|
|
1812
|
-
" if (d.node && d.node.v8) html += '<div class=\"sys-card\"><div class=\"label\">V8 Engine</div><div class=\"value text-sm\">' + d.node.v8 + '</div></div>';",
|
|
1813
|
-
" if (d.pid) html += '<div class=\"sys-card\"><div class=\"label\">PID</div><div class=\"value text-sm\">' + d.pid + '</div></div>';",
|
|
1814
|
-
" if (d.cpus) html += '<div class=\"sys-card\"><div class=\"label\">CPU Cores</div><div class=\"value\">' + d.cpus + '</div></div>';",
|
|
1815
|
-
" if (d.uptime) html += '<div class=\"sys-card\"><div class=\"label\">Uptime</div><div class=\"value text-sm\">' + (d.uptime.formatted || d.uptime.seconds + 's') + '</div></div>';",
|
|
1816
|
-
" if (d.debug_level) html += '<div class=\"sys-card\"><div class=\"label\">Debug Level</div><div class=\"value text-sm\">' + d.debug_level + '</div></div>';",
|
|
1817
|
-
" if (d.memory && d.memory.heapTotal) html += '<div class=\"sys-card\"><div class=\"label\">Heap Total</div><div class=\"value text-sm\">' + d.memory.heapTotal + '</div></div>';",
|
|
1818
|
-
" if (d.memory && d.memory.external) html += '<div class=\"sys-card\"><div class=\"label\">External</div><div class=\"value text-sm\">' + d.memory.external + '</div></div>';",
|
|
1819
|
-
" if (d.env) html += '<div class=\"sys-card\"><div class=\"label\">TINA4_DEBUG</div><div class=\"value text-sm\">' + (d.env.TINA4_DEBUG || 'false') + '</div></div>';",
|
|
1820
|
-
"",
|
|
1821
|
-
" document.getElementById('sys-cards').innerHTML = html;",
|
|
1822
|
-
" });",
|
|
1823
|
-
"}",
|
|
1824
|
-
];
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
function renderChatTab(): string[] {
|
|
1828
|
-
return [
|
|
1829
|
-
"// -- Chat (Tina4) --",
|
|
1830
|
-
"var _aiKey = '';",
|
|
1831
|
-
"var _aiProvider = 'anthropic';",
|
|
1832
|
-
"function setAiKey() {",
|
|
1833
|
-
" _aiKey = document.getElementById('ai-key').value.trim();",
|
|
1834
|
-
" _aiProvider = document.getElementById('ai-provider').value;",
|
|
1835
|
-
" document.getElementById('ai-key').value = '';",
|
|
1836
|
-
" document.getElementById('ai-status').textContent = _aiKey ? (_aiProvider === 'anthropic' ? 'Claude key set' : 'OpenAI key set') : 'No key set';",
|
|
1837
|
-
" document.getElementById('ai-status').style.color = _aiKey ? 'var(--success)' : 'var(--muted)';",
|
|
1838
|
-
"}",
|
|
1839
|
-
"function sendChat() {",
|
|
1840
|
-
" var input = document.getElementById('chat-input');",
|
|
1841
|
-
" var msg = input.value.trim();",
|
|
1842
|
-
" if (!msg) return;",
|
|
1843
|
-
" input.value = '';",
|
|
1844
|
-
" var container = document.getElementById('chat-messages');",
|
|
1845
|
-
" container.innerHTML += '<div class=\"chat-msg chat-user\">' + esc(msg) + '</div>';",
|
|
1846
|
-
" container.innerHTML += '<div class=\"chat-msg chat-bot\" id=\"chat-loading\" style=\"color:var(--muted)\">Thinking...</div>';",
|
|
1847
|
-
" container.scrollTop = container.scrollHeight;",
|
|
1848
|
-
" var body = {message: msg, provider: _aiProvider};",
|
|
1849
|
-
" if (_aiKey) body.api_key = _aiKey;",
|
|
1850
|
-
" api('/__dev/api/chat', 'POST', body).then(function(d) {",
|
|
1851
|
-
" var loading = document.getElementById('chat-loading');",
|
|
1852
|
-
" if (loading) loading.remove();",
|
|
1853
|
-
" container.innerHTML += '<div class=\"chat-msg chat-bot\">' + formatChat(d.reply||'No response') + '</div>';",
|
|
1854
|
-
" container.scrollTop = container.scrollHeight;",
|
|
1855
|
-
" }).catch(function() {",
|
|
1856
|
-
" var loading = document.getElementById('chat-loading');",
|
|
1857
|
-
" if (loading) { loading.textContent = 'Error connecting to API'; loading.id = ''; }",
|
|
1858
|
-
" });",
|
|
1859
|
-
"}",
|
|
1860
|
-
"function formatChat(text) {",
|
|
1861
|
-
" return text.replace(/`([^`]+)`/g, '<code style=\"background:var(--surface);padding:0.1rem 0.25rem;border-radius:0.2rem;font-size:0.8em\">$1</code>').replace(/\\n/g, '<br>');",
|
|
1862
|
-
"}",
|
|
1863
|
-
"",
|
|
1864
|
-
"// -- Ask Tina4 about errors --",
|
|
1865
|
-
"function askAboutError(btn) {",
|
|
1866
|
-
" var error = atob(btn.dataset.err);",
|
|
1867
|
-
" var trace = atob(btn.dataset.tb);",
|
|
1868
|
-
" currentTab = 'chat';",
|
|
1869
|
-
" document.querySelectorAll('.dev-tab').forEach(function(t) { t.classList.remove('active'); });",
|
|
1870
|
-
" document.querySelectorAll('.dev-panel').forEach(function(p) { p.classList.add('hidden'); });",
|
|
1871
|
-
" document.querySelectorAll('.dev-tab').forEach(function(t) { if(t.textContent.includes('Tina4')) t.classList.add('active'); });",
|
|
1872
|
-
" document.getElementById('panel-chat').classList.remove('hidden');",
|
|
1873
|
-
" var msg = 'I have this error in my Tina4 app, help me fix it:\\n\\n' + error + '\\n\\nStack trace:\\n' + trace;",
|
|
1874
|
-
" document.getElementById('chat-input').value = msg;",
|
|
1875
|
-
" sendChat();",
|
|
1876
|
-
"}",
|
|
1877
|
-
];
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
function renderToolsTab(): string[] {
|
|
1881
|
-
return [
|
|
1882
|
-
"// -- Tools --",
|
|
1883
|
-
"function runTool(tool) {",
|
|
1884
|
-
" var titles = {carbon:'Carbon Benchmark',test:'Test Suite',routes:'Routes',migrate:'Migrations',seed:'Seeders',ai:'AI Detection'};",
|
|
1885
|
-
" document.getElementById('tool-title').textContent = titles[tool] || tool;",
|
|
1886
|
-
" document.getElementById('tool-result').textContent = 'Running...';",
|
|
1887
|
-
" document.getElementById('tool-output').classList.remove('hidden');",
|
|
1888
|
-
" api('/__dev/api/tool', 'POST', {tool:tool}).then(function(d) {",
|
|
1889
|
-
" document.getElementById('tool-result').textContent = d.output || d.error || JSON.stringify(d, null, 2);",
|
|
1890
|
-
" }).catch(function(e) {",
|
|
1891
|
-
" document.getElementById('tool-result').textContent = 'Error: ' + e.message;",
|
|
1892
|
-
" });",
|
|
1893
|
-
"}",
|
|
1894
|
-
];
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function renderUtilitiesAndInit(): string[] {
|
|
1898
|
-
return [
|
|
1899
|
-
"// -- Exit Dev Admin --",
|
|
1900
|
-
"function exitDevAdmin() {",
|
|
1901
|
-
" if (document.referrer && !document.referrer.includes('/__dev')) { window.location.href = document.referrer; }",
|
|
1902
|
-
" else if (window.history.length > 1) { window.history.back(); }",
|
|
1903
|
-
" else { window.location.href = '/'; }",
|
|
1904
|
-
"}",
|
|
1905
|
-
"",
|
|
1906
|
-
"// -- Utilities --",
|
|
1907
|
-
"function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }",
|
|
1908
|
-
"",
|
|
1909
|
-
"document.addEventListener('keydown', function(e) {",
|
|
1910
|
-
" if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && currentTab === 'database') { e.preventDefault(); runQuery(); }",
|
|
1911
|
-
"});",
|
|
1912
|
-
"",
|
|
1913
|
-
"// Init",
|
|
1914
|
-
"function updateTimestamp() { document.getElementById('timestamp').textContent = new Date().toLocaleTimeString(); }",
|
|
1915
|
-
"setInterval(updateTimestamp, 1000);",
|
|
1916
|
-
"updateTimestamp();",
|
|
1917
|
-
"loadRoutes();",
|
|
1918
|
-
"",
|
|
1919
|
-
"api('/__dev/api/status').then(function(d) {",
|
|
1920
|
-
" if (d.mailbox) document.getElementById('mailbox-count').textContent = d.mailbox.total || 0;",
|
|
1921
|
-
" if (d.messages) document.getElementById('messages-count').textContent = d.messages.total || 0;",
|
|
1922
|
-
" if (d.message_counts) document.getElementById('messages-count').textContent = d.message_counts.total || 0;",
|
|
1923
|
-
" if (d.health) document.getElementById('err-count').textContent = d.health.unresolved || 0;",
|
|
1924
|
-
" if (d.requests) document.getElementById('req-count').textContent = d.requests.total || 0;",
|
|
1925
|
-
" if (d.request_stats) document.getElementById('req-count').textContent = d.request_stats.total || 0;",
|
|
1926
|
-
" if (d.db_tables !== undefined) document.getElementById('db-count').textContent = d.db_tables;",
|
|
1927
|
-
"});",
|
|
1928
|
-
];
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
function renderDevAdminJs(): string {
|
|
1932
|
-
return [
|
|
1933
|
-
...renderAppShell(),
|
|
1934
|
-
"",
|
|
1935
|
-
...renderRoutesTab(),
|
|
1936
|
-
"",
|
|
1937
|
-
...renderQueueTab(),
|
|
1938
|
-
"",
|
|
1939
|
-
...renderMailboxTab(),
|
|
1940
|
-
"",
|
|
1941
|
-
...renderMessagesTab(),
|
|
1942
|
-
"",
|
|
1943
|
-
...renderDatabaseTab(),
|
|
1944
|
-
"",
|
|
1945
|
-
...renderRequestsTab(),
|
|
1946
|
-
"",
|
|
1947
|
-
...renderErrorsTab(),
|
|
1948
|
-
"",
|
|
1949
|
-
...renderWebSocketTab(),
|
|
1950
|
-
"",
|
|
1951
|
-
...renderSystemTab(),
|
|
1952
|
-
"",
|
|
1953
|
-
...renderChatTab(),
|
|
1954
|
-
"",
|
|
1955
|
-
...renderToolsTab(),
|
|
1956
|
-
"",
|
|
1957
|
-
...renderUtilitiesAndInit(),
|
|
1958
|
-
].join("\n");
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
// ---------------------------------------------------------------------------
|
|
1962
|
-
// Dashboard HTML — Single-page app
|
|
1963
|
-
// ---------------------------------------------------------------------------
|
|
1964
|
-
|
|
1965
|
-
export function renderDashboard(): string {
|
|
1966
|
-
return `<!DOCTYPE html>
|
|
1967
|
-
<html lang="en">
|
|
1968
|
-
<head>
|
|
1969
|
-
<meta charset="UTF-8">
|
|
1970
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1971
|
-
<title>Tina4 Dev Admin</title>
|
|
1972
|
-
<style>
|
|
1973
|
-
:root {
|
|
1974
|
-
--bg: #0f172a; --surface: #1e293b; --border: #334155;
|
|
1975
|
-
--text: #e2e8f0; --muted: #94a3b8; --primary: #2e7d32;
|
|
1976
|
-
--success: #22c55e; --danger: #ef4444; --warn: #f59e0b;
|
|
1977
|
-
--info: #06b6d4; --radius: 0.5rem;
|
|
1978
|
-
--mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
1979
|
-
--font: system-ui, -apple-system, sans-serif;
|
|
1980
|
-
}
|
|
1981
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1982
|
-
|
|
1983
|
-
body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 0.875rem; }
|
|
1984
|
-
.dev-header {
|
|
1985
|
-
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
1986
|
-
padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 1rem;
|
|
1987
|
-
position: sticky; top: 0; z-index: 100;
|
|
1988
|
-
}
|
|
1989
|
-
.dev-header h1 { font-size: 1rem; font-weight: 600; }
|
|
1990
|
-
.dev-header .badge {
|
|
1991
|
-
background: var(--primary); color: #fff; padding: 0.15rem 0.5rem;
|
|
1992
|
-
border-radius: 1rem; font-size: 0.7rem; font-weight: 600;
|
|
1993
|
-
}
|
|
1994
|
-
.dev-tabs {
|
|
1995
|
-
display: flex; gap: 0; background: var(--surface);
|
|
1996
|
-
border-bottom: 1px solid var(--border); overflow-x: auto;
|
|
1997
|
-
position: sticky; top: 2.75rem; z-index: 100;
|
|
1998
|
-
}
|
|
1999
|
-
.dev-tab {
|
|
2000
|
-
padding: 0.6rem 1rem; cursor: pointer; font-size: 0.8rem;
|
|
2001
|
-
border-bottom: 2px solid transparent; color: var(--muted);
|
|
2002
|
-
transition: all 0.15s; background: none; border-top: none;
|
|
2003
|
-
border-left: none; border-right: none; white-space: nowrap;
|
|
2004
|
-
}
|
|
2005
|
-
.dev-tab:hover { color: var(--text); }
|
|
2006
|
-
.dev-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
2007
|
-
.dev-tab .count {
|
|
2008
|
-
background: var(--border); color: var(--muted); padding: 0.1rem 0.4rem;
|
|
2009
|
-
border-radius: 0.75rem; font-size: 0.65rem; margin-left: 0.25rem;
|
|
2010
|
-
}
|
|
2011
|
-
.dev-content { padding: 0.25rem; }
|
|
2012
|
-
.dev-panel {
|
|
2013
|
-
background: var(--surface); border: 1px solid var(--border);
|
|
2014
|
-
border-radius: var(--radius); overflow: visible;
|
|
2015
|
-
}
|
|
2016
|
-
.dev-panel-header {
|
|
2017
|
-
padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
|
|
2018
|
-
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;
|
|
2019
|
-
}
|
|
2020
|
-
.dev-panel-header h2 { font-size: 0.9rem; font-weight: 600; }
|
|
2021
|
-
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
2022
|
-
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--muted); font-weight: 500; border-bottom: 1px solid var(--border); }
|
|
2023
|
-
td { padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); }
|
|
2024
|
-
tr:hover { background: rgba(46, 125, 50, 0.05); }
|
|
2025
|
-
.method { font-family: var(--mono); font-size: 0.7rem; font-weight: 700; }
|
|
2026
|
-
.method-get { color: var(--success); }
|
|
2027
|
-
.method-post { color: var(--primary); }
|
|
2028
|
-
.method-put { color: var(--warn); }
|
|
2029
|
-
.method-delete { color: var(--danger); }
|
|
2030
|
-
.path { font-family: var(--mono); font-size: 0.75rem; }
|
|
2031
|
-
.badge-pill {
|
|
2032
|
-
display: inline-block; padding: 0.1rem 0.5rem; border-radius: 1rem;
|
|
2033
|
-
font-size: 0.65rem; font-weight: 600; text-transform: uppercase;
|
|
2034
|
-
}
|
|
2035
|
-
.bg-pending { background: rgba(245,158,11,0.15); color: var(--warn); }
|
|
2036
|
-
.bg-completed, .bg-success { background: rgba(34,197,94,0.15); color: var(--success); }
|
|
2037
|
-
.bg-failed, .bg-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
|
|
2038
|
-
.bg-reserved, .bg-primary { background: rgba(46,125,50,0.15); color: var(--primary); }
|
|
2039
|
-
.bg-info { background: rgba(6,182,212,0.15); color: var(--info); }
|
|
2040
|
-
.btn {
|
|
2041
|
-
padding: 0.3rem 0.65rem; border: 1px solid var(--border); border-radius: var(--radius);
|
|
2042
|
-
background: var(--surface); color: var(--text); cursor: pointer; font-size: 0.75rem;
|
|
2043
|
-
transition: all 0.15s;
|
|
2044
|
-
}
|
|
2045
|
-
.btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
2046
|
-
.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
2047
|
-
.btn-primary:hover { background: #388e3c; }
|
|
2048
|
-
.btn-danger { border-color: var(--danger); color: var(--danger); }
|
|
2049
|
-
.btn-danger:hover { background: rgba(239,68,68,0.1); }
|
|
2050
|
-
.btn-success { border-color: var(--success); color: var(--success); }
|
|
2051
|
-
.btn-sm { padding: 0.2rem 0.5rem; font-size: 0.7rem; }
|
|
2052
|
-
.empty { padding: 2rem; text-align: center; color: var(--muted); }
|
|
2053
|
-
.input {
|
|
2054
|
-
background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
|
2055
|
-
border-radius: var(--radius); padding: 0.35rem 0.5rem; font-size: 0.8rem;
|
|
2056
|
-
font-family: var(--font);
|
|
2057
|
-
}
|
|
2058
|
-
.input:focus { outline: none; border-color: var(--primary); }
|
|
2059
|
-
.input-mono { font-family: var(--mono); }
|
|
2060
|
-
select.input { padding: 0.3rem; }
|
|
2061
|
-
textarea.input { resize: vertical; font-family: var(--mono); }
|
|
2062
|
-
.flex { display: flex; }
|
|
2063
|
-
.gap-sm { gap: 0.5rem; }
|
|
2064
|
-
.gap-md { gap: 1rem; }
|
|
2065
|
-
.items-center { align-items: center; }
|
|
2066
|
-
.justify-between { justify-content: space-between; }
|
|
2067
|
-
.flex-1 { flex: 1; }
|
|
2068
|
-
.p-sm { padding: 0.5rem; }
|
|
2069
|
-
.p-md { padding: 1rem; }
|
|
2070
|
-
.mb-sm { margin-bottom: 0.5rem; }
|
|
2071
|
-
.text-sm { font-size: 0.75rem; }
|
|
2072
|
-
.text-muted { color: var(--muted); }
|
|
2073
|
-
.text-mono { font-family: var(--mono); }
|
|
2074
|
-
.mail-item { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
2075
|
-
.mail-item:hover { background: rgba(46,125,50,0.05); }
|
|
2076
|
-
.mail-item.unread { border-left: 3px solid var(--primary); }
|
|
2077
|
-
.msg-entry { padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.75rem; }
|
|
2078
|
-
.msg-entry .cat {
|
|
2079
|
-
font-family: var(--mono); font-size: 0.65rem; padding: 0.1rem 0.35rem;
|
|
2080
|
-
border-radius: 0.25rem; background: rgba(46,125,50,0.15); color: var(--primary);
|
|
2081
|
-
}
|
|
2082
|
-
.msg-entry .time { color: var(--muted); font-size: 0.7rem; font-family: var(--mono); }
|
|
2083
|
-
.level-error { color: var(--danger); }
|
|
2084
|
-
.level-warn { color: var(--warn); }
|
|
2085
|
-
.toolbar { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; }
|
|
2086
|
-
.hidden { display: none; }
|
|
2087
|
-
/* Chat panel */
|
|
2088
|
-
.chat-container { display: flex; flex-direction: column; height: 500px; }
|
|
2089
|
-
.chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; }
|
|
2090
|
-
.chat-msg { margin-bottom: 0.75rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.8rem; max-width: 85%; }
|
|
2091
|
-
.chat-user { background: var(--primary); color: #fff; margin-left: auto; }
|
|
2092
|
-
.chat-bot { background: var(--bg); border: 1px solid var(--border); }
|
|
2093
|
-
.chat-input-row { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--border); }
|
|
2094
|
-
.chat-input-row input { flex: 1; }
|
|
2095
|
-
/* System cards */
|
|
2096
|
-
.sys-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem; padding: 1rem; }
|
|
2097
|
-
.sys-card { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem; }
|
|
2098
|
-
.sys-card .label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
2099
|
-
.sys-card .value { font-size: 1.25rem; font-weight: 600; margin-top: 0.25rem; }
|
|
2100
|
-
/* Request table */
|
|
2101
|
-
.status-ok { color: var(--success); }
|
|
2102
|
-
.status-err { color: var(--danger); }
|
|
2103
|
-
.status-warn { color: var(--warn); }
|
|
2104
|
-
/* Extension tags */
|
|
2105
|
-
.ext-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
2106
|
-
.ext-tag { background: rgba(46, 125, 50, 0.15); color: #81c784; padding: 3px 10px; border-radius: 12px; font-size: 0.78em; }
|
|
2107
|
-
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; margin-bottom: 0.75rem; }
|
|
2108
|
-
.card h3 { color: #81c784; margin-bottom: 0.75rem; font-size: 0.95rem; }
|
|
2109
|
-
.sys-item { display: flex; justify-content: space-between; padding: 0.4rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
|
2110
|
-
.sys-label { color: var(--muted); font-size: 0.82rem; }
|
|
2111
|
-
.sys-value { font-weight: 500; font-size: 0.82rem; }
|
|
2112
|
-
code, .mono { font-family: var(--mono); font-size: 0.82rem; }
|
|
2113
|
-
</style>
|
|
2114
|
-
</head>
|
|
2115
|
-
<body>
|
|
2116
|
-
|
|
2117
|
-
<div class="dev-header">
|
|
2118
|
-
<img src="/images/logo.svg" style="width:1.5rem;height:1.5rem;cursor:pointer;opacity:0.7;transition:opacity 0.15s" title="Back to app" onclick="exitDevAdmin()" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'" alt="Tina4">
|
|
2119
|
-
<h1>Tina4 Dev Admin</h1>
|
|
2120
|
-
<span class="badge">DEV</span>
|
|
2121
|
-
<span style="margin-left:auto; font-size:0.75rem; color:var(--muted)" id="timestamp"></span>
|
|
2122
|
-
</div>
|
|
2123
|
-
|
|
2124
|
-
<div class="dev-tabs">
|
|
2125
|
-
<button class="dev-tab active" onclick="showTab('routes', event)">Routes <span class="count" id="routes-count">0</span></button>
|
|
2126
|
-
<button class="dev-tab" onclick="showTab('queue', event)">Queue <span class="count" id="queue-count">0</span></button>
|
|
2127
|
-
<button class="dev-tab" onclick="showTab('mailbox', event)">Mailbox <span class="count" id="mailbox-count">0</span></button>
|
|
2128
|
-
<button class="dev-tab" onclick="showTab('messages', event)">Messages <span class="count" id="messages-count">0</span></button>
|
|
2129
|
-
<button class="dev-tab" onclick="showTab('database', event)">Database <span class="count" id="db-count">0</span></button>
|
|
2130
|
-
<button class="dev-tab" onclick="showTab('requests', event)">Requests <span class="count" id="req-count">0</span></button>
|
|
2131
|
-
<button class="dev-tab" onclick="showTab('errors', event)">Errors <span class="count" id="err-count">0</span></button>
|
|
2132
|
-
<button class="dev-tab" onclick="showTab('websockets', event)">WS <span class="count" id="ws-count">0</span></button>
|
|
2133
|
-
<button class="dev-tab" onclick="showTab('system', event)">System</button>
|
|
2134
|
-
<button class="dev-tab" onclick="showTab('tools', event)">Tools</button>
|
|
2135
|
-
<button class="dev-tab" onclick="showTab('connections', event)">Connections</button>
|
|
2136
|
-
<button class="dev-tab" onclick="showTab('metrics', event)">Metrics</button>
|
|
2137
|
-
<button class="dev-tab" onclick="showTab('chat', event)">Tina4</button>
|
|
2138
|
-
</div>
|
|
2139
|
-
|
|
2140
|
-
<div class="dev-content">
|
|
2141
|
-
|
|
2142
|
-
<!-- Routes Panel -->
|
|
2143
|
-
<div id="panel-routes" class="dev-panel">
|
|
2144
|
-
<div class="dev-panel-header">
|
|
2145
|
-
<h2>Registered Routes</h2>
|
|
2146
|
-
<button class="btn btn-sm" onclick="loadRoutes()">Refresh</button>
|
|
2147
|
-
</div>
|
|
2148
|
-
<table>
|
|
2149
|
-
<thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Handler</th></tr></thead>
|
|
2150
|
-
<tbody id="routes-body"></tbody>
|
|
2151
|
-
</table>
|
|
2152
|
-
</div>
|
|
2153
|
-
|
|
2154
|
-
<!-- Queue Panel -->
|
|
2155
|
-
<div id="panel-queue" class="dev-panel hidden">
|
|
2156
|
-
<div class="dev-panel-header">
|
|
2157
|
-
<h2>Queue Jobs</h2>
|
|
2158
|
-
<div class="flex gap-sm">
|
|
2159
|
-
<button class="btn btn-sm" onclick="loadQueue()">Refresh</button>
|
|
2160
|
-
<button class="btn btn-sm" onclick="retryQueue()">Retry Failed</button>
|
|
2161
|
-
<button class="btn btn-sm btn-danger" onclick="purgeQueue()">Purge Done</button>
|
|
2162
|
-
</div>
|
|
2163
|
-
</div>
|
|
2164
|
-
<div class="toolbar">
|
|
2165
|
-
<button class="btn btn-sm filter-btn active" onclick="filterQueue('', event)">All</button>
|
|
2166
|
-
<button class="btn btn-sm filter-btn" onclick="filterQueue('pending', event)">Pending <span id="q-pending">0</span></button>
|
|
2167
|
-
<button class="btn btn-sm filter-btn" onclick="filterQueue('completed', event)">Done <span id="q-completed">0</span></button>
|
|
2168
|
-
<button class="btn btn-sm filter-btn" onclick="filterQueue('failed', event)">Failed <span id="q-failed">0</span></button>
|
|
2169
|
-
<button class="btn btn-sm filter-btn" onclick="filterQueue('reserved', event)">Active <span id="q-reserved">0</span></button>
|
|
2170
|
-
</div>
|
|
2171
|
-
<table>
|
|
2172
|
-
<thead><tr><th>ID</th><th>Topic</th><th>Status</th><th>Attempts</th><th>Created</th><th>Data</th><th></th></tr></thead>
|
|
2173
|
-
<tbody id="queue-body"></tbody>
|
|
2174
|
-
</table>
|
|
2175
|
-
<div id="queue-empty" class="empty hidden">No queue jobs</div>
|
|
2176
|
-
</div>
|
|
2177
|
-
|
|
2178
|
-
<!-- Mailbox Panel -->
|
|
2179
|
-
<div id="panel-mailbox" class="dev-panel hidden">
|
|
2180
|
-
<div class="dev-panel-header">
|
|
2181
|
-
<h2>Dev Mailbox</h2>
|
|
2182
|
-
<div class="flex gap-sm">
|
|
2183
|
-
<button class="btn btn-sm" onclick="loadMailbox()">Refresh</button>
|
|
2184
|
-
<button class="btn btn-sm btn-primary" onclick="seedMailbox()">Seed 5</button>
|
|
2185
|
-
<button class="btn btn-sm btn-danger" onclick="clearMailbox()">Clear</button>
|
|
2186
|
-
</div>
|
|
2187
|
-
</div>
|
|
2188
|
-
<div class="toolbar">
|
|
2189
|
-
<button class="btn btn-sm filter-btn active" onclick="filterMailbox('', event)">All</button>
|
|
2190
|
-
<button class="btn btn-sm filter-btn" onclick="filterMailbox('inbox', event)">Inbox</button>
|
|
2191
|
-
<button class="btn btn-sm filter-btn" onclick="filterMailbox('outbox', event)">Outbox</button>
|
|
2192
|
-
</div>
|
|
2193
|
-
<div id="mailbox-list"></div>
|
|
2194
|
-
<div id="mail-detail" class="hidden p-md"></div>
|
|
2195
|
-
</div>
|
|
2196
|
-
|
|
2197
|
-
<!-- Messages Panel -->
|
|
2198
|
-
<div id="panel-messages" class="dev-panel hidden">
|
|
2199
|
-
<div class="dev-panel-header">
|
|
2200
|
-
<h2>Message Log</h2>
|
|
2201
|
-
<div class="flex gap-sm items-center">
|
|
2202
|
-
<input type="text" id="msg-search" class="input" placeholder="Search messages..." onkeydown="if(event.key==='Enter')searchMessages()">
|
|
2203
|
-
<button class="btn btn-sm" onclick="searchMessages()">Search</button>
|
|
2204
|
-
<button class="btn btn-sm" onclick="loadMessages()">All</button>
|
|
2205
|
-
<button class="btn btn-sm btn-danger" onclick="clearMessages()">Clear</button>
|
|
2206
|
-
</div>
|
|
2207
|
-
</div>
|
|
2208
|
-
<div id="messages-list"></div>
|
|
2209
|
-
<div id="messages-empty" class="empty">No messages logged</div>
|
|
2210
|
-
</div>
|
|
2211
|
-
|
|
2212
|
-
<!-- Database Panel -->
|
|
2213
|
-
<div id="panel-database" class="dev-panel hidden">
|
|
2214
|
-
<div class="dev-panel-header">
|
|
2215
|
-
<h2>Database</h2>
|
|
2216
|
-
<button class="btn btn-sm" onclick="loadTables()">Refresh</button>
|
|
2217
|
-
</div>
|
|
2218
|
-
<div style="display:flex;height:calc(100vh - 140px);overflow:hidden">
|
|
2219
|
-
<!-- Left: Tables navigation -->
|
|
2220
|
-
<div style="width:200px;min-width:200px;border-right:1px solid var(--border);padding:0.5rem;overflow-y:auto;display:flex;flex-direction:column;gap:0.5rem">
|
|
2221
|
-
<div class="text-sm text-muted" style="font-weight:600">Tables</div>
|
|
2222
|
-
<div id="table-list" class="text-sm"></div>
|
|
2223
|
-
<div style="border-top:1px solid var(--border);padding-top:0.5rem;margin-top:auto">
|
|
2224
|
-
<div class="text-sm text-muted" style="font-weight:600;margin-bottom:0.25rem">Seed Data</div>
|
|
2225
|
-
<select id="seed-table" class="input" style="width:100%;margin-bottom:0.25rem;font-size:0.75rem"><option value="">Pick table...</option></select>
|
|
2226
|
-
<div class="flex gap-sm items-center">
|
|
2227
|
-
<input type="number" id="seed-count" class="input" value="10" min="1" max="1000" style="width:60px;font-size:0.75rem">
|
|
2228
|
-
<button class="btn btn-sm btn-success" onclick="seedTable()">Seed</button>
|
|
2229
|
-
</div>
|
|
2230
|
-
</div>
|
|
2231
|
-
</div>
|
|
2232
|
-
<!-- Right: Query + Results -->
|
|
2233
|
-
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;padding:0.5rem">
|
|
2234
|
-
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.25rem">
|
|
2235
|
-
<select id="query-type" class="input" style="width:auto;font-size:0.75rem">
|
|
2236
|
-
<option value="sql">SQL</option>
|
|
2237
|
-
<option value="graphql">GraphQL</option>
|
|
2238
|
-
</select>
|
|
2239
|
-
<span class="text-sm text-muted">Limit</span>
|
|
2240
|
-
<select id="query-limit" class="input" style="width:70px;font-size:0.75rem">
|
|
2241
|
-
<option value="20">20</option>
|
|
2242
|
-
<option value="50">50</option>
|
|
2243
|
-
<option value="100">100</option>
|
|
2244
|
-
<option value="500">500</option>
|
|
2245
|
-
<option value="0">All</option>
|
|
2246
|
-
</select>
|
|
2247
|
-
<button class="btn btn-sm btn-primary" onclick="runQuery()">Run</button>
|
|
2248
|
-
<button class="btn btn-sm" id="btn-csv" onclick="copyResults('csv',this)" title="Copy results as CSV">Copy CSV</button>
|
|
2249
|
-
<button class="btn btn-sm" id="btn-json" onclick="copyResults('json',this)" title="Copy results as JSON">Copy JSON</button>
|
|
2250
|
-
<button class="btn btn-sm" onclick="pasteData()" title="Paste tab-separated data as INSERTs">Paste</button>
|
|
2251
|
-
<span class="text-sm text-muted">Ctrl+Enter</span>
|
|
2252
|
-
</div>
|
|
2253
|
-
<textarea id="query-input" rows="3" placeholder="SELECT * FROM users LIMIT 20" class="input input-mono" style="width:100%;font-size:0.75rem;resize:vertical"></textarea>
|
|
2254
|
-
<div id="query-error" class="hidden" style="color:var(--danger);font-size:0.75rem;margin-top:0.25rem"></div>
|
|
2255
|
-
<div id="query-results" style="flex:1;overflow:auto;margin-top:0.25rem;font-size:0.75rem"></div>
|
|
2256
|
-
</div>
|
|
2257
|
-
</div>
|
|
2258
|
-
</div>
|
|
2259
|
-
|
|
2260
|
-
<!-- Requests Panel -->
|
|
2261
|
-
<div id="panel-requests" class="dev-panel hidden">
|
|
2262
|
-
<div class="dev-panel-header">
|
|
2263
|
-
<h2>Request Inspector</h2>
|
|
2264
|
-
<div class="flex gap-sm">
|
|
2265
|
-
<button class="btn btn-sm" onclick="loadRequests()">Refresh</button>
|
|
2266
|
-
<button class="btn btn-sm btn-danger" onclick="clearRequests()">Clear</button>
|
|
2267
|
-
</div>
|
|
2268
|
-
</div>
|
|
2269
|
-
<div id="req-stats" class="toolbar text-sm text-muted"></div>
|
|
2270
|
-
<table>
|
|
2271
|
-
<thead><tr><th>Time</th><th>Method</th><th>Path</th><th>Status</th><th>Duration</th><th>Size</th></tr></thead>
|
|
2272
|
-
<tbody id="req-body"></tbody>
|
|
2273
|
-
</table>
|
|
2274
|
-
<div id="req-empty" class="empty hidden">No requests captured</div>
|
|
2275
|
-
</div>
|
|
2276
|
-
|
|
2277
|
-
<!-- Errors Panel -->
|
|
2278
|
-
<div id="panel-errors" class="dev-panel hidden">
|
|
2279
|
-
<div class="dev-panel-header">
|
|
2280
|
-
<h2>Error Tracker</h2>
|
|
2281
|
-
<div class="flex gap-sm">
|
|
2282
|
-
<button class="btn btn-sm" onclick="loadErrors()">Refresh</button>
|
|
2283
|
-
<button class="btn btn-sm btn-danger" onclick="clearResolvedErrors()">Clear Resolved</button>
|
|
2284
|
-
</div>
|
|
2285
|
-
</div>
|
|
2286
|
-
<div id="errors-list"></div>
|
|
2287
|
-
<div id="errors-empty" class="empty">No errors tracked</div>
|
|
2288
|
-
</div>
|
|
2289
|
-
|
|
2290
|
-
<!-- WebSocket Panel -->
|
|
2291
|
-
<div id="panel-websockets" class="dev-panel hidden">
|
|
2292
|
-
<div class="dev-panel-header">
|
|
2293
|
-
<h2>WebSocket Connections</h2>
|
|
2294
|
-
<button class="btn btn-sm" onclick="loadWebSockets()">Refresh</button>
|
|
2295
|
-
</div>
|
|
2296
|
-
<table>
|
|
2297
|
-
<thead><tr><th>ID</th><th>Path</th><th>IP</th><th>Connected</th><th>Status</th><th></th></tr></thead>
|
|
2298
|
-
<tbody id="ws-body"></tbody>
|
|
2299
|
-
</table>
|
|
2300
|
-
<div id="ws-empty" class="empty">No active connections</div>
|
|
2301
|
-
</div>
|
|
2302
|
-
|
|
2303
|
-
<!-- System Panel -->
|
|
2304
|
-
<div id="panel-system" class="dev-panel hidden">
|
|
2305
|
-
<div class="dev-panel-header">
|
|
2306
|
-
<h2>System Overview</h2>
|
|
2307
|
-
<button class="btn btn-sm" onclick="loadSystem()">Refresh</button>
|
|
2308
|
-
</div>
|
|
2309
|
-
<div id="sys-cards" class="sys-grid"></div>
|
|
2310
|
-
<div id="sys-extensions" class="hidden"></div>
|
|
2311
|
-
</div>
|
|
2312
|
-
|
|
2313
|
-
<!-- Tools Panel -->
|
|
2314
|
-
<div id="panel-tools" class="dev-panel hidden">
|
|
2315
|
-
<div class="dev-panel-header">
|
|
2316
|
-
<h2>Developer Tools</h2>
|
|
2317
|
-
</div>
|
|
2318
|
-
<div class="sys-grid">
|
|
2319
|
-
<div class="sys-card" style="cursor:pointer" onclick="runTool('test')">
|
|
2320
|
-
<div class="label">Run Tests</div>
|
|
2321
|
-
<div style="font-size:0.8rem;margin-top:0.25rem">Execute the test suite</div>
|
|
2322
|
-
</div>
|
|
2323
|
-
<div class="sys-card" style="cursor:pointer" onclick="runTool('routes')">
|
|
2324
|
-
<div class="label">List Routes</div>
|
|
2325
|
-
<div style="font-size:0.8rem;margin-top:0.25rem">Show all registered routes with auth status</div>
|
|
2326
|
-
</div>
|
|
2327
|
-
<div class="sys-card" style="cursor:pointer" onclick="runTool('migrate')">
|
|
2328
|
-
<div class="label">Run Migrations</div>
|
|
2329
|
-
<div style="font-size:0.8rem;margin-top:0.25rem">Apply pending database migrations</div>
|
|
2330
|
-
</div>
|
|
2331
|
-
<div class="sys-card" style="cursor:pointer" onclick="runTool('seed')">
|
|
2332
|
-
<div class="label">Run Seeders</div>
|
|
2333
|
-
<div style="font-size:0.8rem;margin-top:0.25rem">Execute seed scripts</div>
|
|
2334
|
-
</div>
|
|
2335
|
-
</div>
|
|
2336
|
-
<div id="tool-output" class="hidden" style="margin:1rem">
|
|
2337
|
-
<div class="dev-panel-header">
|
|
2338
|
-
<h2 id="tool-title">Output</h2>
|
|
2339
|
-
<button class="btn btn-sm" onclick="document.getElementById('tool-output').classList.add('hidden')">Close</button>
|
|
2340
|
-
</div>
|
|
2341
|
-
<pre id="tool-result" style="padding:1rem;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);font-size:0.75rem;font-family:var(--mono);max-height:400px;overflow:auto;white-space:pre-wrap"></pre>
|
|
2342
|
-
</div>
|
|
2343
|
-
</div>
|
|
2344
|
-
|
|
2345
|
-
<!-- Connections Panel -->
|
|
2346
|
-
<div id="panel-connections" class="dev-panel hidden">
|
|
2347
|
-
<div class="dev-panel-header">
|
|
2348
|
-
<h2>Connection Builder</h2>
|
|
2349
|
-
</div>
|
|
2350
|
-
<div class="p-md">
|
|
2351
|
-
<div class="flex gap-md" style="flex-wrap:wrap">
|
|
2352
|
-
<div style="flex:1;min-width:300px">
|
|
2353
|
-
<div class="mb-sm">
|
|
2354
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Driver</label>
|
|
2355
|
-
<select id="conn-driver" class="input" style="width:100%" onchange="connDriverChanged()">
|
|
2356
|
-
<option value="sqlite">SQLite</option>
|
|
2357
|
-
<option value="postgresql">PostgreSQL</option>
|
|
2358
|
-
<option value="mysql">MySQL</option>
|
|
2359
|
-
<option value="mssql">MSSQL</option>
|
|
2360
|
-
<option value="firebird">Firebird</option>
|
|
2361
|
-
</select>
|
|
2362
|
-
</div>
|
|
2363
|
-
<div class="mb-sm conn-server-field">
|
|
2364
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Host</label>
|
|
2365
|
-
<input type="text" id="conn-host" class="input" style="width:100%" value="localhost" placeholder="localhost" oninput="updateConnectionUrl()">
|
|
2366
|
-
</div>
|
|
2367
|
-
<div class="mb-sm conn-server-field">
|
|
2368
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Port</label>
|
|
2369
|
-
<input type="number" id="conn-port" class="input" style="width:100%" placeholder="5432" oninput="updateConnectionUrl()">
|
|
2370
|
-
</div>
|
|
2371
|
-
<div class="mb-sm">
|
|
2372
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Database</label>
|
|
2373
|
-
<input type="text" id="conn-database" class="input" style="width:100%" placeholder="mydb" oninput="updateConnectionUrl()">
|
|
2374
|
-
</div>
|
|
2375
|
-
<div class="mb-sm conn-server-field">
|
|
2376
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Username</label>
|
|
2377
|
-
<input type="text" id="conn-username" class="input" style="width:100%" placeholder="username">
|
|
2378
|
-
</div>
|
|
2379
|
-
<div class="mb-sm conn-server-field">
|
|
2380
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Password</label>
|
|
2381
|
-
<input type="password" id="conn-password" class="input" style="width:100%" placeholder="password">
|
|
2382
|
-
</div>
|
|
2383
|
-
<div class="mb-sm">
|
|
2384
|
-
<label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Connection URL</label>
|
|
2385
|
-
<input type="text" id="conn-url" class="input input-mono" style="width:100%" readonly>
|
|
2386
|
-
</div>
|
|
2387
|
-
<div class="flex gap-sm">
|
|
2388
|
-
<button class="btn btn-primary" onclick="testConnection()">Test Connection</button>
|
|
2389
|
-
<button class="btn btn-success" onclick="saveConnection()">Save to .env</button>
|
|
2390
|
-
</div>
|
|
2391
|
-
</div>
|
|
2392
|
-
<div style="width:300px">
|
|
2393
|
-
<div class="dev-panel" style="margin-bottom:1rem">
|
|
2394
|
-
<div class="dev-panel-header"><h2>Test Result</h2></div>
|
|
2395
|
-
<div id="conn-test-result" class="p-md text-sm text-muted">No test run yet</div>
|
|
2396
|
-
</div>
|
|
2397
|
-
<div class="dev-panel">
|
|
2398
|
-
<div class="dev-panel-header"><h2>Current .env Values</h2></div>
|
|
2399
|
-
<div id="conn-env-values" class="p-md text-sm text-muted">Loading...</div>
|
|
2400
|
-
</div>
|
|
2401
|
-
</div>
|
|
2402
|
-
</div>
|
|
2403
|
-
</div>
|
|
2404
|
-
</div>
|
|
2405
|
-
|
|
2406
|
-
<script>
|
|
2407
|
-
function connDriverChanged() {
|
|
2408
|
-
var driver = document.getElementById('conn-driver').value;
|
|
2409
|
-
var ports = {postgresql: 5432, mysql: 3306, mssql: 1433, firebird: 3050};
|
|
2410
|
-
var isSqlite = (driver === 'sqlite');
|
|
2411
|
-
document.getElementById('conn-port').value = ports[driver] || '';
|
|
2412
|
-
var fields = document.querySelectorAll('.conn-server-field');
|
|
2413
|
-
for (var i = 0; i < fields.length; i++) {
|
|
2414
|
-
fields[i].style.display = isSqlite ? 'none' : '';
|
|
2415
|
-
}
|
|
2416
|
-
updateConnectionUrl();
|
|
2417
|
-
}
|
|
2418
|
-
function updateConnectionUrl() {
|
|
2419
|
-
var driver = document.getElementById('conn-driver').value;
|
|
2420
|
-
var host = document.getElementById('conn-host').value || 'localhost';
|
|
2421
|
-
var port = document.getElementById('conn-port').value;
|
|
2422
|
-
var database = document.getElementById('conn-database').value;
|
|
2423
|
-
if (driver === 'sqlite') {
|
|
2424
|
-
document.getElementById('conn-url').value = 'sqlite:///' + database;
|
|
2425
|
-
} else {
|
|
2426
|
-
document.getElementById('conn-url').value = driver + '://' + host + ':' + port + '/' + database;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
function testConnection() {
|
|
2430
|
-
var url = document.getElementById('conn-url').value;
|
|
2431
|
-
var username = document.getElementById('conn-username').value;
|
|
2432
|
-
var password = document.getElementById('conn-password').value;
|
|
2433
|
-
var el = document.getElementById('conn-test-result');
|
|
2434
|
-
el.innerHTML = '<span class="text-muted">Testing...</span>';
|
|
2435
|
-
fetch('/__dev/api/connections/test', {
|
|
2436
|
-
method: 'POST',
|
|
2437
|
-
headers: {'Content-Type': 'application/json'},
|
|
2438
|
-
body: JSON.stringify({url: url, username: username, password: password})
|
|
2439
|
-
}).then(function(r){return r.json()}).then(function(data) {
|
|
2440
|
-
if (data.success) {
|
|
2441
|
-
el.innerHTML = '<div style="color:var(--success);font-weight:600;margin-bottom:0.5rem">✔ Connected</div>' +
|
|
2442
|
-
'<div class="text-sm">Version: ' + (data.version || 'N/A') + '</div>' +
|
|
2443
|
-
'<div class="text-sm">Tables: ' + (data.tables !== undefined ? data.tables : 'N/A') + '</div>';
|
|
2444
|
-
} else {
|
|
2445
|
-
el.innerHTML = '<div style="color:var(--danger);font-weight:600;margin-bottom:0.5rem">✘ Failed</div>' +
|
|
2446
|
-
'<div class="text-sm" style="color:var(--danger)">' + (data.error || 'Unknown error') + '</div>';
|
|
2447
|
-
}
|
|
2448
|
-
}).catch(function(e) {
|
|
2449
|
-
el.innerHTML = '<div style="color:var(--danger)">Error: ' + e.message + '</div>';
|
|
2450
|
-
});
|
|
2451
|
-
}
|
|
2452
|
-
function saveConnection() {
|
|
2453
|
-
var url = document.getElementById('conn-url').value;
|
|
2454
|
-
var username = document.getElementById('conn-username').value;
|
|
2455
|
-
var password = document.getElementById('conn-password').value;
|
|
2456
|
-
if (!url) { alert('Please build a connection URL first'); return; }
|
|
2457
|
-
fetch('/__dev/api/connections/save', {
|
|
2458
|
-
method: 'POST',
|
|
2459
|
-
headers: {'Content-Type': 'application/json'},
|
|
2460
|
-
body: JSON.stringify({url: url, username: username, password: password})
|
|
2461
|
-
}).then(function(r){return r.json()}).then(function(data) {
|
|
2462
|
-
if (data.success) {
|
|
2463
|
-
alert('Connection saved to .env');
|
|
2464
|
-
loadConnectionEnv();
|
|
2465
|
-
} else {
|
|
2466
|
-
alert('Save failed: ' + (data.error || 'Unknown error'));
|
|
2467
|
-
}
|
|
2468
|
-
}).catch(function(e) { alert('Error: ' + e.message); });
|
|
2469
|
-
}
|
|
2470
|
-
function loadConnectionEnv() {
|
|
2471
|
-
fetch('/__dev/api/connections').then(function(r){return r.json()}).then(function(data) {
|
|
2472
|
-
var el = document.getElementById('conn-env-values');
|
|
2473
|
-
el.innerHTML = '<div class="mb-sm"><span class="text-muted">DATABASE_URL:</span> <code>' + (data.url || '<em>not set</em>') + '</code></div>' +
|
|
2474
|
-
'<div class="mb-sm"><span class="text-muted">DATABASE_USERNAME:</span> <code>' + (data.username || '<em>not set</em>') + '</code></div>' +
|
|
2475
|
-
'<div><span class="text-muted">DATABASE_PASSWORD:</span> <code>' + (data.password || '<em>not set</em>') + '</code></div>';
|
|
2476
|
-
}).catch(function() {
|
|
2477
|
-
document.getElementById('conn-env-values').innerHTML = '<span class="text-muted">Could not load .env values</span>';
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2480
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
2481
|
-
var connTab = document.querySelector('[onclick*="connections"]');
|
|
2482
|
-
if (connTab) {
|
|
2483
|
-
connTab.addEventListener('click', function() { loadConnectionEnv(); }, {once: true});
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2486
|
-
</script>
|
|
2487
|
-
|
|
2488
|
-
<!-- Metrics Panel -->
|
|
2489
|
-
<div id="panel-metrics" class="dev-panel hidden">
|
|
2490
|
-
<div class="dev-panel-header">
|
|
2491
|
-
<h2>Code Metrics</h2>
|
|
2492
|
-
<div>
|
|
2493
|
-
<button class="btn btn-sm" onclick="loadAllMetrics()">Refresh</button>
|
|
2494
|
-
</div>
|
|
2495
|
-
</div>
|
|
2496
|
-
<div id="metrics-bubble" style="margin:1rem;"></div>
|
|
2497
|
-
<div id="metrics-drilldown" style="margin:0 1rem;display:none;"></div>
|
|
2498
|
-
<div id="metrics-quick" class="sys-grid"></div>
|
|
2499
|
-
<div id="metrics-largest" style="margin-top:1rem;"></div>
|
|
2500
|
-
<div id="metrics-tables" style="margin-top:1rem;padding:0 1rem 1rem;overflow-x:auto;">
|
|
2501
|
-
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">File Analysis</h3>
|
|
2502
|
-
<div id="metrics-heatmap"></div>
|
|
2503
|
-
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Most Complex Functions</h3>
|
|
2504
|
-
<div id="metrics-complex"></div>
|
|
2505
|
-
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Coupling Analysis</h3>
|
|
2506
|
-
<div id="metrics-coupling"></div>
|
|
2507
|
-
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Violations</h3>
|
|
2508
|
-
<div id="metrics-violations"></div>
|
|
2509
|
-
</div>
|
|
2510
|
-
</div>
|
|
2511
|
-
|
|
2512
|
-
<!-- Chat Panel (Tina4) -->
|
|
2513
|
-
<div id="panel-chat" class="dev-panel hidden">
|
|
2514
|
-
<div class="dev-panel-header">
|
|
2515
|
-
<h2>Tina4</h2>
|
|
2516
|
-
<div class="flex gap-sm items-center">
|
|
2517
|
-
<select id="ai-provider" class="input" style="width:120px">
|
|
2518
|
-
<option value="anthropic">Claude</option>
|
|
2519
|
-
<option value="openai">OpenAI</option>
|
|
2520
|
-
</select>
|
|
2521
|
-
<input type="password" id="ai-key" class="input" placeholder="Paste API key..." style="width:250px">
|
|
2522
|
-
<button class="btn btn-sm btn-primary" onclick="setAiKey()">Set Key</button>
|
|
2523
|
-
<span class="text-sm text-muted" id="ai-status">No key set</span>
|
|
2524
|
-
</div>
|
|
2525
|
-
</div>
|
|
2526
|
-
<div class="chat-container">
|
|
2527
|
-
<div class="chat-messages" id="chat-messages">
|
|
2528
|
-
<div class="chat-msg chat-bot">Hi! I'm Tina4. Ask me about routes, ORM, database, queues, templates, auth, or any Tina4 feature.</div>
|
|
2529
|
-
</div>
|
|
2530
|
-
<div class="chat-input-row">
|
|
2531
|
-
<input type="text" id="chat-input" class="input" placeholder="Ask Tina4..." onkeydown="if(event.key==='Enter')sendChat()">
|
|
2532
|
-
<button class="btn btn-primary" onclick="sendChat()">Send</button>
|
|
2533
|
-
</div>
|
|
2534
|
-
</div>
|
|
2535
|
-
</div>
|
|
2536
|
-
|
|
2537
|
-
</div>
|
|
2538
|
-
|
|
2539
|
-
<script src="/__dev/js/tina4-dev-admin.min.js"></script>
|
|
2540
|
-
<script>
|
|
2541
|
-
// ── Metrics Panel JS ──
|
|
2542
|
-
var _metricsFullData=null;
|
|
2543
|
-
function miColor(mi){
|
|
2544
|
-
if(mi>=60) return 'rgb('+(Math.round(34+(1-((mi-60)/40))*186))+','+(Math.round(197-(1-((mi-60)/40))*50))+',0)';
|
|
2545
|
-
if(mi>=30) return 'rgb('+(Math.round(220+((60-mi)/30)*19))+','+(Math.round(180-((60-mi)/30)*112))+',0)';
|
|
2546
|
-
return 'rgb(239,'+(Math.round(68-mi*2))+',0)';
|
|
2547
|
-
}
|
|
2548
|
-
function renderBubbleChart(files,depGraph,scanMode){
|
|
2549
|
-
var container=document.getElementById('metrics-bubble');
|
|
2550
|
-
if(!files||!files.length){container.innerHTML='<p style="color:var(--muted);padding:1rem">No files to analyze</p>';return;}
|
|
2551
|
-
depGraph=depGraph||{};
|
|
2552
|
-
scanMode=scanMode||'project';
|
|
2553
|
-
var W=container.offsetWidth||900,H=Math.max(450,Math.min(650,W*0.45));
|
|
2554
|
-
var maxLoc=Math.max.apply(null,files.map(function(f){return f.loc}))||1;
|
|
2555
|
-
var maxDeps=Math.max.apply(null,files.map(function(f){return f.dep_count||0}))||1;
|
|
2556
|
-
var maxCC=Math.max.apply(null,files.map(function(f){return f.complexity||0}))||1;
|
|
2557
|
-
var minR=14,maxR=Math.min(70,W/10);
|
|
2558
|
-
// Composite health colour: complexity + tests + dependencies
|
|
2559
|
-
function healthColor(f){
|
|
2560
|
-
var cc=Math.min((f.avg_complexity||0)/10,1);
|
|
2561
|
-
var untested=f.has_tests?0:1;
|
|
2562
|
-
var deps=Math.min((f.dep_count||0)/5,1);
|
|
2563
|
-
var score=cc*0.4+untested*0.4+deps*0.2;
|
|
2564
|
-
score=Math.max(0,Math.min(1,score));
|
|
2565
|
-
var hue=Math.round(120*(1-score));
|
|
2566
|
-
var sat=Math.round(70+score*30);
|
|
2567
|
-
var lit=Math.round(42+18*(1-score));
|
|
2568
|
-
return 'hsl('+hue+','+sat+'%,'+lit+'%)';
|
|
2569
|
-
}
|
|
2570
|
-
// Build path->index lookup
|
|
2571
|
-
var pathIdx={};
|
|
2572
|
-
files.forEach(function(f,i){pathIdx[f.path]=i;});
|
|
2573
|
-
// Spiral placement
|
|
2574
|
-
function sizeScore(f){return (f.loc/maxLoc)*0.4+((f.avg_complexity||0)/10)*0.4+((f.dep_count||0)/maxDeps)*0.2;}
|
|
2575
|
-
var sorted=files.slice().sort(function(a,b){return sizeScore(a)-sizeScore(b)});
|
|
2576
|
-
var cx=W/2,cy=H/2;
|
|
2577
|
-
var bubbles=[];
|
|
2578
|
-
var angle=0,spiralR=0;
|
|
2579
|
-
for(var i=0;i<sorted.length;i++){
|
|
2580
|
-
var f=sorted[i];
|
|
2581
|
-
var r=minR+Math.sqrt(sizeScore(f))*(maxR-minR);
|
|
2582
|
-
var color=healthColor(f);
|
|
2583
|
-
var placed=false;
|
|
2584
|
-
for(var attempt=0;attempt<800;attempt++){
|
|
2585
|
-
var px=cx+spiralR*Math.cos(angle);
|
|
2586
|
-
var py=cy+spiralR*Math.sin(angle);
|
|
2587
|
-
var collides=false;
|
|
2588
|
-
for(var j=0;j<bubbles.length;j++){
|
|
2589
|
-
var dx=px-bubbles[j].x,dy=py-bubbles[j].y;
|
|
2590
|
-
if(Math.sqrt(dx*dx+dy*dy)<r+bubbles[j].r+2){collides=true;break;}
|
|
2591
|
-
}
|
|
2592
|
-
if(!collides&&px>r+2&&px<W-r-2&&py>r+25&&py<H-r-2){
|
|
2593
|
-
bubbles.push({x:px,y:py,vx:0,vy:0,r:r,color:color,f:f});
|
|
2594
|
-
placed=true;break;
|
|
2595
|
-
}
|
|
2596
|
-
angle+=0.2;spiralR+=0.04;
|
|
2597
|
-
}
|
|
2598
|
-
if(!placed){bubbles.push({x:cx+(Math.random()-0.5)*W*0.3,y:cy+(Math.random()-0.5)*H*0.3,vx:0,vy:0,r:r,color:color,f:f});}
|
|
2599
|
-
}
|
|
2600
|
-
// Build edge list from dependency graph — match by filename not full path
|
|
2601
|
-
var edges=[];
|
|
2602
|
-
function basename(p){var n=p.split('/').pop();var d=n.lastIndexOf('.');return(d>0?n.substring(0,d):n).toLowerCase();}
|
|
2603
|
-
var nameIdx={};
|
|
2604
|
-
bubbles.forEach(function(b,i){nameIdx[basename(b.f.path)]=i;});
|
|
2605
|
-
Object.keys(depGraph).forEach(function(src){
|
|
2606
|
-
var srcIdx=null;
|
|
2607
|
-
bubbles.forEach(function(b,i){if(b.f.path===src)srcIdx=i;});
|
|
2608
|
-
if(srcIdx===null)return;
|
|
2609
|
-
(depGraph[src]||[]).forEach(function(tgt){
|
|
2610
|
-
var tgtName=basename(tgt);
|
|
2611
|
-
var tgtIdx=nameIdx[tgtName];
|
|
2612
|
-
if(tgtIdx!==undefined&&srcIdx!==tgtIdx)edges.push([srcIdx,tgtIdx]);
|
|
2613
|
-
});
|
|
2614
|
-
});
|
|
2615
|
-
// Canvas
|
|
2616
|
-
var canvas=document.createElement('canvas');
|
|
2617
|
-
canvas.width=W;canvas.height=H;
|
|
2618
|
-
canvas.style.cssText='display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a';
|
|
2619
|
-
var modeLabel=scanMode==='framework'?'<span style="color:#cba6f7;font-weight:600"> (Framework)</span> Add code to src/ to see your project':'';
|
|
2620
|
-
container.innerHTML='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;color:var(--primary)">Code Landscape'+modeLabel+'</h3><span style="font-size:0.7rem;color:var(--muted)">Drag bubbles | Click to drill down | Size=LOC | Colour=health | T=tested | D=deps</span></div>';
|
|
2621
|
-
container.appendChild(canvas);
|
|
2622
|
-
var ctx=canvas.getContext('2d');
|
|
2623
|
-
var hoveredIdx=-1,dragIdx=-1,dragOX=0,dragOY=0;
|
|
2624
|
-
// Physics
|
|
2625
|
-
function simulate(){
|
|
2626
|
-
var damping=0.65,springK=0.002,repulse=40,gravity=0.008;
|
|
2627
|
-
var cx=W/2,cy=H/2;
|
|
2628
|
-
// Gravity: pull all bubbles toward center, bigger = stronger pull
|
|
2629
|
-
bubbles.forEach(function(b,idx){
|
|
2630
|
-
if(idx===dragIdx)return;
|
|
2631
|
-
var dx=cx-b.x,dy=cy-b.y;
|
|
2632
|
-
var sizeFactor=0.3+(b.r/maxR)*0.7;
|
|
2633
|
-
var pull=gravity*sizeFactor*sizeFactor;
|
|
2634
|
-
b.vx+=dx*pull;b.vy+=dy*pull;
|
|
2635
|
-
});
|
|
2636
|
-
// Spring forces along edges
|
|
2637
|
-
edges.forEach(function(e){
|
|
2638
|
-
var a=bubbles[e[0]],b=bubbles[e[1]];
|
|
2639
|
-
var dx=b.x-a.x,dy=b.y-a.y;
|
|
2640
|
-
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
2641
|
-
var rest=a.r+b.r+20;
|
|
2642
|
-
var force=(dist-rest)*springK;
|
|
2643
|
-
var fx=dx/dist*force,fy=dy/dist*force;
|
|
2644
|
-
if(e[0]!==dragIdx){a.vx+=fx;a.vy+=fy;}
|
|
2645
|
-
if(e[1]!==dragIdx){b.vx-=fx;b.vy-=fy;}
|
|
2646
|
-
});
|
|
2647
|
-
// Soft repulsion
|
|
2648
|
-
for(var i=0;i<bubbles.length;i++){
|
|
2649
|
-
for(var j=i+1;j<bubbles.length;j++){
|
|
2650
|
-
var a=bubbles[i],b=bubbles[j];
|
|
2651
|
-
var dx=b.x-a.x,dy=b.y-a.y;
|
|
2652
|
-
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
2653
|
-
var minDist=a.r+b.r+20;
|
|
2654
|
-
if(dist<minDist){
|
|
2655
|
-
var force=repulse*(minDist-dist)/minDist;
|
|
2656
|
-
var fx=dx/dist*force,fy=dy/dist*force;
|
|
2657
|
-
if(i!==dragIdx){a.vx-=fx;a.vy-=fy;}
|
|
2658
|
-
if(j!==dragIdx){b.vx+=fx;b.vy+=fy;}
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
// Apply velocity + damping + boundary
|
|
2663
|
-
bubbles.forEach(function(b,idx){
|
|
2664
|
-
if(idx===dragIdx)return;
|
|
2665
|
-
b.vx*=damping;b.vy*=damping;
|
|
2666
|
-
// Cap velocity
|
|
2667
|
-
var maxV=2;
|
|
2668
|
-
if(b.vx>maxV)b.vx=maxV;if(b.vx<-maxV)b.vx=-maxV;
|
|
2669
|
-
if(b.vy>maxV)b.vy=maxV;if(b.vy<-maxV)b.vy=-maxV;
|
|
2670
|
-
b.x+=b.vx;b.y+=b.vy;
|
|
2671
|
-
b.x=Math.max(b.r+2,Math.min(W-b.r-2,b.x));
|
|
2672
|
-
b.y=Math.max(b.r+25,Math.min(H-b.r-2,b.y));
|
|
2673
|
-
});
|
|
2674
|
-
}
|
|
2675
|
-
// Draw
|
|
2676
|
-
function draw(){
|
|
2677
|
-
simulate();
|
|
2678
|
-
ctx.clearRect(0,0,W,H);
|
|
2679
|
-
ctx.save();ctx.translate(panX,panY);ctx.scale(zoom,zoom);
|
|
2680
|
-
// Grid
|
|
2681
|
-
ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=1/zoom;
|
|
2682
|
-
for(var gx=0;gx<W/zoom;gx+=50){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H/zoom);ctx.stroke();}
|
|
2683
|
-
for(var gy=0;gy<H/zoom;gy+=50){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W/zoom,gy);ctx.stroke();}
|
|
2684
|
-
// Dependency arrows
|
|
2685
|
-
edges.forEach(function(e){
|
|
2686
|
-
var a=bubbles[e[0]],b=bubbles[e[1]];
|
|
2687
|
-
var dx=b.x-a.x,dy=b.y-a.y;
|
|
2688
|
-
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
2689
|
-
var highlighted=(hoveredIdx===e[0]||hoveredIdx===e[1]);
|
|
2690
|
-
ctx.beginPath();
|
|
2691
|
-
ctx.moveTo(a.x+dx/dist*a.r,a.y+dy/dist*a.r);
|
|
2692
|
-
var ex=b.x-dx/dist*b.r,ey=b.y-dy/dist*b.r;
|
|
2693
|
-
ctx.lineTo(ex,ey);
|
|
2694
|
-
ctx.strokeStyle=highlighted?'rgba(139,180,250,0.9)':'rgba(255,255,255,0.3)';
|
|
2695
|
-
ctx.lineWidth=highlighted?3:1.5;ctx.stroke();
|
|
2696
|
-
// Arrowhead
|
|
2697
|
-
var aLen=highlighted?14:8;
|
|
2698
|
-
var aAngle=Math.atan2(dy,dx);
|
|
2699
|
-
ctx.beginPath();
|
|
2700
|
-
ctx.moveTo(ex,ey);
|
|
2701
|
-
ctx.lineTo(ex-aLen*Math.cos(aAngle-0.4),ey-aLen*Math.sin(aAngle-0.4));
|
|
2702
|
-
ctx.lineTo(ex-aLen*Math.cos(aAngle+0.4),ey-aLen*Math.sin(aAngle+0.4));
|
|
2703
|
-
ctx.closePath();ctx.fillStyle=ctx.strokeStyle;ctx.fill();
|
|
2704
|
-
});
|
|
2705
|
-
// Bubbles
|
|
2706
|
-
bubbles.forEach(function(b,idx){
|
|
2707
|
-
var isHovered=(idx===hoveredIdx);
|
|
2708
|
-
var drawR=isHovered?b.r+4:b.r;
|
|
2709
|
-
if(isHovered){ctx.beginPath();ctx.arc(b.x,b.y,drawR+8,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.08)';ctx.fill();}
|
|
2710
|
-
// Matte flat bubble
|
|
2711
|
-
ctx.beginPath();ctx.arc(b.x,b.y,drawR,0,Math.PI*2);
|
|
2712
|
-
ctx.fillStyle=b.color;ctx.globalAlpha=isHovered?1.0:0.85;ctx.fill();
|
|
2713
|
-
ctx.globalAlpha=1;ctx.strokeStyle=isHovered?'rgba(255,255,255,0.6)':'rgba(255,255,255,0.25)';ctx.lineWidth=isHovered?2.5:1.5;ctx.stroke();
|
|
2714
|
-
// Label
|
|
2715
|
-
var name=b.f.path.split('/').pop().replace('.ts','').replace('.js','');
|
|
2716
|
-
if(drawR>16){
|
|
2717
|
-
var fs=Math.max(8,Math.min(13,drawR*0.38));
|
|
2718
|
-
ctx.fillStyle='#fff';ctx.font='600 '+fs+'px monospace';ctx.textAlign='center';
|
|
2719
|
-
ctx.fillText(name,b.x,b.y-2);
|
|
2720
|
-
ctx.fillStyle='rgba(255,255,255,0.65)';ctx.font=(fs-1)+'px monospace';
|
|
2721
|
-
ctx.fillText(b.f.loc+' LOC',b.x,b.y+fs);
|
|
2722
|
-
if(isHovered&&drawR>25){
|
|
2723
|
-
ctx.fillStyle='rgba(255,255,255,0.5)';ctx.font=(fs-2)+'px monospace';
|
|
2724
|
-
ctx.fillText('CC:'+b.f.complexity+' MI:'+b.f.maintainability,b.x,b.y+fs*2);
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
// Markers: T (tested) and D (dependencies) — inverted badges
|
|
2728
|
-
var mfs=Math.max(9,drawR*0.3);
|
|
2729
|
-
var mrad=mfs*0.7;
|
|
2730
|
-
var mpad=mrad*2.4;
|
|
2731
|
-
var my=b.y-drawR+mrad+3;
|
|
2732
|
-
if(drawR>14&&b.f.has_tests){
|
|
2733
|
-
var mx=b.x-(b.f.dep_count>0?mpad*0.5:0);
|
|
2734
|
-
ctx.beginPath();ctx.arc(mx,my,mrad,0,Math.PI*2);
|
|
2735
|
-
ctx.fillStyle='#16a34a';ctx.fill();
|
|
2736
|
-
ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
|
|
2737
|
-
ctx.fillText('T',mx,my+mfs*0.35);
|
|
2738
|
-
}
|
|
2739
|
-
if(drawR>14&&b.f.dep_count>0){
|
|
2740
|
-
var mx2=b.x+(b.f.has_tests?mpad*0.5:0);
|
|
2741
|
-
ctx.beginPath();ctx.arc(mx2,my,mrad,0,Math.PI*2);
|
|
2742
|
-
ctx.fillStyle='#ea580c';ctx.fill();
|
|
2743
|
-
ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
|
|
2744
|
-
ctx.fillText('D',mx2,my+mfs*0.35);
|
|
2745
|
-
}
|
|
2746
|
-
b._drawX=b.x;b._drawY=b.y;b._drawR=drawR;
|
|
2747
|
-
});
|
|
2748
|
-
// Summary
|
|
2749
|
-
var totalLoc=0,totalFiles=bubbles.length,testedCount=0;
|
|
2750
|
-
bubbles.forEach(function(b){totalLoc+=b.f.loc;if(b.f.has_tests)testedCount++;});
|
|
2751
|
-
var avgMI=bubbles.reduce(function(s,b){return s+b.f.maintainability},0)/totalFiles;
|
|
2752
|
-
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
|
|
2753
|
-
ctx.restore();
|
|
2754
|
-
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
|
|
2755
|
-
ctx.fillText(totalFiles+' files | '+totalLoc.toLocaleString()+' LOC | MI:'+avgMI.toFixed(1)+' | Tested:'+testedCount+'/'+totalFiles,W-12,H-10);
|
|
2756
|
-
window._metricsAnimFrame=requestAnimationFrame(draw);
|
|
2757
|
-
}
|
|
2758
|
-
draw();
|
|
2759
|
-
// Mouse events — hover + drag bubbles + right-click pan
|
|
2760
|
-
var panning=false,panStartX=0,panStartY=0;
|
|
2761
|
-
canvas.addEventListener('contextmenu',function(e){e.preventDefault();});
|
|
2762
|
-
canvas.addEventListener('mousemove',function(e){
|
|
2763
|
-
var rect=canvas.getBoundingClientRect();
|
|
2764
|
-
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
|
2765
|
-
if(panning){
|
|
2766
|
-
panX+=(mx-panStartX);panY+=(my-panStartY);
|
|
2767
|
-
panStartX=mx;panStartY=my;return;
|
|
2768
|
-
}
|
|
2769
|
-
if(dragIdx>=0){
|
|
2770
|
-
var wmx=(mx-panX)/zoom,wmy=(my-panY)/zoom;
|
|
2771
|
-
bubbles[dragIdx].x=wmx-dragOX;bubbles[dragIdx].y=wmy-dragOY;
|
|
2772
|
-
bubbles[dragIdx].vx=0;bubbles[dragIdx].vy=0;return;
|
|
2773
|
-
}
|
|
2774
|
-
var wmx2=(mx-panX)/zoom,wmy2=(my-panY)/zoom;
|
|
2775
|
-
hoveredIdx=-1;
|
|
2776
|
-
for(var i=bubbles.length-1;i>=0;i--){
|
|
2777
|
-
var b=bubbles[i];
|
|
2778
|
-
var dx=wmx2-b.x,dy=wmy2-b.y;
|
|
2779
|
-
if(Math.sqrt(dx*dx+dy*dy)<=b.r){hoveredIdx=i;break;}
|
|
2780
|
-
}
|
|
2781
|
-
canvas.style.cursor=panning?'move':hoveredIdx>=0?'grab':'default';
|
|
2782
|
-
});
|
|
2783
|
-
canvas.addEventListener('mousedown',function(e){
|
|
2784
|
-
var rect=canvas.getBoundingClientRect();
|
|
2785
|
-
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
|
2786
|
-
if(e.button===2){
|
|
2787
|
-
panning=true;panStartX=mx;panStartY=my;
|
|
2788
|
-
canvas.style.cursor='move';return;
|
|
2789
|
-
}
|
|
2790
|
-
if(hoveredIdx>=0){
|
|
2791
|
-
dragIdx=hoveredIdx;
|
|
2792
|
-
var wmx=(mx-panX)/zoom;
|
|
2793
|
-
var wmy=(my-panY)/zoom;
|
|
2794
|
-
dragOX=wmx-bubbles[dragIdx].x;
|
|
2795
|
-
dragOY=wmy-bubbles[dragIdx].y;
|
|
2796
|
-
canvas.style.cursor='grabbing';
|
|
2797
|
-
}
|
|
2798
|
-
});
|
|
2799
|
-
canvas.addEventListener('mouseup',function(){
|
|
2800
|
-
if(panning){panning=false;canvas.style.cursor='default';}
|
|
2801
|
-
if(dragIdx>=0){canvas.style.cursor='grab';dragIdx=-1;}
|
|
2802
|
-
});
|
|
2803
|
-
canvas.addEventListener('mouseleave',function(){hoveredIdx=-1;dragIdx=-1;panning=false;});
|
|
2804
|
-
canvas.addEventListener('dblclick',function(e){
|
|
2805
|
-
if(hoveredIdx<0)return;
|
|
2806
|
-
drillDownFile(bubbles[hoveredIdx].f.path);
|
|
2807
|
-
});
|
|
2808
|
-
// Zoom with mouse wheel
|
|
2809
|
-
var zoom=1.0,panX=0,panY=0;
|
|
2810
|
-
canvas.addEventListener('wheel',function(e){
|
|
2811
|
-
e.preventDefault();
|
|
2812
|
-
var rect=canvas.getBoundingClientRect();
|
|
2813
|
-
var mx=(e.clientX-rect.left-panX)/zoom;
|
|
2814
|
-
var my=(e.clientY-rect.top-panY)/zoom;
|
|
2815
|
-
var oldZoom=zoom;
|
|
2816
|
-
zoom*=e.deltaY<0?1.08:0.93;
|
|
2817
|
-
zoom=Math.max(0.5,Math.min(2.5,zoom));
|
|
2818
|
-
panX+=(mx*oldZoom-mx*zoom);
|
|
2819
|
-
panY+=(my*oldZoom-my*zoom);
|
|
2820
|
-
bubbles.forEach(function(b){});
|
|
2821
|
-
},{passive:false});
|
|
2822
|
-
bubbles.forEach(function(b){b._baseR=b.r;});
|
|
2823
|
-
}
|
|
2824
|
-
function drillDownFile(path){
|
|
2825
|
-
var dd=document.getElementById('metrics-drilldown');
|
|
2826
|
-
dd.style.display='block';
|
|
2827
|
-
dd.innerHTML='<div class="dev-panel" style="margin-bottom:1rem"><div class="dev-panel-header"><h2>'+path+'</h2><button class="btn btn-sm" onclick="document.getElementById('metrics-drilldown').style.display='none'">Close</button></div><div class="p-md"><p style="color:var(--muted)">Loading file analysis...</p></div></div>';
|
|
2828
|
-
fetch('/__dev/api/metrics/file?path='+encodeURIComponent(path)).then(function(r){return r.json()}).then(function(d){
|
|
2829
|
-
if(d.error){dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">'+d.error+'</p>';return;}
|
|
2830
|
-
var html='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem">';
|
|
2831
|
-
html+='<div class="sys-card"><div class="label">LOC</div><div class="value">'+d.loc+'</div></div>';
|
|
2832
|
-
html+='<div class="sys-card"><div class="label">Total Lines</div><div class="value">'+d.total_lines+'</div></div>';
|
|
2833
|
-
html+='<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>';
|
|
2834
|
-
html+='<div class="sys-card"><div class="label">Functions</div><div class="value">'+(d.functions?d.functions.length:0)+'</div></div>';
|
|
2835
|
-
html+='<div class="sys-card"><div class="label">Imports</div><div class="value">'+(d.imports?d.imports.length:0)+'</div></div>';
|
|
2836
|
-
html+='</div>';
|
|
2837
|
-
if(d.functions&&d.functions.length){
|
|
2838
|
-
html+='<h3 style="margin:0.5rem 0;color:var(--primary);font-size:0.85rem">Cyclomatic Complexity by Function</h3>';
|
|
2839
|
-
var maxCC=Math.max.apply(null,d.functions.map(function(f){return f.complexity}))||1;
|
|
2840
|
-
html+='<div style="display:flex;flex-direction:column;gap:4px">';
|
|
2841
|
-
d.functions.forEach(function(f){
|
|
2842
|
-
var pct=Math.max(3,f.complexity/maxCC*100);
|
|
2843
|
-
var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':f.complexity>5?'#3b82f6':'#22c55e';
|
|
2844
|
-
html+='<div style="display:flex;align-items:center;gap:8px;font-size:0.75rem;font-family:var(--mono)">';
|
|
2845
|
-
html+='<span style="width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="'+f.name+'">'+f.name+'</span>';
|
|
2846
|
-
html+='<div style="flex:1;height:16px;background:var(--bg);border-radius:3px;overflow:hidden;position:relative">';
|
|
2847
|
-
html+='<div style="width:'+pct+'%;height:100%;background:'+color+';border-radius:3px;transition:width 0.3s"></div>';
|
|
2848
|
-
html+='</div>';
|
|
2849
|
-
html+='<span style="width:70px;text-align:right;color:'+color+';font-weight:600">CC:'+f.complexity+'</span>';
|
|
2850
|
-
html+='<span style="width:60px;text-align:right;color:var(--muted)">'+f.loc+' LOC</span>';
|
|
2851
|
-
html+='<span style="width:30px;text-align:right;color:var(--muted)">L'+f.line+'</span>';
|
|
2852
|
-
html+='</div>';
|
|
2853
|
-
});
|
|
2854
|
-
html+='</div>';
|
|
2855
|
-
}
|
|
2856
|
-
if(d.imports&&d.imports.length){
|
|
2857
|
-
html+='<h3 style="margin:0.75rem 0 0.25rem;color:var(--primary);font-size:0.85rem">Dependencies</h3>';
|
|
2858
|
-
html+='<div style="display:flex;flex-wrap:wrap;gap:4px">';
|
|
2859
|
-
d.imports.forEach(function(imp){
|
|
2860
|
-
html+='<span style="padding:2px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.7rem;font-family:var(--mono)">'+imp+'</span>';
|
|
2861
|
-
});
|
|
2862
|
-
html+='</div>';
|
|
2863
|
-
}
|
|
2864
|
-
if(d.warnings&&d.warnings.length){
|
|
2865
|
-
html+='<h3 style="margin:0.75rem 0 0.25rem;color:#eab308;font-size:0.85rem">⚠ Warnings</h3>';
|
|
2866
|
-
html+='<div style="display:flex;flex-direction:column;gap:4px">';
|
|
2867
|
-
d.warnings.forEach(function(w){
|
|
2868
|
-
html+='<div style="padding:4px 8px;background:rgba(234,179,8,0.08);border-left:3px solid #eab308;border-radius:0 4px 4px 0;font-size:0.75rem;font-family:var(--mono);color:var(--text)">';
|
|
2869
|
-
html+='<span style="color:#eab308;margin-right:6px">L'+w.line+'</span>'+w.message+'</div>';
|
|
2870
|
-
});
|
|
2871
|
-
html+='</div>';
|
|
2872
|
-
}
|
|
2873
|
-
dd.querySelector('.p-md').innerHTML=html;
|
|
2874
|
-
}).catch(function(e){
|
|
2875
|
-
dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
|
|
2876
|
-
});
|
|
2877
|
-
dd.scrollIntoView({behavior:'smooth',block:'start'});
|
|
2878
|
-
}
|
|
2879
|
-
function loadAllMetrics(){
|
|
2880
|
-
if(window._metricsAnimFrame)cancelAnimationFrame(window._metricsAnimFrame);
|
|
2881
|
-
var el=document.getElementById('metrics-quick');
|
|
2882
|
-
el.innerHTML='<div class="sys-card"><div class="value">Loading...</div></div>';
|
|
2883
|
-
fetch('/__dev/api/metrics').then(function(r){return r.json()}).then(function(d){
|
|
2884
|
-
if(d.error){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">'+d.error+'</div></div>';return;}
|
|
2885
|
-
el.innerHTML=
|
|
2886
|
-
'<div class="sys-card"><div class="label">TS/JS Files</div><div class="value">'+d.file_count+'</div></div>'+
|
|
2887
|
-
'<div class="sys-card"><div class="label">Lines of Code</div><div class="value">'+d.total_loc.toLocaleString()+'</div></div>'+
|
|
2888
|
-
'<div class="sys-card"><div class="label">Comment Lines</div><div class="value">'+d.total_comment.toLocaleString()+'</div></div>'+
|
|
2889
|
-
'<div class="sys-card"><div class="label">Blank Lines</div><div class="value">'+d.total_blank.toLocaleString()+'</div></div>'+
|
|
2890
|
-
'<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>'+
|
|
2891
|
-
'<div class="sys-card"><div class="label">Functions</div><div class="value">'+d.functions+'</div></div>'+
|
|
2892
|
-
'<div class="sys-card"><div class="label">Routes</div><div class="value">'+d.route_count+'</div></div>'+
|
|
2893
|
-
'<div class="sys-card"><div class="label">ORM Models</div><div class="value">'+d.orm_count+'</div></div>'+
|
|
2894
|
-
'<div class="sys-card"><div class="label">Templates</div><div class="value">'+d.template_count+'</div></div>'+
|
|
2895
|
-
'<div class="sys-card"><div class="label">Migrations</div><div class="value">'+d.migration_count+'</div></div>';
|
|
2896
|
-
}).catch(function(e){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">Error: '+e.message+'</div></div>';});
|
|
2897
|
-
document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--muted);padding:1rem">Analyzing codebase...</p>';
|
|
2898
|
-
fetch('/__dev/api/metrics/full').then(function(r){return r.json()}).then(function(d){
|
|
2899
|
-
_metricsFullData=d;
|
|
2900
|
-
if(d.error){document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">'+d.error+'</p>';return;}
|
|
2901
|
-
renderBubbleChart(d.file_metrics,d.dependency_graph,d.scan_mode);
|
|
2902
|
-
var hm=document.getElementById('metrics-heatmap');
|
|
2903
|
-
var rows=d.file_metrics.map(function(f){
|
|
2904
|
-
var color=miColor(f.maintainability);
|
|
2905
|
-
var barW=Math.max(2,Math.min(100,f.maintainability));
|
|
2906
|
-
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.path+'')"><td><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:'+color+';margin-right:6px"></span>'+f.path+'</td><td>'+f.loc+'</td><td>'+f.complexity+'</td><td>'+f.avg_complexity+'</td><td><div style="display:flex;align-items:center;gap:6px"><div style="width:'+barW+'px;height:6px;border-radius:3px;background:'+color+'"></div><span>'+f.maintainability+'</span></div></td><td>'+f.instability+'</td></tr>';
|
|
2907
|
-
}).join('');
|
|
2908
|
-
hm.innerHTML='<table style="width:100%"><thead><tr><th>File</th><th>LOC</th><th>CC</th><th>Avg CC</th><th>MI</th><th>Instab.</th></tr></thead><tbody>'+rows+'</tbody></table>';
|
|
2909
|
-
var cf=document.getElementById('metrics-complex');
|
|
2910
|
-
var frows=d.most_complex_functions.map(function(f){
|
|
2911
|
-
var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':'#22c55e';
|
|
2912
|
-
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.file+'')"><td><span style="color:'+color+';font-weight:bold">'+f.complexity+'</span></td><td>'+f.name+'</td><td>'+f.file+':'+f.line+'</td><td>'+f.loc+'</td></tr>';
|
|
2913
|
-
}).join('');
|
|
2914
|
-
cf.innerHTML='<table style="width:100%"><thead><tr><th>CC</th><th>Function</th><th>File</th><th>LOC</th></tr></thead><tbody>'+frows+'</tbody></table>';
|
|
2915
|
-
var cp=document.getElementById('metrics-coupling');
|
|
2916
|
-
var crows=d.file_metrics.filter(function(f){return f.coupling_afferent>0||f.coupling_efferent>0}).map(function(f){
|
|
2917
|
-
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.path+'')"><td>'+f.path+'</td><td>'+f.coupling_afferent+'</td><td>'+f.coupling_efferent+'</td><td>'+f.instability+'</td></tr>';
|
|
2918
|
-
}).join('');
|
|
2919
|
-
cp.innerHTML=crows?'<table style="width:100%"><thead><tr><th>File</th><th>Ca (in)</th><th>Ce (out)</th><th>Instability</th></tr></thead><tbody>'+crows+'</tbody></table>':'<p style="color:var(--muted)">No coupling data</p>';
|
|
2920
|
-
var vl=document.getElementById('metrics-violations');
|
|
2921
|
-
if(d.violations&&d.violations.length){
|
|
2922
|
-
var vrows=d.violations.map(function(v){
|
|
2923
|
-
var icon=v.type==='error'?'⚠':'ⓘ';
|
|
2924
|
-
var color=v.type==='error'?'#ef4444':'#eab308';
|
|
2925
|
-
return '<tr style="cursor:pointer" onclick="drillDownFile(''+v.file+'')"><td style="color:'+color+'">'+icon+'</td><td>'+v.message+'</td><td>'+v.file+(v.line?':'+v.line:'')+'</td></tr>';
|
|
2926
|
-
}).join('');
|
|
2927
|
-
vl.innerHTML='<table style="width:100%"><thead><tr><th></th><th>Issue</th><th>Location</th></tr></thead><tbody>'+vrows+'</tbody></table>';
|
|
2928
|
-
}else{
|
|
2929
|
-
vl.innerHTML='<p style="color:#22c55e">✓ No violations found</p>';
|
|
2930
|
-
}
|
|
2931
|
-
}).catch(function(e){
|
|
2932
|
-
document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">Error: '+e.message+'</p>';
|
|
2933
|
-
});
|
|
2934
|
-
}
|
|
2935
|
-
var _metricsLoaded=false;
|
|
2936
|
-
var _origShowTab=typeof showTab==='function'?showTab:null;
|
|
2937
|
-
if(_origShowTab){
|
|
2938
|
-
showTab=function(name){
|
|
2939
|
-
_origShowTab(name);
|
|
2940
|
-
if(name==='metrics'&&!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}
|
|
2941
|
-
};
|
|
2942
|
-
}
|
|
2943
|
-
var metricsTab=document.querySelector('[onclick*="metrics"]');
|
|
2944
|
-
if(metricsTab)metricsTab.addEventListener('click',function(){if(!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}});
|
|
2945
|
-
</script>
|
|
2946
|
-
<script>
|
|
2947
|
-
// Self-diagnostic — detect if the external JS failed to load
|
|
2948
|
-
(function() {
|
|
2949
|
-
if (typeof showTab !== 'function') {
|
|
2950
|
-
var banner = document.createElement('div');
|
|
2951
|
-
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:99999;background:#ef4444;color:#fff;padding:0.75rem 1rem;font-family:system-ui;font-size:0.85rem;text-align:center';
|
|
2952
|
-
banner.innerHTML = '<strong>Dev Admin Error:</strong> tina4-dev-admin.min.js failed to load.';
|
|
2953
|
-
document.body.insertBefore(banner, document.body.firstChild);
|
|
2954
|
-
}
|
|
2955
|
-
})();
|
|
2956
|
-
</script>
|
|
2957
|
-
</body>
|
|
2958
|
-
</html>`;
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
1371
|
// ---------------------------------------------------------------------------
|
|
2962
1372
|
// Overlay script — floating Tina4 button
|
|
2963
1373
|
// ---------------------------------------------------------------------------
|