@zintrust/trace 0.4.75
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/README.md +288 -0
- package/dist/build-manifest.json +365 -0
- package/dist/cli-register.d.ts +9 -0
- package/dist/cli-register.js +32 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +38 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.js +86 -0
- package/dist/dashboard/handlers.d.ts +15 -0
- package/dist/dashboard/handlers.js +179 -0
- package/dist/dashboard/routes.d.ts +19 -0
- package/dist/dashboard/routes.js +50 -0
- package/dist/dashboard/ui.d.ts +2 -0
- package/dist/dashboard/ui.js +870 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +50 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.js +4 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +3 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +140 -0
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/TraceStorage.d.ts +13 -0
- package/dist/storage/TraceStorage.js +195 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +25 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +7 -0
- package/dist/utils/authTag.d.ts +5 -0
- package/dist/utils/authTag.js +18 -0
- package/dist/utils/familyHash.d.ts +1 -0
- package/dist/utils/familyHash.js +8 -0
- package/dist/utils/redact.d.ts +6 -0
- package/dist/utils/redact.js +49 -0
- package/dist/utils/requestFilter.d.ts +4 -0
- package/dist/utils/requestFilter.js +26 -0
- package/dist/utils/stackFrame.d.ts +6 -0
- package/dist/utils/stackFrame.js +38 -0
- package/dist/watchers/AuthWatcher.d.ts +6 -0
- package/dist/watchers/AuthWatcher.js +49 -0
- package/dist/watchers/BatchWatcher.d.ts +6 -0
- package/dist/watchers/BatchWatcher.js +46 -0
- package/dist/watchers/CacheWatcher.d.ts +6 -0
- package/dist/watchers/CacheWatcher.js +51 -0
- package/dist/watchers/CommandWatcher.d.ts +6 -0
- package/dist/watchers/CommandWatcher.js +49 -0
- package/dist/watchers/DumpWatcher.d.ts +7 -0
- package/dist/watchers/DumpWatcher.js +41 -0
- package/dist/watchers/EventWatcher.d.ts +6 -0
- package/dist/watchers/EventWatcher.js +42 -0
- package/dist/watchers/ExceptionWatcher.d.ts +4 -0
- package/dist/watchers/ExceptionWatcher.js +103 -0
- package/dist/watchers/GateWatcher.d.ts +6 -0
- package/dist/watchers/GateWatcher.js +45 -0
- package/dist/watchers/HttpClientWatcher.d.ts +6 -0
- package/dist/watchers/HttpClientWatcher.js +50 -0
- package/dist/watchers/HttpWatcher.d.ts +2 -0
- package/dist/watchers/HttpWatcher.js +71 -0
- package/dist/watchers/JobWatcher.d.ts +10 -0
- package/dist/watchers/JobWatcher.js +108 -0
- package/dist/watchers/LogWatcher.d.ts +2 -0
- package/dist/watchers/LogWatcher.js +50 -0
- package/dist/watchers/MailWatcher.d.ts +6 -0
- package/dist/watchers/MailWatcher.js +45 -0
- package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
- package/dist/watchers/MiddlewareWatcher.js +41 -0
- package/dist/watchers/ModelWatcher.d.ts +6 -0
- package/dist/watchers/ModelWatcher.js +42 -0
- package/dist/watchers/NotificationWatcher.d.ts +6 -0
- package/dist/watchers/NotificationWatcher.js +42 -0
- package/dist/watchers/QueryWatcher.d.ts +2 -0
- package/dist/watchers/QueryWatcher.js +72 -0
- package/dist/watchers/RedisWatcher.d.ts +7 -0
- package/dist/watchers/RedisWatcher.js +38 -0
- package/dist/watchers/ScheduleWatcher.d.ts +6 -0
- package/dist/watchers/ScheduleWatcher.js +46 -0
- package/dist/watchers/ViewWatcher.d.ts +6 -0
- package/dist/watchers/ViewWatcher.js +36 -0
- package/package.json +59 -0
- package/src/cli-register.ts +63 -0
- package/src/config.ts +46 -0
- package/src/context.ts +101 -0
- package/src/dashboard/handlers.ts +197 -0
- package/src/dashboard/routes.ts +101 -0
- package/src/dashboard/ui.ts +879 -0
- package/src/dashboard/zintrust-debuger.svg +30 -0
- package/src/index.ts +88 -0
- package/src/plugin.ts +9 -0
- package/src/register.ts +219 -0
- package/src/storage/TraceStorage.ts +306 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +317 -0
- package/src/ui.ts +9 -0
- package/src/utils/authTag.ts +20 -0
- package/src/utils/familyHash.ts +8 -0
- package/src/utils/redact.ts +64 -0
- package/src/utils/requestFilter.ts +33 -0
- package/src/utils/stackFrame.ts +44 -0
- package/src/watchers/AuthWatcher.ts +50 -0
- package/src/watchers/BatchWatcher.ts +52 -0
- package/src/watchers/CacheWatcher.ts +58 -0
- package/src/watchers/CommandWatcher.ts +55 -0
- package/src/watchers/DumpWatcher.ts +42 -0
- package/src/watchers/EventWatcher.ts +43 -0
- package/src/watchers/ExceptionWatcher.ts +114 -0
- package/src/watchers/GateWatcher.ts +50 -0
- package/src/watchers/HttpClientWatcher.ts +56 -0
- package/src/watchers/HttpWatcher.ts +94 -0
- package/src/watchers/JobWatcher.ts +121 -0
- package/src/watchers/LogWatcher.ts +61 -0
- package/src/watchers/MailWatcher.ts +47 -0
- package/src/watchers/MiddlewareWatcher.ts +42 -0
- package/src/watchers/ModelWatcher.ts +48 -0
- package/src/watchers/NotificationWatcher.ts +43 -0
- package/src/watchers/QueryWatcher.ts +85 -0
- package/src/watchers/RedisWatcher.ts +39 -0
- package/src/watchers/ScheduleWatcher.ts +54 -0
- package/src/watchers/ViewWatcher.ts +37 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace dashboard SPA — inline HTML served at basePath.
|
|
3
|
+
* Full REST API registered under basePath/api/*.
|
|
4
|
+
*/
|
|
5
|
+
const BRAND_SVG = `<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="zt-trace-brand" x1="15" y1="15" x2="85" y2="85" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#38bdf8" />
|
|
9
|
+
<stop offset="1" stop-color="#22c55e" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<path
|
|
13
|
+
d="M50 8L18 22V46C18 66.2 32 84.1 50 92C68 84.1 82 66.2 82 46V22L50 8Z"
|
|
14
|
+
stroke="url(#zt-trace-brand)"
|
|
15
|
+
stroke-width="6"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
d="M34 54H42L46 44L52 62L58 50H66"
|
|
20
|
+
stroke="white"
|
|
21
|
+
stroke-width="8"
|
|
22
|
+
stroke-linecap="round"
|
|
23
|
+
stroke-linejoin="round"
|
|
24
|
+
/>
|
|
25
|
+
<circle cx="34" cy="54" r="2.8" fill="white" fill-opacity="0.7" />
|
|
26
|
+
<circle cx="66" cy="50" r="2.8" fill="white" fill-opacity="0.7" />
|
|
27
|
+
<path
|
|
28
|
+
d="M30 28H70"
|
|
29
|
+
stroke="white"
|
|
30
|
+
stroke-opacity="0.12"
|
|
31
|
+
stroke-width="3"
|
|
32
|
+
stroke-linecap="round"
|
|
33
|
+
/>
|
|
34
|
+
</svg>`;
|
|
35
|
+
const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.2M12 19.8V22M4.93 4.93l1.56 1.56M17.51 17.51l1.56 1.56M2 12h2.2M19.8 12H22M4.93 19.07l1.56-1.56M17.51 6.49l1.56-1.56"></path></svg>`;
|
|
36
|
+
const MOON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>`;
|
|
37
|
+
const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
38
|
+
const JSON_HIGHLIGHT_PATTERN = String.raw `("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(?=\s*:)|(\s*:)|("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?`;
|
|
39
|
+
const SQL_HIGHLIGHT_PATTERN = String.raw `(\/\*[\s\S]*?\*\/|--.*$|'(?:''|[^'])*'|\x60[^\x60]+\x60|\b(?:select|from|where|insert|into|values|update|delete|join|left|right|inner|outer|on|and|or|limit|order|by|group|having|as|distinct|null|is|in|like|set|case|when|then|else|end|returning|union|all)\b|-?\d+(?:\.\d+)?)`;
|
|
40
|
+
const encodeSvgDataUri = (svg) => {
|
|
41
|
+
const compactSvg = svg.replaceAll(/>\s+</g, '><').trim();
|
|
42
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(compactSvg)}`;
|
|
43
|
+
};
|
|
44
|
+
const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="UTF-8">
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
49
|
+
<meta name="color-scheme" content="dark light">
|
|
50
|
+
<title>__TRACE_TITLE__</title>
|
|
51
|
+
<link rel="icon" type="image/svg+xml" href="__TRACE_FAVICON__">
|
|
52
|
+
<script>
|
|
53
|
+
(function(){
|
|
54
|
+
const KEY = 'zintrust-trace-theme';
|
|
55
|
+
let theme = 'dark';
|
|
56
|
+
try {
|
|
57
|
+
const stored = window.localStorage.getItem(KEY);
|
|
58
|
+
if (stored === 'light' || stored === 'dark') theme = stored;
|
|
59
|
+
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) theme = 'light';
|
|
60
|
+
} catch {}
|
|
61
|
+
document.documentElement.dataset.theme = theme;
|
|
62
|
+
})();
|
|
63
|
+
</script>
|
|
64
|
+
<style>
|
|
65
|
+
:root{--bg:#0b1220;--surface:rgba(15,23,42,.82);--surface-strong:#13233b;--surface-soft:rgba(15,23,42,.56);--line:rgba(148,163,184,.18);--text:#e5edf8;--muted:#94a3b8;--accent:#38bdf8;--accent-strong:#0ea5e9;--success:#22c55e;--danger:#ef4444;--warn:#f59e0b;--code-bg:#06101f;--code-border:rgba(56,189,248,.14);--shadow:0 24px 70px rgba(2,8,23,.35);--font:'Inter',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;--mono:'SF Mono',SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;--radius:18px}
|
|
66
|
+
html[data-theme='light']{--bg:#f4f6fb;--surface:rgba(255,255,255,.94);--surface-strong:#ffffff;--surface-soft:rgba(255,255,255,.8);--line:#dde4ef;--text:#172033;--muted:#6d7890;--accent:#2563eb;--accent-strong:#1d4ed8;--success:#16a34a;--danger:#dc2626;--warn:#b45309;--code-bg:#f7fbff;--code-border:#d7e1ee;--shadow:0 20px 60px rgba(15,23,42,.08)}
|
|
67
|
+
*{box-sizing:border-box}html,body{margin:0;min-height:100%}body{min-height:100vh;background:linear-gradient(180deg,rgba(56,189,248,.1),transparent 220px),var(--bg);font-family:var(--font);color:var(--text)}button,input,select{font:inherit}
|
|
68
|
+
.layout{display:grid;grid-template-columns:300px minmax(0,1fr);min-height:100vh}.sidebar{padding:26px 18px 22px;background:var(--surface-soft);backdrop-filter:blur(14px);border-right:1px solid var(--line);position:sticky;top:0;height:100vh;overflow:auto;z-index:2}.brand-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:4px 10px 22px}.brand{display:flex;align-items:center;gap:12px;min-width:0}.brand-mark{width:44px;height:44px;border-radius:14px;border:1px solid rgba(56,189,248,.22);background:linear-gradient(180deg,rgba(56,189,248,.16),rgba(34,197,94,.12));display:grid;place-items:center;flex:none}.brand-mark svg{width:28px;height:28px;display:block}.brand-copy h1{margin:0;font-size:1.42rem;line-height:1.08}.brand-copy p{margin:4px 0 0;color:var(--muted);font-size:.92rem}.sidebar-status{margin:0 10px 18px;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:var(--surface);color:var(--muted);line-height:1.5}.sidebar-status strong{display:block;color:var(--text);font-size:.95rem;margin-bottom:4px}.sidebar-group{padding:0 8px;margin-top:8px}.sidebar-label{margin:0 0 10px;font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}.nav-button{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;border:none;border-radius:14px;padding:12px 14px;background:transparent;color:var(--muted);cursor:pointer;transition:background .16s ease,color .16s ease,box-shadow .16s ease;position:relative;z-index:1}.nav-button:hover,.nav-button:focus-visible{background:rgba(56,189,248,.1);color:var(--text);outline:none}.nav-button.active{background:rgba(56,189,248,.14);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.22)}.nav-button+.nav-button{margin-top:6px}.nav-title{font-weight:700}.nav-meta{font-size:.8rem;opacity:.75}.icon-button{width:42px;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--surface);color:var(--text);display:grid;place-items:center;cursor:pointer;transition:border-color .16s ease,transform .16s ease,color .16s ease}.icon-button:hover,.icon-button:focus-visible{border-color:rgba(56,189,248,.4);color:var(--accent);transform:translateY(-1px);outline:none}.icon-button svg{width:18px;height:18px;display:block}
|
|
69
|
+
.main{padding:28px;min-width:0}.shell{max-width:1320px;margin:0 auto;display:grid;gap:18px}
|
|
70
|
+
.panel{border-radius:var(--radius);border:1px solid var(--line);background:var(--surface);box-shadow:var(--shadow);backdrop-filter:blur(16px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin-bottom:18px}.stat-card{padding:20px;position:relative;overflow:hidden}.stat-card::after{content:'';position:absolute;right:-18px;bottom:-26px;width:92px;height:92px;border-radius:28px;background:linear-gradient(135deg,rgba(56,189,248,.16),rgba(34,197,94,.08));transform:rotate(18deg)}.stat-label{font-size:.74rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800;margin-bottom:12px}.stat-value{font-size:2.25rem;font-weight:800;line-height:1}.stat-meta{margin-top:10px;color:var(--muted);font-size:.9rem}.content-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(320px,.95fr);gap:18px}.side-stack{display:grid;gap:18px}
|
|
71
|
+
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
|
|
72
|
+
.table-wrap{overflow:auto;padding:0 12px 12px}table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
|
|
73
|
+
.activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}
|
|
74
|
+
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18)}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
75
|
+
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
76
|
+
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
77
|
+
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
78
|
+
@media (max-width:1120px){.content-grid{grid-template-columns:1fr}}@media (max-width:920px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line);padding:20px 16px 18px}.brand-row{padding:0 0 16px}.sidebar-status{margin:0 0 16px}.sidebar-group{padding:0}.main{padding:20px}}@media (max-width:640px){.stats-grid{grid-template-columns:1fr}.detail-card{padding:18px}.toolbar,.section-head,.pagination,.activity-list,.monitoring-wrap{padding-left:18px;padding-right:18px}.table-wrap{padding:0 8px 10px}.brand-row{align-items:stretch;gap:14px;padding:0 0 14px}.brand{width:100%;align-items:flex-start}.brand-copy{min-width:0}.brand-copy h1{font-size:1.18rem;line-height:1.12}.brand-copy p{font-size:.82rem;overflow-wrap:anywhere}.icon-button{align-self:flex-end}.sidebar-status{padding:12px}.nav-button{padding:11px 12px}.nav-title{font-size:.95rem}.nav-meta{font-size:.72rem}}@media (max-width:480px){.brand-row{flex-direction:column}.icon-button{align-self:flex-start}.nav-button{align-items:flex-start;flex-direction:column}.nav-meta{font-size:.7rem}}
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<div class="layout">
|
|
83
|
+
<aside class="sidebar">
|
|
84
|
+
<div class="brand-row">
|
|
85
|
+
<div class="brand">
|
|
86
|
+
<div class="brand-mark">__TRACE_LOGO__</div>
|
|
87
|
+
<div class="brand-copy">
|
|
88
|
+
<h1>ZinTrust Trace</h1>
|
|
89
|
+
<p class="mono">__TRACE_PROJECT_NAME__</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<button type="button" class="icon-button" id="theme-toggle" aria-label="Toggle theme"></button>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="sidebar-status">
|
|
95
|
+
<strong id="page-title">Runtime overview</strong>
|
|
96
|
+
<span id="page-subtitle">Recent trace activity and trace filters.</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="sidebar-group">
|
|
99
|
+
<p class="sidebar-label">Navigation</p>
|
|
100
|
+
<button type="button" class="nav-button active" data-page="overview"><span class="nav-title">Overview</span><span class="nav-meta">Summary</span></button>
|
|
101
|
+
<button type="button" class="nav-button" data-page="entries"><span class="nav-title">Entries</span><span class="nav-meta">Events</span></button>
|
|
102
|
+
<button type="button" class="nav-button" data-page="monitoring"><span class="nav-title">Monitoring</span><span class="nav-meta">Tags</span></button>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="sidebar-group">
|
|
105
|
+
<p class="sidebar-label">Streams</p>
|
|
106
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="request"><span class="nav-title">Requests</span><span class="nav-meta">HTTP</span></button>
|
|
107
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="query"><span class="nav-title">Queries</span><span class="nav-meta">SQL</span></button>
|
|
108
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="job"><span class="nav-title">Jobs</span><span class="nav-meta">Queue</span></button>
|
|
109
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="exception"><span class="nav-title">Exceptions</span><span class="nav-meta">Errors</span></button>
|
|
110
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="log"><span class="nav-title">Logs</span><span class="nav-meta">App</span></button>
|
|
111
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="cache"><span class="nav-title">Cache</span><span class="nav-meta">Store</span></button>
|
|
112
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="client_request"><span class="nav-title">Http Client</span><span class="nav-meta">Outbound</span></button>
|
|
113
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="redis"><span class="nav-title">Redis</span><span class="nav-meta">Command</span></button>
|
|
114
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="notification"><span class="nav-title">Notifications</span><span class="nav-meta">Alerts</span></button>
|
|
115
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="model"><span class="nav-title">Models</span><span class="nav-meta">ORM</span></button>
|
|
116
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="schedule"><span class="nav-title">Schedule</span><span class="nav-meta">Tasks</span></button>
|
|
117
|
+
<button type="button" class="nav-button" data-action="type-shortcut" data-type="mail"><span class="nav-title">Mail</span><span class="nav-meta">Outbound</span></button>
|
|
118
|
+
</div>
|
|
119
|
+
</aside>
|
|
120
|
+
<main class="main">
|
|
121
|
+
<div class="shell">
|
|
122
|
+
<div id="main"></div>
|
|
123
|
+
</div>
|
|
124
|
+
</main>
|
|
125
|
+
</div>
|
|
126
|
+
<script>
|
|
127
|
+
(function(){
|
|
128
|
+
const BASE = __TRACE_BASE_PATH_JSON__;
|
|
129
|
+
const API = BASE + '/api';
|
|
130
|
+
const THEME_KEY = 'zintrust-trace-theme';
|
|
131
|
+
const SUN_ICON = __TRACE_SUN_ICON__;
|
|
132
|
+
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
133
|
+
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
134
|
+
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
135
|
+
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
136
|
+
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
137
|
+
const PAGE_COPY = {
|
|
138
|
+
overview: { title: 'Runtime overview', subtitle: 'Recent trace activity and trace filters.' },
|
|
139
|
+
entries: { title: 'Entry explorer', subtitle: 'Filter by type, tag, or batch.' },
|
|
140
|
+
monitoring: { title: 'Monitoring tags', subtitle: 'Pinned tags for trace pivots.' }
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
let state = {
|
|
144
|
+
page: 'overview',
|
|
145
|
+
entriesPage: 1,
|
|
146
|
+
entriesFilter: { type: '', tag: '', batchId: '' },
|
|
147
|
+
detail: null,
|
|
148
|
+
detailBatch: null,
|
|
149
|
+
detailTab: 'summary'
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let copySequence = 0;
|
|
153
|
+
const copyPayloads = new Map();
|
|
154
|
+
|
|
155
|
+
const updateThemeButton = () => {
|
|
156
|
+
const theme = document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
|
|
157
|
+
const toggle = document.getElementById('theme-toggle');
|
|
158
|
+
if (toggle) {
|
|
159
|
+
toggle.innerHTML = theme === 'dark' ? SUN_ICON : MOON_ICON;
|
|
160
|
+
toggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const setTheme = (theme) => {
|
|
165
|
+
document.documentElement.dataset.theme = theme;
|
|
166
|
+
try { window.localStorage.setItem(THEME_KEY, theme); } catch {}
|
|
167
|
+
updateThemeButton();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const setPageCopy = (page) => {
|
|
171
|
+
const copy = PAGE_COPY[page] || PAGE_COPY.overview;
|
|
172
|
+
const title = document.getElementById('page-title');
|
|
173
|
+
const subtitle = document.getElementById('page-subtitle');
|
|
174
|
+
if (title) title.textContent = copy.title;
|
|
175
|
+
if (subtitle) subtitle.textContent = copy.subtitle;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const activeEntryShortcut = () => state.page === 'entries' && state.entriesFilter.type !== '' ? state.entriesFilter.type : '';
|
|
179
|
+
|
|
180
|
+
const escapeHtml = (value) => String(value ?? '')
|
|
181
|
+
.replace(/&/g, '&')
|
|
182
|
+
.replace(/</g, '<')
|
|
183
|
+
.replace(/>/g, '>')
|
|
184
|
+
.replace(/"/g, '"')
|
|
185
|
+
.replace(/'/g, ''');
|
|
186
|
+
|
|
187
|
+
const api = async (path, opts) => {
|
|
188
|
+
const response = await fetch(API + path, opts);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
191
|
+
throw new Error(errorBody.error || response.statusText);
|
|
192
|
+
}
|
|
193
|
+
return response.json();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const typeClass = (type) => 'type-pill pill-' + String(type || '').replace(/_/g, '-');
|
|
197
|
+
|
|
198
|
+
const timeSince = (value) => {
|
|
199
|
+
const createdAt = Number(value);
|
|
200
|
+
if (!Number.isFinite(createdAt)) return 'Unknown';
|
|
201
|
+
const seconds = Math.max(0, Math.floor((Date.now() - createdAt) / 1000));
|
|
202
|
+
if (seconds < 60) return seconds + 's ago';
|
|
203
|
+
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
|
204
|
+
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
|
205
|
+
return Math.floor(seconds / 86400) + 'd ago';
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const formatDuration = (value) => {
|
|
209
|
+
const duration = Number(value);
|
|
210
|
+
if (!Number.isFinite(duration) || duration < 0) return '-';
|
|
211
|
+
if (duration === 0) return '0 ms';
|
|
212
|
+
if (duration < 10) {
|
|
213
|
+
const fixed = duration.toFixed(2);
|
|
214
|
+
const compact = fixed.endsWith('.00') ? fixed.slice(0, -3) : fixed.endsWith('0') ? fixed.slice(0, -1) : fixed;
|
|
215
|
+
return compact + ' ms';
|
|
216
|
+
}
|
|
217
|
+
if (duration < 100) {
|
|
218
|
+
const fixed = duration.toFixed(1);
|
|
219
|
+
return (fixed.endsWith('.0') ? fixed.slice(0, -2) : fixed) + ' ms';
|
|
220
|
+
}
|
|
221
|
+
return Math.round(duration) + ' ms';
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const getEntryDuration = (entry) => {
|
|
225
|
+
const content = entry && entry.content ? entry.content : {};
|
|
226
|
+
const primary = Number(content.duration);
|
|
227
|
+
if (Number.isFinite(primary)) return primary;
|
|
228
|
+
const fallback = Number(content.time);
|
|
229
|
+
return Number.isFinite(fallback) ? fallback : null;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const getDurationTone = (duration) => {
|
|
233
|
+
if (!Number.isFinite(duration) || duration < 0) return '';
|
|
234
|
+
if (duration < 25) return 'vfast';
|
|
235
|
+
if (duration < 150) return 'fast';
|
|
236
|
+
if (duration < 600) return 'slow';
|
|
237
|
+
return 'vslow';
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const durationHtml = (entry) => {
|
|
241
|
+
const duration = getEntryDuration(entry);
|
|
242
|
+
if (duration === null) return '<span class="activity-time">-</span>';
|
|
243
|
+
const tone = getDurationTone(duration);
|
|
244
|
+
const toneLabel = tone === 'vfast' ? 'VFast' : tone === 'fast' ? 'Fast' : tone === 'slow' ? 'Slow' : 'VSlow';
|
|
245
|
+
return '<span class="duration-chip ' + tone + '" title="' + toneLabel + '">' + escapeHtml(formatDuration(duration)) + '</span>';
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const tagsHtml = (tags) => (tags || []).map((tag) => {
|
|
249
|
+
const css = tag === 'failed' ? 'tag failed' : tag === 'slow' ? 'tag slow' : 'tag';
|
|
250
|
+
return '<button type="button" class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button>';
|
|
251
|
+
}).join('');
|
|
252
|
+
|
|
253
|
+
const batchSnippet = (batchId) => {
|
|
254
|
+
const raw = String(batchId || '');
|
|
255
|
+
return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const batchEntries = () => Array.isArray(state.detailBatch) ? state.detailBatch : [];
|
|
259
|
+
const batchEntriesByType = (type) => batchEntries().filter((entry) => entry.type === type);
|
|
260
|
+
const hasRequestTrace = () => Boolean(state.detail && state.detail.type === 'request' && batchEntries().length > 0);
|
|
261
|
+
|
|
262
|
+
const prettyJson = (value) => {
|
|
263
|
+
try {
|
|
264
|
+
return JSON.stringify(value ?? {}, null, 2) ?? '{}';
|
|
265
|
+
} catch {
|
|
266
|
+
return String(value ?? '');
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const registerCopyPayload = (text) => {
|
|
271
|
+
const id = 'copy-' + (++copySequence);
|
|
272
|
+
copyPayloads.set(id, text);
|
|
273
|
+
return id;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const renderCodeCard = (label, rawText, highlightedHtml, languageClass) => {
|
|
277
|
+
const copyId = registerCopyPayload(rawText);
|
|
278
|
+
return [
|
|
279
|
+
'<section class="code-card">',
|
|
280
|
+
'<div class="code-toolbar">',
|
|
281
|
+
'<span class="code-label">' + escapeHtml(label) + '</span>',
|
|
282
|
+
'<button type="button" class="copy-button" data-action="copy-code" data-copy-id="' + escapeHtml(copyId) + '" title="Copy ' + escapeHtml(label) + '">',
|
|
283
|
+
COPY_ICON,
|
|
284
|
+
'</button>',
|
|
285
|
+
'</div>',
|
|
286
|
+
'<pre class="code-block ' + escapeHtml(languageClass) + '"><code>' + highlightedHtml + '</code></pre>',
|
|
287
|
+
'</section>'
|
|
288
|
+
].join('');
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const highlightJson = (value) => {
|
|
292
|
+
const source = prettyJson(value);
|
|
293
|
+
let output = '';
|
|
294
|
+
let lastIndex = 0;
|
|
295
|
+
|
|
296
|
+
for (const match of source.matchAll(JSON_HIGHLIGHT_PATTERN)) {
|
|
297
|
+
const index = match.index ?? 0;
|
|
298
|
+
output += escapeHtml(source.slice(lastIndex, index));
|
|
299
|
+
const token = match[0];
|
|
300
|
+
if (match[1]) output += '<span class="tok-key">' + escapeHtml(match[1]) + '</span>';
|
|
301
|
+
else if (match[2]) output += '<span class="tok-punctuation">' + escapeHtml(match[2]) + '</span>';
|
|
302
|
+
else if (token === 'true' || token === 'false') output += '<span class="tok-boolean">' + token + '</span>';
|
|
303
|
+
else if (token === 'null') output += '<span class="tok-null">null</span>';
|
|
304
|
+
else if (/^"/.test(token)) output += '<span class="tok-string">' + escapeHtml(token) + '</span>';
|
|
305
|
+
else output += '<span class="tok-number">' + escapeHtml(token) + '</span>';
|
|
306
|
+
lastIndex = index + token.length;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
output += escapeHtml(source.slice(lastIndex));
|
|
310
|
+
return renderCodeCard('JSON', source, output, 'language-json');
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const highlightSql = (sql) => {
|
|
314
|
+
const source = String(sql || '');
|
|
315
|
+
let output = '';
|
|
316
|
+
let lastIndex = 0;
|
|
317
|
+
|
|
318
|
+
for (const match of source.matchAll(SQL_HIGHLIGHT_PATTERN)) {
|
|
319
|
+
const index = match.index ?? 0;
|
|
320
|
+
const token = match[0];
|
|
321
|
+
output += escapeHtml(source.slice(lastIndex, index));
|
|
322
|
+
|
|
323
|
+
if (token.startsWith('/*') || token.startsWith('--')) output += '<span class="tok-sql-comment">' + escapeHtml(token) + '</span>';
|
|
324
|
+
else if (token.startsWith("'")) output += '<span class="tok-sql-string">' + escapeHtml(token) + '</span>';
|
|
325
|
+
else if (token.charCodeAt(0) === 96) output += '<span class="tok-sql-identifier">' + escapeHtml(token) + '</span>';
|
|
326
|
+
else {
|
|
327
|
+
const numericIndex = token.startsWith('-') ? 1 : 0;
|
|
328
|
+
const numericChar = token.charAt(numericIndex);
|
|
329
|
+
if (numericChar >= '0' && numericChar <= '9') output += '<span class="tok-sql-number">' + escapeHtml(token) + '</span>';
|
|
330
|
+
else output += '<span class="tok-sql-keyword">' + escapeHtml(token) + '</span>';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
lastIndex = index + token.length;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
output += escapeHtml(source.slice(lastIndex));
|
|
337
|
+
return renderCodeCard('SQL', source, output, 'language-sql');
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const detailJson = (value) => highlightJson(value ?? {});
|
|
341
|
+
|
|
342
|
+
const entrySummaryText = (entry) => {
|
|
343
|
+
const content = entry && entry.content ? entry.content : {};
|
|
344
|
+
if (entry.type === 'request') return [content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
345
|
+
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
346
|
+
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
347
|
+
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
348
|
+
if (entry.type === 'job') return [content.name || '', content.status || 'queued'].filter(Boolean).join(' · ');
|
|
349
|
+
if (entry.type === 'cache') return [content.operation || '', content.key || ''].filter(Boolean).join(' ');
|
|
350
|
+
if (entry.type === 'schedule') return [content.name || '', content.status || 'ran'].filter(Boolean).join(' · ');
|
|
351
|
+
if (entry.type === 'mail') return ['To ' + (content.to || 'unknown'), content.subject || 'No subject'].join(' · ');
|
|
352
|
+
if (entry.type === 'auth') return [content.event || 'auth', content.userId ? '#' + content.userId : ''].filter(Boolean).join(' ');
|
|
353
|
+
if (entry.type === 'event') return String(content.name || 'event');
|
|
354
|
+
if (entry.type === 'model') return [content.action || '', content.model || ''].filter(Boolean).join(' ');
|
|
355
|
+
if (entry.type === 'notification') return [content.notification || '', (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
|
|
356
|
+
if (entry.type === 'redis') return String(content.command || 'redis');
|
|
357
|
+
if (entry.type === 'gate') return [content.ability || '', content.result || ''].filter(Boolean).join(' · ');
|
|
358
|
+
if (entry.type === 'middleware') return [content.name || '', content.event || ''].filter(Boolean).join(' · ');
|
|
359
|
+
if (entry.type === 'command') return [content.name || '', content.exitCode !== undefined ? 'exit=' + content.exitCode : ''].filter(Boolean).join(' ');
|
|
360
|
+
if (entry.type === 'batch') return [content.name || '', 'processed ' + (content.processed || 0) + '/' + (content.total || 0)].join(' · ');
|
|
361
|
+
if (entry.type === 'view') return String(content.template || 'view');
|
|
362
|
+
if (entry.type === 'client_request') return [content.method || '', content.url || ''].filter(Boolean).join(' ');
|
|
363
|
+
return JSON.stringify(content).slice(0, 160);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const entrySummaryHtml = (entry) => {
|
|
367
|
+
const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
|
|
368
|
+
const secondary = [
|
|
369
|
+
entry.type === 'request' ? 'Incoming request' : '',
|
|
370
|
+
entry.type === 'query' ? 'Database query' : '',
|
|
371
|
+
entry.type === 'exception' ? 'Unhandled error' : '',
|
|
372
|
+
entry.type === 'client_request' ? 'Outbound HTTP call' : ''
|
|
373
|
+
].find(Boolean) || 'Trace record';
|
|
374
|
+
return '<div class="summary">' + summary + '</div><div class="summary-sub">' + escapeHtml(secondary) + '</div>';
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const renderMetricBox = (title, items) => {
|
|
378
|
+
return [
|
|
379
|
+
'<section class="detail-box">',
|
|
380
|
+
'<h4>' + escapeHtml(title) + '</h4>',
|
|
381
|
+
'<dl>',
|
|
382
|
+
items.map((item) => '<dt>' + escapeHtml(item.label) + '</dt><dd>' + item.value + '</dd>').join(''),
|
|
383
|
+
'</dl>',
|
|
384
|
+
'</section>'
|
|
385
|
+
].join('');
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const renderEntryBody = (entry) => {
|
|
389
|
+
const content = entry && entry.content ? entry.content : {};
|
|
390
|
+
|
|
391
|
+
if (entry.type === 'query') {
|
|
392
|
+
return [
|
|
393
|
+
'<div class="detail-grid">',
|
|
394
|
+
renderMetricBox('Query', [
|
|
395
|
+
{ label: 'Connection', value: escapeHtml(content.connection || 'default') },
|
|
396
|
+
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) },
|
|
397
|
+
{ label: 'Slow', value: escapeHtml(content.slow ? 'Yes' : 'No') },
|
|
398
|
+
{ label: 'Hash', value: '<span class="mono">' + escapeHtml(content.hash || '') + '</span>' }
|
|
399
|
+
]),
|
|
400
|
+
renderMetricBox('Runtime', [
|
|
401
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
402
|
+
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
403
|
+
]),
|
|
404
|
+
'</div>',
|
|
405
|
+
highlightSql(content.sql || '')
|
|
406
|
+
].join('');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (entry.type === 'log') {
|
|
410
|
+
return [
|
|
411
|
+
'<div class="detail-grid">',
|
|
412
|
+
renderMetricBox('Log', [
|
|
413
|
+
{ label: 'Level', value: escapeHtml(content.level || '') },
|
|
414
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
415
|
+
]),
|
|
416
|
+
renderMetricBox('Message', [
|
|
417
|
+
{ label: 'Text', value: escapeHtml(content.message || '') }
|
|
418
|
+
]),
|
|
419
|
+
'</div>',
|
|
420
|
+
content.context ? detailJson(content.context) : '<p class="trace-note">No log context was captured for this entry.</p>'
|
|
421
|
+
].join('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (entry.type === 'exception') {
|
|
425
|
+
return [
|
|
426
|
+
'<div class="detail-grid">',
|
|
427
|
+
renderMetricBox('Exception', [
|
|
428
|
+
{ label: 'Class', value: escapeHtml(content.class || '') },
|
|
429
|
+
{ label: 'Message', value: escapeHtml(content.message || '') },
|
|
430
|
+
{ label: 'File', value: '<span class="mono">' + escapeHtml(content.file || '') + '</span>' },
|
|
431
|
+
{ label: 'Line', value: escapeHtml(content.line || '') }
|
|
432
|
+
]),
|
|
433
|
+
renderMetricBox('Runtime', [
|
|
434
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
435
|
+
{ label: 'User', value: escapeHtml(content.userId || 'Anonymous') },
|
|
436
|
+
{ label: 'Occurrences', value: escapeHtml(content.occurrences || 1) }
|
|
437
|
+
]),
|
|
438
|
+
'</div>',
|
|
439
|
+
detailJson({ trace: content.trace || [], linePreview: content.linePreview || {} })
|
|
440
|
+
].join('');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (entry.type === 'client_request') {
|
|
444
|
+
return [
|
|
445
|
+
'<div class="detail-grid">',
|
|
446
|
+
renderMetricBox('Request', [
|
|
447
|
+
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
448
|
+
{ label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
|
|
449
|
+
{ label: 'Status', value: escapeHtml(content.responseStatus || '') },
|
|
450
|
+
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
451
|
+
]),
|
|
452
|
+
renderMetricBox('Runtime', [
|
|
453
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
454
|
+
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
455
|
+
]),
|
|
456
|
+
'</div>',
|
|
457
|
+
detailJson(content.requestHeaders || {})
|
|
458
|
+
].join('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return detailJson(content);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const renderTraceItems = (entries) => {
|
|
465
|
+
if (entries.length === 0) return '<p class="trace-note">No related entries captured.</p>';
|
|
466
|
+
|
|
467
|
+
return '<div class="trace-panel">' + entries.map((entry) => {
|
|
468
|
+
return [
|
|
469
|
+
'<section class="trace-item">',
|
|
470
|
+
'<div class="trace-item-head">',
|
|
471
|
+
'<div>',
|
|
472
|
+
'<span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span>',
|
|
473
|
+
'</div>',
|
|
474
|
+
'<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
|
|
475
|
+
'</div>',
|
|
476
|
+
'<div class="trace-item-summary">',
|
|
477
|
+
entrySummaryHtml(entry),
|
|
478
|
+
'<div>' + tagsHtml(entry.tags) + '</div>',
|
|
479
|
+
renderEntryBody(entry),
|
|
480
|
+
'</div>',
|
|
481
|
+
'</section>'
|
|
482
|
+
].join('');
|
|
483
|
+
}).join('') + '</div>';
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const renderRequestTrace = (main) => {
|
|
487
|
+
const entry = state.detail;
|
|
488
|
+
const content = entry && entry.content ? entry.content : {};
|
|
489
|
+
const traceTabs = [
|
|
490
|
+
{ id: 'summary', label: 'Summary' },
|
|
491
|
+
{ id: 'payload', label: 'Payload' },
|
|
492
|
+
{ id: 'headers', label: 'Headers' },
|
|
493
|
+
{ id: 'response', label: 'Response' },
|
|
494
|
+
{ id: 'queries', label: 'Queries', count: batchEntriesByType('query').length },
|
|
495
|
+
{ id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
|
|
496
|
+
{ id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
|
|
497
|
+
{ id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
|
|
498
|
+
{ id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type)).length }
|
|
499
|
+
];
|
|
500
|
+
const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
|
|
501
|
+
const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type));
|
|
502
|
+
const panels = {
|
|
503
|
+
summary: [
|
|
504
|
+
'<div class="detail-grid">',
|
|
505
|
+
renderMetricBox('Request', [
|
|
506
|
+
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
507
|
+
{ label: 'Path', value: '<span class="mono">' + escapeHtml(content.uri || '') + '</span>' },
|
|
508
|
+
{ label: 'Status', value: escapeHtml(content.responseStatus || '') },
|
|
509
|
+
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
510
|
+
]),
|
|
511
|
+
renderMetricBox('Runtime', [
|
|
512
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
513
|
+
{ label: 'User', value: escapeHtml(content.userId || 'Anonymous') },
|
|
514
|
+
{ label: 'Memory', value: escapeHtml(content.memory === null || content.memory === undefined ? 'Unavailable' : String(content.memory)) },
|
|
515
|
+
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '') + '</span>' }
|
|
516
|
+
]),
|
|
517
|
+
renderMetricBox('Tags', [
|
|
518
|
+
{ label: 'Values', value: tagsHtml(entry.tags) || '<span class="activity-time">-</span>' }
|
|
519
|
+
]),
|
|
520
|
+
'</div>'
|
|
521
|
+
].join(''),
|
|
522
|
+
payload: detailJson(content.payload || {}),
|
|
523
|
+
headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
524
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div><p class="trace-note">Response body capture is not wired yet. Status and headers are available.</p>' + detailJson(content.responseHeaders || {}) + '</div>',
|
|
525
|
+
queries: renderTraceItems(batchEntriesByType('query')),
|
|
526
|
+
logs: renderTraceItems(batchEntriesByType('log')),
|
|
527
|
+
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
528
|
+
http: renderTraceItems(batchEntriesByType('client_request')),
|
|
529
|
+
other: renderTraceItems(otherEntries)
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
main.innerHTML = [
|
|
533
|
+
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
534
|
+
'<section class="panel detail-card">',
|
|
535
|
+
'<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
|
|
536
|
+
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
537
|
+
'<div class="trace-tabs">',
|
|
538
|
+
traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
|
|
539
|
+
'</div>',
|
|
540
|
+
panels[currentTab] || panels.summary,
|
|
541
|
+
'</section>'
|
|
542
|
+
].join('');
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const statsCardsHtml = (stats) => {
|
|
546
|
+
const total = Object.values(stats).reduce((sum, value) => sum + Number(value || 0), 0);
|
|
547
|
+
const cards = [{ label: 'Total entries', value: total, meta: 'Stored trace entries.' }]
|
|
548
|
+
.concat(Object.entries(stats).filter((pair) => Number(pair[1]) > 0).map((pair) => ({ label: pair[0], value: Number(pair[1]), meta: pair[0] === 'query' ? 'Captured queries.' : 'Captured ' + pair[0] + '.' })));
|
|
549
|
+
return '<div class="stats-grid">' + cards.map((card) => '<section class="panel stat-card"><div class="stat-label">' + escapeHtml(card.label) + '</div><div class="stat-value">' + escapeHtml(card.value) + '</div><div class="stat-meta">' + escapeHtml(card.meta) + '</div></section>').join('') + '</div>';
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const renderOverview = async (main) => {
|
|
553
|
+
main.innerHTML = '<div class="panel empty">Loading trace overview...</div>';
|
|
554
|
+
try {
|
|
555
|
+
const results = await Promise.all([api('/stats'), api('/entries?perPage=8&page=1')]);
|
|
556
|
+
const stats = results[0].stats;
|
|
557
|
+
const recent = results[1];
|
|
558
|
+
const recentRows = recent.data || [];
|
|
559
|
+
const recentTable = recentRows.length === 0
|
|
560
|
+
? '<div class="empty">No trace entries recorded.</div>'
|
|
561
|
+
: '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
|
|
562
|
+
const activityList = recentRows.length === 0
|
|
563
|
+
? '<div class="empty">No recent activity.</div>'
|
|
564
|
+
: '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
|
|
565
|
+
|
|
566
|
+
main.innerHTML = [
|
|
567
|
+
statsCardsHtml(stats),
|
|
568
|
+
'<div class="content-grid">',
|
|
569
|
+
'<section class="panel">',
|
|
570
|
+
'<div class="section-head"><div><h3>Recent entries</h3><p>Latest captured records.</p></div><button type="button" class="btn btn-primary" data-action="go-page" data-page="entries">Open entries</button></div>',
|
|
571
|
+
recentTable,
|
|
572
|
+
'</section>',
|
|
573
|
+
'<div class="side-stack">',
|
|
574
|
+
'<section class="panel">',
|
|
575
|
+
'<div class="section-head"><div><h3>Actions</h3><p>Trace maintenance.</p></div></div>',
|
|
576
|
+
'<div class="toolbar"><button type="button" class="btn btn-danger" data-action="clear-all">Clear entries</button><button type="button" class="btn btn-ghost" data-action="go-page" data-page="monitoring">Manage tags</button></div>',
|
|
577
|
+
'</section>',
|
|
578
|
+
'<section class="panel">',
|
|
579
|
+
'<div class="section-head"><div><h3>Recent activity</h3><p>Latest captured events.</p></div></div>',
|
|
580
|
+
activityList,
|
|
581
|
+
'</section>',
|
|
582
|
+
'</div>',
|
|
583
|
+
'</div>'
|
|
584
|
+
].join('');
|
|
585
|
+
} catch (error) {
|
|
586
|
+
main.innerHTML = '<div class="panel empty">Error loading overview: ' + escapeHtml(error.message) + '</div>';
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const renderEntries = async (main) => {
|
|
591
|
+
if (state.detail) {
|
|
592
|
+
renderDetail(main);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
main.innerHTML = '<div class="panel empty">Loading entries...</div>';
|
|
597
|
+
try {
|
|
598
|
+
const qs = new URLSearchParams({ page: String(state.entriesPage), perPage: '50' });
|
|
599
|
+
if (state.entriesFilter.type) qs.set('type', state.entriesFilter.type);
|
|
600
|
+
if (state.entriesFilter.tag) qs.set('tag', state.entriesFilter.tag);
|
|
601
|
+
if (state.entriesFilter.batchId) qs.set('batchId', state.entriesFilter.batchId);
|
|
602
|
+
|
|
603
|
+
const response = await api('/entries?' + qs.toString());
|
|
604
|
+
const data = response.data || [];
|
|
605
|
+
const total = Number(response.total || 0);
|
|
606
|
+
const perPage = Number(response.perPage || 50);
|
|
607
|
+
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
608
|
+
const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
|
|
609
|
+
|
|
610
|
+
main.innerHTML = [
|
|
611
|
+
'<section class="panel">',
|
|
612
|
+
'<div class="section-head"><div><h3>Entries</h3><p>Filter by type, tag, or batch.</p></div></div>',
|
|
613
|
+
'<div class="toolbar">',
|
|
614
|
+
'<select id="f-type"><option value="">All types</option>' + ENTRY_TYPES.map((type) => '<option value="' + escapeHtml(type) + '"' + (state.entriesFilter.type === type ? ' selected' : '') + '>' + escapeHtml(type) + '</option>').join('') + '</select>',
|
|
615
|
+
'<input id="f-tag" type="text" placeholder="Tag" value="' + escapeHtml(state.entriesFilter.tag) + '">',
|
|
616
|
+
'<input id="f-batch" type="text" placeholder="Batch ID" value="' + escapeHtml(state.entriesFilter.batchId) + '">',
|
|
617
|
+
'<button type="button" class="btn btn-ghost" data-action="clear-filters">Reset</button>',
|
|
618
|
+
'</div>',
|
|
619
|
+
data.length === 0 ? '<div class="empty">No entries match the current filter.</div>' : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Batch</th><th>Happened</th></tr></thead><tbody>' + rows + '</tbody></table></div>',
|
|
620
|
+
'<div class="pagination"><span>Page ' + escapeHtml(state.entriesPage) + ' of ' + escapeHtml(totalPages) + ' · ' + escapeHtml(total) + ' total entries</span><div class="pagination-controls"><button type="button" data-action="page-prev"' + (state.entriesPage <= 1 ? ' disabled' : '') + '>Previous</button><button type="button" data-action="page-next"' + (state.entriesPage >= totalPages ? ' disabled' : '') + '>Next</button></div></div>',
|
|
621
|
+
'</section>'
|
|
622
|
+
].join('');
|
|
623
|
+
} catch (error) {
|
|
624
|
+
main.innerHTML = '<div class="panel empty">Error loading entries: ' + escapeHtml(error.message) + '</div>';
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const renderDetail = (main) => {
|
|
629
|
+
if (!state.detail) {
|
|
630
|
+
state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' };
|
|
631
|
+
renderEntries(main);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (hasRequestTrace()) {
|
|
636
|
+
renderRequestTrace(main);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const entry = state.detail;
|
|
641
|
+
main.innerHTML = [
|
|
642
|
+
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
643
|
+
'<section class="panel detail-card">',
|
|
644
|
+
'<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
|
|
645
|
+
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
646
|
+
'<div class="detail-stack">',
|
|
647
|
+
renderEntryBody(entry),
|
|
648
|
+
'</div>',
|
|
649
|
+
'</section>'
|
|
650
|
+
].join('');
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const renderMonitoring = async (main) => {
|
|
654
|
+
main.innerHTML = '<div class="panel empty">Loading monitoring tags...</div>';
|
|
655
|
+
try {
|
|
656
|
+
const result = await api('/monitoring');
|
|
657
|
+
const tags = result.tags || [];
|
|
658
|
+
main.innerHTML = [
|
|
659
|
+
'<section class="panel">',
|
|
660
|
+
'<div class="section-head"><div><h3>Monitoring tags</h3><p>Pinned tags for quick filtering.</p></div></div>',
|
|
661
|
+
'<div class="monitoring-wrap">',
|
|
662
|
+
'<div class="tag-list">',
|
|
663
|
+
tags.length === 0 ? '<span class="helper-text">No tags monitored.</span>' : tags.map((tag) => '<span class="tag-item"><button type="button" class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
|
|
664
|
+
'</div>',
|
|
665
|
+
'<div class="toolbar" style="padding:0;margin-top:8px">',
|
|
666
|
+
'<input id="new-tag" class="control" type="text" placeholder="Add tag">',
|
|
667
|
+
'<button type="button" class="btn btn-primary" data-action="add-tag">Add tag</button>',
|
|
668
|
+
'</div>',
|
|
669
|
+
'</div>',
|
|
670
|
+
'</section>'
|
|
671
|
+
].join('');
|
|
672
|
+
} catch (error) {
|
|
673
|
+
main.innerHTML = '<div class="panel empty">Error loading monitoring tags: ' + escapeHtml(error.message) + '</div>';
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const render = async () => {
|
|
678
|
+
const main = document.getElementById('main');
|
|
679
|
+
if (!main) return;
|
|
680
|
+
|
|
681
|
+
setPageCopy(state.page);
|
|
682
|
+
updateThemeButton();
|
|
683
|
+
const activeShortcut = activeEntryShortcut();
|
|
684
|
+
document.querySelectorAll('[data-page]').forEach((button) => {
|
|
685
|
+
button.classList.toggle('active', button.getAttribute('data-page') === state.page);
|
|
686
|
+
});
|
|
687
|
+
document.querySelectorAll('[data-action="type-shortcut"]').forEach((button) => {
|
|
688
|
+
button.classList.toggle('active', button.getAttribute('data-type') === activeShortcut);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (state.page === 'overview') await renderOverview(main);
|
|
692
|
+
if (state.page === 'entries') await renderEntries(main);
|
|
693
|
+
if (state.page === 'monitoring') await renderMonitoring(main);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const setPage = (page) => {
|
|
697
|
+
state = { ...state, page, entriesPage: 1, detail: null, detailBatch: null, detailTab: 'summary' };
|
|
698
|
+
render();
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const setTypeShortcut = (type) => {
|
|
702
|
+
state = { ...state, page: 'entries', detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { ...state.entriesFilter, type } };
|
|
703
|
+
render();
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const filterByTag = (tag) => {
|
|
707
|
+
state = { ...state, page: 'entries', detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { ...state.entriesFilter, tag, batchId: '' } };
|
|
708
|
+
render();
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const syncFilters = () => {
|
|
712
|
+
const typeInput = document.getElementById('f-type');
|
|
713
|
+
const tagInput = document.getElementById('f-tag');
|
|
714
|
+
const batchInput = document.getElementById('f-batch');
|
|
715
|
+
state = {
|
|
716
|
+
...state,
|
|
717
|
+
entriesPage: 1,
|
|
718
|
+
entriesFilter: {
|
|
719
|
+
type: typeInput && 'value' in typeInput ? String(typeInput.value || '') : '',
|
|
720
|
+
tag: tagInput && 'value' in tagInput ? String(tagInput.value || '') : '',
|
|
721
|
+
batchId: batchInput && 'value' in batchInput ? String(batchInput.value || '') : ''
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
render();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const clearAll = async () => {
|
|
728
|
+
if (!window.confirm('Delete all trace entries?')) return;
|
|
729
|
+
try {
|
|
730
|
+
await api('/entries', { method: 'DELETE' });
|
|
731
|
+
state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1 };
|
|
732
|
+
render();
|
|
733
|
+
} catch (error) {
|
|
734
|
+
window.alert(error.message);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const showDetail = async (uuid) => {
|
|
739
|
+
try {
|
|
740
|
+
const detailResult = await api('/entries/' + encodeURIComponent(uuid));
|
|
741
|
+
const entry = detailResult.entry;
|
|
742
|
+
let detailBatch = null;
|
|
743
|
+
if (entry.type === 'request' && entry.batchId) {
|
|
744
|
+
const batch = await api('/batch/' + encodeURIComponent(entry.batchId));
|
|
745
|
+
detailBatch = batch.entries || [];
|
|
746
|
+
}
|
|
747
|
+
state = { ...state, detail: entry, detailBatch, detailTab: 'summary', page: 'entries' };
|
|
748
|
+
render();
|
|
749
|
+
} catch (error) {
|
|
750
|
+
window.alert(error.message);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const addTag = async () => {
|
|
755
|
+
const input = document.getElementById('new-tag');
|
|
756
|
+
const value = input && 'value' in input ? String(input.value || '').trim() : '';
|
|
757
|
+
if (value === '') return;
|
|
758
|
+
try {
|
|
759
|
+
await api('/monitoring/' + encodeURIComponent(value), { method: 'POST' });
|
|
760
|
+
render();
|
|
761
|
+
} catch (error) {
|
|
762
|
+
window.alert(error.message);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const removeTag = async (tag) => {
|
|
767
|
+
try {
|
|
768
|
+
await api('/monitoring/' + encodeURIComponent(tag), { method: 'DELETE' });
|
|
769
|
+
render();
|
|
770
|
+
} catch (error) {
|
|
771
|
+
window.alert(error.message);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const copyText = async (text) => {
|
|
776
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
777
|
+
await navigator.clipboard.writeText(text);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const textarea = document.createElement('textarea');
|
|
782
|
+
textarea.value = text;
|
|
783
|
+
textarea.setAttribute('readonly', 'true');
|
|
784
|
+
textarea.style.position = 'absolute';
|
|
785
|
+
textarea.style.left = '-9999px';
|
|
786
|
+
document.body.appendChild(textarea);
|
|
787
|
+
textarea.select();
|
|
788
|
+
document.execCommand('copy');
|
|
789
|
+
document.body.removeChild(textarea);
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const copyCode = async (copyId, button) => {
|
|
793
|
+
const payload = copyPayloads.get(copyId);
|
|
794
|
+
if (typeof payload !== 'string') return;
|
|
795
|
+
try {
|
|
796
|
+
await copyText(payload);
|
|
797
|
+
if (button instanceof HTMLElement) {
|
|
798
|
+
button.dataset.copied = 'true';
|
|
799
|
+
window.setTimeout(() => {
|
|
800
|
+
if (button.dataset.copied === 'true') delete button.dataset.copied;
|
|
801
|
+
}, 1200);
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
window.alert(error.message || 'Failed to copy block');
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
document.addEventListener('click', (event) => {
|
|
809
|
+
const target = event.target instanceof Element ? event.target.closest('[data-action],[data-page],#theme-toggle') : null;
|
|
810
|
+
if (!target) return;
|
|
811
|
+
|
|
812
|
+
if (target.id === 'theme-toggle') {
|
|
813
|
+
setTheme(document.documentElement.dataset.theme === 'light' ? 'dark' : 'light');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (target.hasAttribute('data-page') && !target.hasAttribute('data-action')) {
|
|
818
|
+
setPage(String(target.getAttribute('data-page') || 'overview'));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const action = target.getAttribute('data-action');
|
|
823
|
+
if (action === 'go-page') { setPage(String(target.getAttribute('data-page') || 'overview')); return; }
|
|
824
|
+
if (action === 'type-shortcut') { setTypeShortcut(String(target.getAttribute('data-type') || '')); return; }
|
|
825
|
+
if (action === 'filter-tag') { filterByTag(String(target.getAttribute('data-tag') || '')); return; }
|
|
826
|
+
if (action === 'detail-tab') { state = { ...state, detailTab: String(target.getAttribute('data-tab') || 'summary') }; render(); return; }
|
|
827
|
+
if (action === 'clear-all') { clearAll(); return; }
|
|
828
|
+
if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
|
|
829
|
+
if (action === 'close-detail') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary' }; render(); return; }
|
|
830
|
+
if (action === 'page-prev') { state = { ...state, entriesPage: Math.max(1, state.entriesPage - 1) }; render(); return; }
|
|
831
|
+
if (action === 'page-next') { state = { ...state, entriesPage: state.entriesPage + 1 }; render(); return; }
|
|
832
|
+
if (action === 'clear-filters') { state = { ...state, detail: null, detailBatch: null, detailTab: 'summary', entriesPage: 1, entriesFilter: { type: '', tag: '', batchId: '' } }; render(); return; }
|
|
833
|
+
if (action === 'add-tag') { addTag(); return; }
|
|
834
|
+
if (action === 'remove-tag') { removeTag(String(target.getAttribute('data-tag') || '')); return; }
|
|
835
|
+
if (action === 'copy-code') { copyCode(String(target.getAttribute('data-copy-id') || ''), target); }
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
document.addEventListener('input', (event) => {
|
|
839
|
+
const target = event.target;
|
|
840
|
+
if (!(target instanceof HTMLElement)) return;
|
|
841
|
+
if (target.id === 'f-tag' || target.id === 'f-batch') syncFilters();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
document.addEventListener('change', (event) => {
|
|
845
|
+
const target = event.target;
|
|
846
|
+
if (!(target instanceof HTMLElement)) return;
|
|
847
|
+
if (target.id === 'f-type') syncFilters();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
render();
|
|
851
|
+
})();
|
|
852
|
+
</script>
|
|
853
|
+
</body>
|
|
854
|
+
</html>`;
|
|
855
|
+
const buildDashboardHtml = (basePath, projectName) => {
|
|
856
|
+
const resolvedProjectName = projectName && projectName.trim() !== '' ? projectName : 'ZinTrust';
|
|
857
|
+
const resolvedTitle = `ZinTrust Trace - ${resolvedProjectName}`;
|
|
858
|
+
return DASHBOARD_DOCUMENT.replace('__TRACE_FAVICON__', encodeSvgDataUri(BRAND_SVG))
|
|
859
|
+
.replace('__TRACE_TITLE__', resolvedTitle)
|
|
860
|
+
.replace('__TRACE_LOGO__', BRAND_SVG)
|
|
861
|
+
.replaceAll('__TRACE_PROJECT_NAME__', resolvedProjectName)
|
|
862
|
+
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
863
|
+
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
864
|
+
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
865
|
+
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
866
|
+
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
867
|
+
.replace('__TRACE_BASE_PATH_LABEL__', basePath)
|
|
868
|
+
.replace('__TRACE_BASE_PATH_JSON__', JSON.stringify(basePath));
|
|
869
|
+
};
|
|
870
|
+
export { buildDashboardHtml };
|