@mantiq/heartbeat 0.5.21 → 0.5.23
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/src/Heartbeat.ts +15 -27
- package/src/HeartbeatServiceProvider.ts +66 -0
- package/src/contracts/Entry.ts +19 -0
- package/src/contracts/HeartbeatConfig.ts +2 -0
- package/src/dashboard/DashboardController.ts +71 -12
- package/src/dashboard/pages/CachePage.ts +61 -5
- package/src/dashboard/pages/CommandDetailPage.ts +216 -0
- package/src/dashboard/pages/CommandsPage.ts +72 -0
- package/src/dashboard/pages/EventsPage.ts +59 -6
- package/src/dashboard/pages/ExceptionsPage.ts +37 -6
- package/src/dashboard/pages/JobsPage.ts +61 -5
- package/src/dashboard/pages/LogsPage.ts +116 -0
- package/src/dashboard/pages/ModelsPage.ts +112 -0
- package/src/dashboard/pages/NotificationsPage.ts +87 -0
- package/src/dashboard/pages/OverviewPage.ts +109 -45
- package/src/dashboard/pages/PerformancePage.ts +151 -20
- package/src/dashboard/pages/QueriesPage.ts +92 -8
- package/src/dashboard/pages/RequestDetailPage.ts +227 -3
- package/src/dashboard/pages/RequestsPage.ts +90 -3
- package/src/dashboard/pages/SchedulesPage.ts +71 -0
- package/src/dashboard/shared/components.ts +140 -0
- package/src/dashboard/shared/layout.ts +327 -108
- package/src/index.ts +9 -0
- package/src/metrics/MetricsCollector.ts +7 -3
- package/src/middleware/HeartbeatMiddleware.ts +26 -9
- package/src/migrations/CreateHeartbeatTables.ts +14 -0
- package/src/models/EntryModel.ts +1 -1
- package/src/storage/HeartbeatStore.ts +174 -1
- package/src/testing/HeartbeatFake.ts +6 -0
- package/src/tracing/Tracer.ts +32 -0
- package/src/watchers/CommandWatcher.ts +23 -0
- package/src/watchers/EventWatcher.ts +13 -0
- package/src/watchers/ScheduleWatcher.ts +1 -0
- package/src/widget/DebugWidget.ts +53 -30
|
@@ -11,23 +11,56 @@ export function renderLayout(options: {
|
|
|
11
11
|
}): string {
|
|
12
12
|
const { title, activePage, basePath, content } = options
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
14
|
+
const sections = [
|
|
15
|
+
{
|
|
16
|
+
label: '',
|
|
17
|
+
items: [
|
|
18
|
+
{ key: 'overview', label: 'Overview', icon: ICONS.grid },
|
|
19
|
+
{ key: 'performance', label: 'Performance', icon: ICONS.activity },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: 'Inspect',
|
|
24
|
+
items: [
|
|
25
|
+
{ key: 'requests', label: 'Requests', icon: ICONS.arrow },
|
|
26
|
+
{ key: 'queries', label: 'Queries', icon: ICONS.db },
|
|
27
|
+
{ key: 'exceptions', label: 'Exceptions', icon: ICONS.alert },
|
|
28
|
+
{ key: 'logs', label: 'Logs', icon: ICONS.file },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Services',
|
|
33
|
+
items: [
|
|
34
|
+
{ key: 'jobs', label: 'Jobs', icon: ICONS.layers },
|
|
35
|
+
{ key: 'cache', label: 'Cache', icon: ICONS.box },
|
|
36
|
+
{ key: 'events', label: 'Events', icon: ICONS.zap },
|
|
37
|
+
{ key: 'mail', label: 'Mail', icon: ICONS.mail },
|
|
38
|
+
{ key: 'notifications', label: 'Notifications', icon: ICONS.bell },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'System',
|
|
43
|
+
items: [
|
|
44
|
+
{ key: 'models', label: 'Models', icon: ICONS.cube },
|
|
45
|
+
{ key: 'schedules', label: 'Schedules', icon: ICONS.clock },
|
|
46
|
+
{ key: 'commands', label: 'Commands', icon: ICONS.terminal },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
24
49
|
]
|
|
25
50
|
|
|
26
|
-
const nav =
|
|
27
|
-
.map((
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
const nav = sections
|
|
52
|
+
.map((section) => {
|
|
53
|
+
const heading = section.label
|
|
54
|
+
? `<div class="nav-section">${section.label}</div>`
|
|
55
|
+
: ''
|
|
56
|
+
const links = section.items
|
|
57
|
+
.map((p) => {
|
|
58
|
+
const cls = p.key === activePage ? ' class="active"' : ''
|
|
59
|
+
const href = p.key === 'overview' ? basePath : `${basePath}/${p.key}`
|
|
60
|
+
return `<a href="${href}"${cls}>${p.icon}<span>${p.label}</span></a>`
|
|
61
|
+
})
|
|
62
|
+
.join('\n ')
|
|
63
|
+
return heading + links
|
|
31
64
|
})
|
|
32
65
|
.join('\n ')
|
|
33
66
|
|
|
@@ -42,18 +75,21 @@ export function renderLayout(options: {
|
|
|
42
75
|
<body>
|
|
43
76
|
<aside class="sidebar">
|
|
44
77
|
<div class="brand">
|
|
45
|
-
<svg width="
|
|
46
|
-
<span>
|
|
78
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
79
|
+
<span>heartbeat</span>
|
|
47
80
|
</div>
|
|
48
81
|
<nav>${nav}</nav>
|
|
49
82
|
<div class="sidebar-footer">
|
|
50
|
-
<
|
|
83
|
+
<div class="sidebar-brand-footer">
|
|
84
|
+
<span class="dot">.</span>mantiq
|
|
85
|
+
</div>
|
|
86
|
+
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">${ICONS.moon}</button>
|
|
51
87
|
</div>
|
|
52
88
|
</aside>
|
|
53
89
|
<main>
|
|
54
90
|
<div class="topbar">
|
|
55
91
|
<h1 class="page-title">${title}</h1>
|
|
56
|
-
<button class="
|
|
92
|
+
<button class="refresh-toggle" onclick="toggleAutoRefresh()" title="Toggle auto-refresh (R)">${ICONS.refresh} Auto</button>
|
|
57
93
|
</div>
|
|
58
94
|
${content}
|
|
59
95
|
</main>
|
|
@@ -70,6 +106,23 @@ export function renderLayout(options: {
|
|
|
70
106
|
document.documentElement.setAttribute('data-theme',n);
|
|
71
107
|
localStorage.setItem('hb-theme',n);
|
|
72
108
|
}
|
|
109
|
+
var _autoRefresh = false;
|
|
110
|
+
var _refreshTimer = null;
|
|
111
|
+
function toggleAutoRefresh() {
|
|
112
|
+
_autoRefresh = !_autoRefresh;
|
|
113
|
+
var btn = document.querySelector('.refresh-toggle');
|
|
114
|
+
if (btn) btn.classList.toggle('active', _autoRefresh);
|
|
115
|
+
if (_autoRefresh) {
|
|
116
|
+
_refreshTimer = setInterval(function() { location.reload(); }, 5000);
|
|
117
|
+
} else {
|
|
118
|
+
clearInterval(_refreshTimer);
|
|
119
|
+
_refreshTimer = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
document.addEventListener('keydown', function(e) {
|
|
123
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
|
|
124
|
+
if (e.key === 'r') toggleAutoRefresh();
|
|
125
|
+
});
|
|
73
126
|
</script>
|
|
74
127
|
</body>
|
|
75
128
|
</html>`
|
|
@@ -88,164 +141,330 @@ const ICONS = {
|
|
|
88
141
|
activity: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>`,
|
|
89
142
|
mail: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`,
|
|
90
143
|
moon: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`,
|
|
144
|
+
file: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
|
|
145
|
+
cube: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
|
|
146
|
+
clock: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
|
|
147
|
+
terminal: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
|
|
148
|
+
bell: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>`,
|
|
149
|
+
refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>`,
|
|
91
150
|
}
|
|
92
151
|
|
|
93
152
|
// ── CSS ──────────────────────────────────────────────────────────────────────
|
|
94
153
|
|
|
95
154
|
const CSS = `
|
|
155
|
+
/* ── Animations ──────────────────────────────────────────────────────────── */
|
|
156
|
+
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
157
|
+
@keyframes pulse-glow{0%,100%{opacity:.6}50%{opacity:1}}
|
|
158
|
+
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
|
159
|
+
@keyframes slide-up{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
|
160
|
+
|
|
161
|
+
/* ── Theme — Solid emerald, no transparency ─────────────────────────────── */
|
|
96
162
|
:root,[data-theme="light"]{
|
|
97
|
-
--bg-0:#
|
|
98
|
-
--fg-0:#
|
|
99
|
-
--border:#
|
|
100
|
-
--accent:#
|
|
101
|
-
--green:#
|
|
102
|
-
--amber:#
|
|
103
|
-
--red:#dc2626;--red-soft
|
|
104
|
-
--blue:#2563eb;--blue-soft
|
|
163
|
+
--bg-0:#fafafa;--bg-1:#f5f5f4;--bg-2:#e7e5e4;--bg-3:#d6d3d1;
|
|
164
|
+
--fg-0:#0c0a09;--fg-1:#1c1917;--fg-2:#78716c;--fg-3:#a8a29e;
|
|
165
|
+
--border:#e7e5e4;--border-2:#d6d3d1;
|
|
166
|
+
--accent:#059669;--accent-hover:#047857;--accent-text:#047857;
|
|
167
|
+
--green:#059669;--green-soft:#ecfdf5;
|
|
168
|
+
--amber:#b45309;--amber-soft:#fffbeb;
|
|
169
|
+
--red:#dc2626;--red-soft:#fef2f2;
|
|
170
|
+
--blue:#2563eb;--blue-soft:#eff6ff;
|
|
171
|
+
--card-bg:#ffffff;--card-border:#e7e5e4;
|
|
172
|
+
|
|
173
|
+
--mono:'SF Mono',ui-monospace,'Cascadia Mono','JetBrains Mono',Menlo,monospace;
|
|
174
|
+
--radius:10px;
|
|
105
175
|
}
|
|
106
176
|
[data-theme="dark"]{
|
|
107
|
-
--bg-0:#
|
|
108
|
-
--fg-0:#fafafa;--fg-1:#
|
|
109
|
-
--border:#27272a;--
|
|
110
|
-
--accent:#34d399;--accent-
|
|
111
|
-
--green:#
|
|
112
|
-
--amber:#
|
|
113
|
-
--red:#
|
|
114
|
-
--blue:#
|
|
177
|
+
--bg-0:#09090b;--bg-1:#0c0c0e;--bg-2:#18181b;--bg-3:#27272a;
|
|
178
|
+
--fg-0:#fafafa;--fg-1:#e4e4e7;--fg-2:#a1a1aa;--fg-3:#52525b;
|
|
179
|
+
--border:#27272a;--border-2:#3f3f46;
|
|
180
|
+
--accent:#10b981;--accent-hover:#34d399;--accent-text:#34d399;
|
|
181
|
+
--green:#10b981;--green-soft:#0c1f17;
|
|
182
|
+
--amber:#f59e0b;--amber-soft:#1a1508;
|
|
183
|
+
--red:#ef4444;--red-soft:#1f0c0c;
|
|
184
|
+
--blue:#3b82f6;--blue-soft:#0c1425;
|
|
185
|
+
--card-bg:#0c0c0e;--card-border:#27272a;
|
|
186
|
+
|
|
187
|
+
--mono:'SF Mono',ui-monospace,'Cascadia Mono','JetBrains Mono',Menlo,monospace;
|
|
188
|
+
--radius:10px;
|
|
115
189
|
}
|
|
190
|
+
|
|
191
|
+
/* ── Reset ───────────────────────────────────────────────────────────────── */
|
|
116
192
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
117
193
|
body{
|
|
118
|
-
font-family
|
|
119
|
-
font-size:
|
|
194
|
+
font-family:var(--mono);
|
|
195
|
+
font-size:13px;line-height:1.6;color:var(--fg-0);background:var(--bg-0);
|
|
120
196
|
display:flex;min-height:100vh;-webkit-font-smoothing:antialiased;
|
|
197
|
+
font-feature-settings:'liga' 1,'calt' 1;
|
|
121
198
|
}
|
|
199
|
+
::selection{background:var(--hover-bg);color:var(--fg-0)}
|
|
200
|
+
::-webkit-scrollbar{width:5px;height:5px}
|
|
201
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
202
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:10px}
|
|
203
|
+
::-webkit-scrollbar-thumb:hover{background:var(--fg-3)}
|
|
122
204
|
|
|
123
|
-
/* Sidebar */
|
|
205
|
+
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
|
|
124
206
|
.sidebar{
|
|
125
|
-
width:200px;background:var(--bg-
|
|
207
|
+
width:200px;background:var(--bg-0);border-right:1px solid var(--border);
|
|
126
208
|
display:flex;flex-direction:column;position:fixed;top:0;bottom:0;z-index:10;
|
|
127
209
|
}
|
|
128
210
|
.brand{
|
|
129
|
-
padding:16px 14px;font-size:
|
|
130
|
-
display:flex;align-items:center;gap:
|
|
211
|
+
padding:18px 16px 14px;font-size:12px;font-weight:700;color:var(--fg-0);
|
|
212
|
+
display:flex;align-items:center;gap:7px;letter-spacing:-.02em;
|
|
213
|
+
font-family:var(--mono);border-bottom:1px solid var(--border);
|
|
131
214
|
}
|
|
132
215
|
.brand svg{color:var(--accent)}
|
|
133
|
-
.sidebar nav{padding:
|
|
216
|
+
.sidebar nav{padding:6px 0;flex:1;overflow-y:auto}
|
|
217
|
+
.nav-section{
|
|
218
|
+
font-size:9px;font-weight:700;color:var(--fg-3);
|
|
219
|
+
text-transform:uppercase;letter-spacing:.1em;
|
|
220
|
+
padding:16px 16px 4px;font-family:var(--mono);
|
|
221
|
+
}
|
|
134
222
|
.sidebar nav a{
|
|
135
223
|
display:flex;align-items:center;gap:8px;
|
|
136
|
-
padding:
|
|
137
|
-
color:var(--fg-2);text-decoration:none;font-size:
|
|
138
|
-
transition:color .
|
|
224
|
+
padding:7px 16px;margin:0;
|
|
225
|
+
color:var(--fg-2);text-decoration:none;font-size:11px;font-weight:500;
|
|
226
|
+
transition:color .12s;
|
|
227
|
+
font-family:var(--mono);
|
|
228
|
+
border-left:2px solid transparent;
|
|
139
229
|
}
|
|
140
230
|
.sidebar nav a span{flex:1}
|
|
141
|
-
.sidebar nav a svg{flex-shrink:0;opacity:.
|
|
142
|
-
.sidebar nav a:hover{color:var(--fg-0)
|
|
143
|
-
.sidebar nav a:hover svg{opacity
|
|
144
|
-
.sidebar nav a.active{
|
|
231
|
+
.sidebar nav a svg{flex-shrink:0;opacity:.35;transition:opacity .12s}
|
|
232
|
+
.sidebar nav a:hover{color:var(--fg-0)}
|
|
233
|
+
.sidebar nav a:hover svg{opacity:.6}
|
|
234
|
+
.sidebar nav a.active{
|
|
235
|
+
color:var(--accent);font-weight:600;
|
|
236
|
+
border-left-color:var(--accent);
|
|
237
|
+
}
|
|
145
238
|
.sidebar nav a.active svg{opacity:1;color:var(--accent)}
|
|
146
239
|
.sidebar-footer{
|
|
147
|
-
border-top:1px solid var(--border);padding:
|
|
240
|
+
border-top:1px solid var(--border);padding:14px 16px;
|
|
148
241
|
display:flex;align-items:center;justify-content:space-between;
|
|
149
242
|
}
|
|
150
243
|
.sidebar-brand-footer{
|
|
151
|
-
font-size:
|
|
152
|
-
font-family:
|
|
244
|
+
font-size:12px;font-weight:700;color:var(--fg-3);
|
|
245
|
+
font-family:var(--mono);letter-spacing:-.02em;
|
|
153
246
|
}
|
|
247
|
+
.sidebar-brand-footer .dot{color:var(--accent);font-weight:800}
|
|
154
248
|
.theme-btn{
|
|
155
249
|
all:unset;color:var(--fg-3);cursor:pointer;display:flex;align-items:center;
|
|
156
|
-
padding:
|
|
250
|
+
padding:6px;border-radius:8px;border:1px solid var(--border);
|
|
251
|
+
transition:all .2s;
|
|
157
252
|
}
|
|
158
|
-
.theme-btn:hover{color:var(--
|
|
253
|
+
.theme-btn:hover{color:var(--accent);border-color:var(--accent)}
|
|
159
254
|
|
|
160
|
-
/* Main */
|
|
161
|
-
main{
|
|
162
|
-
|
|
163
|
-
.
|
|
255
|
+
/* ── Main ────────────────────────────────────────────────────────────────── */
|
|
256
|
+
main{
|
|
257
|
+
margin-left:200px;flex:1;padding:24px 28px;max-width:1200px;position:relative;
|
|
258
|
+
animation:fadeIn .3s ease-out;
|
|
259
|
+
}
|
|
260
|
+
.topbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
|
261
|
+
.page-title{
|
|
262
|
+
font-size:20px;font-weight:700;letter-spacing:-.03em;color:var(--fg-0);margin:0;
|
|
263
|
+
font-family:var(--mono);
|
|
264
|
+
}
|
|
164
265
|
|
|
165
|
-
/* Cards */
|
|
266
|
+
/* ── Cards — Raised Apple-like ───────────────────────────────────────────── */
|
|
166
267
|
.card{
|
|
167
|
-
background:var(--bg
|
|
168
|
-
border-radius:
|
|
268
|
+
background:var(--card-bg);border:1px solid var(--card-border);
|
|
269
|
+
border-radius:var(--radius);padding:18px 20px;margin-bottom:16px;
|
|
270
|
+
transition:border-color .2s;
|
|
169
271
|
}
|
|
272
|
+
.card:hover{border-color:var(--border-2)}
|
|
170
273
|
.card-title{
|
|
171
|
-
font-size:
|
|
172
|
-
text-transform:uppercase;letter-spacing:.
|
|
274
|
+
font-size:10px;font-weight:700;color:var(--fg-3);
|
|
275
|
+
text-transform:uppercase;letter-spacing:.1em;margin-bottom:12px;
|
|
276
|
+
font-family:var(--mono);
|
|
173
277
|
}
|
|
174
278
|
|
|
175
|
-
/*
|
|
176
|
-
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(
|
|
177
|
-
.stat{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.
|
|
279
|
+
/* ── Stats — Neon glow on hover ──────────────────────────────────────────── */
|
|
280
|
+
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:20px}
|
|
281
|
+
.stat{
|
|
282
|
+
background:var(--card-bg);border:1px solid var(--card-border);
|
|
283
|
+
border-radius:var(--radius);padding:16px 18px;
|
|
284
|
+
transition:all .25s cubic-bezier(.4,0,.2,1);
|
|
285
|
+
animation:slide-up .4s ease-out both;
|
|
286
|
+
}
|
|
287
|
+
.stat:hover{border-color:var(--accent)}
|
|
288
|
+
.stat-label{
|
|
289
|
+
font-size:10px;font-weight:700;color:var(--fg-3);
|
|
290
|
+
text-transform:uppercase;letter-spacing:.1em;
|
|
291
|
+
font-family:var(--mono);
|
|
292
|
+
}
|
|
293
|
+
.stat-val{
|
|
294
|
+
font-size:28px;font-weight:800;letter-spacing:-.04em;margin-top:6px;
|
|
295
|
+
color:var(--fg-0);font-variant-numeric:tabular-nums;
|
|
296
|
+
font-family:var(--mono);
|
|
297
|
+
}
|
|
298
|
+
.stat-sub{font-size:10px;color:var(--fg-3);margin-top:3px;font-family:var(--mono)}
|
|
181
299
|
|
|
182
|
-
/* Tables */
|
|
183
|
-
table{width:100%;border-collapse:collapse;font-size:
|
|
300
|
+
/* ── Tables — Clean monospace ────────────────────────────────────────────── */
|
|
301
|
+
table{width:100%;border-collapse:collapse;font-size:12px;font-family:var(--mono)}
|
|
184
302
|
thead th{
|
|
185
|
-
text-align:left;padding:
|
|
186
|
-
color:var(--fg-3);text-transform:uppercase;letter-spacing:.
|
|
303
|
+
text-align:left;padding:8px 12px;font-size:10px;font-weight:700;
|
|
304
|
+
color:var(--fg-3);text-transform:uppercase;letter-spacing:.08em;
|
|
187
305
|
border-bottom:1px solid var(--border);
|
|
188
306
|
}
|
|
189
|
-
tbody td{
|
|
307
|
+
tbody td{
|
|
308
|
+
padding:10px 12px;border-bottom:1px solid var(--border);
|
|
309
|
+
color:var(--fg-1);vertical-align:middle;
|
|
310
|
+
transition:background .15s;
|
|
311
|
+
}
|
|
190
312
|
tbody tr:last-child td{border-bottom:none}
|
|
191
313
|
tbody tr:hover td{background:var(--bg-2)}
|
|
314
|
+
tbody tr{animation:fadeIn .3s ease-out both}
|
|
315
|
+
tbody tr:nth-child(1){animation-delay:.02s}
|
|
316
|
+
tbody tr:nth-child(2){animation-delay:.04s}
|
|
317
|
+
tbody tr:nth-child(3){animation-delay:.06s}
|
|
318
|
+
tbody tr:nth-child(4){animation-delay:.08s}
|
|
319
|
+
tbody tr:nth-child(5){animation-delay:.1s}
|
|
192
320
|
|
|
193
|
-
/* Badges */
|
|
321
|
+
/* ── Badges — Pill-shaped with glow ──────────────────────────────────────── */
|
|
194
322
|
.b{
|
|
195
|
-
display:inline-flex;align-items:center;padding:
|
|
196
|
-
font-size:11px;font-weight:
|
|
323
|
+
display:inline-flex;align-items:center;padding:2px 10px;border-radius:100px;
|
|
324
|
+
font-size:11px;font-weight:600;line-height:18px;white-space:nowrap;
|
|
325
|
+
font-family:var(--mono);letter-spacing:.01em;
|
|
326
|
+
transition:all .2s;
|
|
197
327
|
}
|
|
198
|
-
.b-green{background:var(--green-soft);color:var(--green)}
|
|
199
|
-
.b-amber{background:var(--amber-soft);color:var(--amber)}
|
|
200
|
-
.b-red{background:var(--red-soft);color:var(--red)}
|
|
201
|
-
.b-blue{background:var(--blue-soft);color:var(--blue)}
|
|
202
|
-
.b-mute{background:var(--bg-
|
|
328
|
+
.b-green{background:var(--green-soft);color:var(--green);border:1px solid var(--border)}
|
|
329
|
+
.b-amber{background:var(--amber-soft);color:var(--amber);border:1px solid var(--border)}
|
|
330
|
+
.b-red{background:var(--red-soft);color:var(--red);border:1px solid var(--border)}
|
|
331
|
+
.b-blue{background:var(--blue-soft);color:var(--blue);border:1px solid var(--border)}
|
|
332
|
+
.b-mute{background:var(--bg-2);color:var(--fg-2);border:1px solid var(--border)}
|
|
203
333
|
|
|
204
|
-
/* Tabs */
|
|
334
|
+
/* ── Tabs ────────────────────────────────────────────────────────────────── */
|
|
205
335
|
.tabs{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:0}
|
|
206
336
|
.tabs input[type="radio"]{display:none}
|
|
207
337
|
.tabs label{
|
|
208
|
-
padding:
|
|
338
|
+
padding:10px 18px;font-size:12px;font-weight:600;color:var(--fg-3);
|
|
209
339
|
cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;
|
|
210
|
-
transition:
|
|
340
|
+
transition:all .2s;font-family:var(--mono);
|
|
211
341
|
}
|
|
212
342
|
.tabs label:hover{color:var(--fg-1)}
|
|
213
|
-
.tabs input[type="radio"]:checked+label{
|
|
214
|
-
|
|
343
|
+
.tabs input[type="radio"]:checked+label{
|
|
344
|
+
color:var(--accent-text);border-bottom-color:var(--accent);
|
|
345
|
+
}
|
|
346
|
+
.tab-panel{display:none;padding:16px 0 0;animation:fadeIn .2s ease-out}
|
|
215
347
|
.tab-panel.active{display:block}
|
|
216
348
|
|
|
217
|
-
/* Detail meta */
|
|
218
|
-
.meta-grid{
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
.meta-item{padding:10px 14px;border-bottom:1px solid var(--border)}
|
|
349
|
+
/* ── Detail meta ─────────────────────────────────────────────────────────── */
|
|
350
|
+
.meta-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0}
|
|
351
|
+
.meta-item{padding:12px 16px;border-bottom:1px solid var(--border)}
|
|
222
352
|
.meta-item:last-child{border-bottom:none}
|
|
223
|
-
.meta-label{
|
|
224
|
-
|
|
353
|
+
.meta-label{
|
|
354
|
+
font-size:10px;font-weight:700;color:var(--fg-3);
|
|
355
|
+
text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px;
|
|
356
|
+
font-family:var(--mono);
|
|
357
|
+
}
|
|
358
|
+
.meta-value{font-size:12px;color:var(--fg-1);word-break:break-all;font-family:var(--mono)}
|
|
225
359
|
|
|
226
|
-
/*
|
|
360
|
+
/* ── Copy ────────────────────────────────────────────────────────────────── */
|
|
227
361
|
.copy-bar{
|
|
228
362
|
display:flex;align-items:center;gap:8px;
|
|
229
|
-
background:var(--bg-2);border:1px solid var(--border);border-radius:
|
|
230
|
-
padding:
|
|
363
|
+
background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);
|
|
364
|
+
padding:10px 14px;margin-bottom:16px;
|
|
231
365
|
}
|
|
232
|
-
.copy-bar code{flex:1;font-size:
|
|
366
|
+
.copy-bar code{flex:1;font-size:11px;color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
233
367
|
.copy-btn{
|
|
234
368
|
all:unset;cursor:pointer;color:var(--fg-3);display:flex;align-items:center;
|
|
235
|
-
padding:
|
|
236
|
-
transition:
|
|
369
|
+
padding:4px 8px;border-radius:8px;font-size:11px;gap:4px;flex-shrink:0;
|
|
370
|
+
transition:all .2s;border:1px solid transparent;font-family:var(--mono);
|
|
237
371
|
}
|
|
238
|
-
.copy-btn:hover{color:var(--
|
|
239
|
-
.copy-btn.copied{color:var(--green)}
|
|
372
|
+
.copy-btn:hover{color:var(--accent);border-color:var(--border)}
|
|
373
|
+
.copy-btn.copied{color:var(--green);border-color:var(--green)}
|
|
240
374
|
|
|
241
|
-
/* Utilities */
|
|
242
|
-
.mono{font-family:
|
|
375
|
+
/* ── Utilities ───────────────────────────────────────────────────────────── */
|
|
376
|
+
.mono{font-family:var(--mono);font-size:11px}
|
|
243
377
|
.trunc{max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle}
|
|
244
378
|
.muted{color:var(--fg-2)}
|
|
245
379
|
.dim{color:var(--fg-3)}
|
|
246
|
-
.sm{font-size:
|
|
247
|
-
.empty{text-align:center;padding:
|
|
380
|
+
.sm{font-size:11px}
|
|
381
|
+
.empty{text-align:center;padding:40px 16px;color:var(--fg-3);font-size:12px}
|
|
248
382
|
.flex-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
|
249
|
-
.mt{margin-top:
|
|
250
|
-
.mb{margin-bottom:
|
|
383
|
+
.mt{margin-top:16px}
|
|
384
|
+
.mb{margin-bottom:16px}
|
|
385
|
+
|
|
386
|
+
/* ── Pagination — Pill buttons ───────────────────────────────────────────── */
|
|
387
|
+
.pagination{display:flex;gap:4px;align-items:center;margin-top:20px;justify-content:center}
|
|
388
|
+
.pagination a,.pagination span{
|
|
389
|
+
padding:5px 12px;border-radius:100px;font-size:11px;font-weight:600;
|
|
390
|
+
text-decoration:none;color:var(--fg-3);border:1px solid var(--border);
|
|
391
|
+
transition:all .2s;font-family:var(--mono);
|
|
392
|
+
}
|
|
393
|
+
.pagination a:hover{color:var(--accent);border-color:var(--accent)}
|
|
394
|
+
.pagination .active{background:var(--accent);color:var(--bg-0);border-color:var(--accent)}
|
|
395
|
+
.pagination .disabled{opacity:.3;pointer-events:none}
|
|
396
|
+
|
|
397
|
+
/* ── Filter bar ──────────────────────────────────────────────────────────── */
|
|
398
|
+
.filter-bar{display:flex;gap:8px;align-items:center;margin-bottom:18px;flex-wrap:wrap}
|
|
399
|
+
.filter-bar input,.filter-bar select{
|
|
400
|
+
background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius);
|
|
401
|
+
padding:8px 12px;font-size:11px;color:var(--fg-1);outline:none;
|
|
402
|
+
font-family:var(--mono);transition:all .2s;
|
|
403
|
+
}
|
|
404
|
+
.filter-bar input:focus,.filter-bar select:focus{border-color:var(--accent)}
|
|
405
|
+
.filter-bar input{min-width:200px}
|
|
406
|
+
.filter-bar select{min-width:100px}
|
|
407
|
+
|
|
408
|
+
/* ── Breadcrumbs ─────────────────────────────────────────────────────────── */
|
|
409
|
+
.breadcrumbs{display:flex;gap:6px;align-items:center;font-size:11px;color:var(--fg-3);margin-bottom:14px;font-family:var(--mono)}
|
|
410
|
+
.breadcrumbs a{color:var(--fg-2);text-decoration:none;transition:color .15s}
|
|
411
|
+
.breadcrumbs a:hover{color:var(--accent)}
|
|
412
|
+
.breadcrumbs .sep{color:var(--fg-3);opacity:.5}
|
|
413
|
+
|
|
414
|
+
/* ── Collapsible ─────────────────────────────────────────────────────────── */
|
|
415
|
+
.collapsible{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:8px;transition:border-color .2s}
|
|
416
|
+
.collapsible:hover{border-color:var(--border-2)}
|
|
417
|
+
.collapsible-header{
|
|
418
|
+
display:flex;align-items:center;gap:8px;padding:12px 16px;
|
|
419
|
+
cursor:pointer;background:var(--bg-1);user-select:none;
|
|
420
|
+
transition:background .15s;
|
|
421
|
+
}
|
|
422
|
+
.collapsible-header:hover{background:var(--bg-2)}
|
|
423
|
+
.collapsible-header .chevron{transition:transform .2s cubic-bezier(.4,0,.2,1);font-size:10px;color:var(--fg-3)}
|
|
424
|
+
.collapsible-header.open .chevron{transform:rotate(90deg)}
|
|
425
|
+
.collapsible-body{padding:0 16px 12px;display:none;animation:fadeIn .2s ease-out}
|
|
426
|
+
.collapsible-body.open{display:block}
|
|
427
|
+
|
|
428
|
+
/* ── Waterfall ───────────────────────────────────────────────────────────── */
|
|
429
|
+
.waterfall{display:flex;flex-direction:column;gap:4px}
|
|
430
|
+
.waterfall-row{display:flex;align-items:center;gap:8px;font-size:10px;font-family:var(--mono);animation:fadeIn .3s ease-out both}
|
|
431
|
+
.waterfall-label{width:110px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fg-2);flex-shrink:0}
|
|
432
|
+
.waterfall-track{flex:1;height:18px;background:var(--bg-2);border-radius:100px;position:relative;overflow:hidden}
|
|
433
|
+
.waterfall-bar{position:absolute;height:100%;border-radius:100px;min-width:3px;opacity:.8;transition:opacity .2s}
|
|
434
|
+
.waterfall-row:hover .waterfall-bar{opacity:1}
|
|
435
|
+
.waterfall-dur{width:60px;text-align:right;color:var(--fg-3);flex-shrink:0}
|
|
436
|
+
|
|
437
|
+
/* ── SQL ─────────────────────────────────────────────────────────────────── */
|
|
438
|
+
.sql-kw{color:#a78bfa;font-weight:700}
|
|
439
|
+
.sql-str{color:var(--accent)}
|
|
440
|
+
.sql-num{color:#fbbf24}
|
|
441
|
+
.sql-fn{color:#7dd3fc}
|
|
442
|
+
|
|
443
|
+
/* ── Auto-refresh ────────────────────────────────────────────────────────── */
|
|
444
|
+
.refresh-toggle{
|
|
445
|
+
background:none;border:1px solid var(--border);border-radius:100px;
|
|
446
|
+
padding:5px 12px;cursor:pointer;color:var(--fg-3);font-size:11px;
|
|
447
|
+
display:flex;align-items:center;gap:5px;font-family:var(--mono);font-weight:600;
|
|
448
|
+
transition:all .2s;
|
|
449
|
+
}
|
|
450
|
+
.refresh-toggle:hover{border-color:var(--fg-3);color:var(--fg-2)}
|
|
451
|
+
.refresh-toggle.active{
|
|
452
|
+
border-color:var(--accent);color:var(--accent);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* ── Empty state ─────────────────────────────────────────────────────────── */
|
|
456
|
+
.empty-state{text-align:center;padding:56px 24px;color:var(--fg-3)}
|
|
457
|
+
.empty-state .icon{font-size:36px;margin-bottom:14px;opacity:.4}
|
|
458
|
+
.empty-state .title{font-size:13px;font-weight:700;color:var(--fg-2);margin-bottom:4px;font-family:var(--mono)}
|
|
459
|
+
.empty-state .desc{font-size:11px;font-family:var(--mono)}
|
|
460
|
+
|
|
461
|
+
/* ── Links ───────────────────────────────────────────────────────────────── */
|
|
462
|
+
a{color:var(--accent);transition:color .15s}
|
|
463
|
+
a:hover{color:var(--accent-text)}
|
|
464
|
+
|
|
465
|
+
/* ── Responsive ──────────────────────────────────────────────────────────── */
|
|
466
|
+
@media(max-width:768px){
|
|
467
|
+
.sidebar{display:none}
|
|
468
|
+
main{margin-left:0;padding:16px}
|
|
469
|
+
}
|
|
251
470
|
`
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { HeartbeatServiceProvider } from './HeartbeatServiceProvider.ts'
|
|
|
5
5
|
// ── Contracts ───────────────────────────────────────────────────────────────
|
|
6
6
|
export type {
|
|
7
7
|
EntryType,
|
|
8
|
+
OriginType,
|
|
8
9
|
PendingEntry,
|
|
9
10
|
HeartbeatEntry,
|
|
10
11
|
RequestEntryContent,
|
|
@@ -16,6 +17,7 @@ export type {
|
|
|
16
17
|
ModelEntryContent,
|
|
17
18
|
LogEntryContent,
|
|
18
19
|
ScheduleEntryContent,
|
|
20
|
+
CommandEntryContent,
|
|
19
21
|
SpanStatus,
|
|
20
22
|
StoredSpan,
|
|
21
23
|
ExceptionGroup,
|
|
@@ -44,6 +46,7 @@ export { ModelWatcher } from './watchers/ModelWatcher.ts'
|
|
|
44
46
|
export { LogWatcher } from './watchers/LogWatcher.ts'
|
|
45
47
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher.ts'
|
|
46
48
|
export { MailWatcher } from './watchers/MailWatcher.ts'
|
|
49
|
+
export { CommandWatcher } from './watchers/CommandWatcher.ts'
|
|
47
50
|
|
|
48
51
|
// ── Widget ──────────────────────────────────────────────────────────────────
|
|
49
52
|
export { renderWidget } from './widget/DebugWidget.ts'
|
|
@@ -85,6 +88,12 @@ export { HeartbeatFake } from './testing/HeartbeatFake.ts'
|
|
|
85
88
|
|
|
86
89
|
// ── Dashboard ───────────────────────────────────────────────────────────────
|
|
87
90
|
export { DashboardController } from './dashboard/DashboardController.ts'
|
|
91
|
+
export { renderLogsPage } from './dashboard/pages/LogsPage.ts'
|
|
92
|
+
export { renderModelsPage } from './dashboard/pages/ModelsPage.ts'
|
|
93
|
+
export { renderSchedulesPage } from './dashboard/pages/SchedulesPage.ts'
|
|
94
|
+
export { renderCommandsPage } from './dashboard/pages/CommandsPage.ts'
|
|
95
|
+
export { renderCommandDetailPage } from './dashboard/pages/CommandDetailPage.ts'
|
|
96
|
+
export { renderNotificationsPage } from './dashboard/pages/NotificationsPage.ts'
|
|
88
97
|
|
|
89
98
|
// ── Middleware ──────────────────────────────────────────────────────────
|
|
90
99
|
export { HeartbeatMiddleware } from './middleware/HeartbeatMiddleware.ts'
|
|
@@ -138,9 +138,12 @@ export class MetricsCollector {
|
|
|
138
138
|
const now = Date.now()
|
|
139
139
|
const bucket = Math.floor(now / 60_000) // 1-minute buckets
|
|
140
140
|
|
|
141
|
+
// Snapshot current counters so new increments during the write are not lost
|
|
142
|
+
const counterSnapshot = new Map(this.counters)
|
|
143
|
+
|
|
141
144
|
try {
|
|
142
145
|
// Flush counters
|
|
143
|
-
for (const [key, value] of
|
|
146
|
+
for (const [key, value] of counterSnapshot) {
|
|
144
147
|
const { name, tags } = this.parseMetricKey(key)
|
|
145
148
|
await this.store.insertMetric(name, 'counter', value, tags, 60, bucket)
|
|
146
149
|
}
|
|
@@ -159,10 +162,11 @@ export class MetricsCollector {
|
|
|
159
162
|
await this.store.insertMetric(name, 'histogram', avg, tags, 60, bucket)
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
// Reset counters after
|
|
165
|
+
// Reset counters only after successful write — if the write failed,
|
|
166
|
+
// data is preserved for the next flush attempt.
|
|
163
167
|
this.counters.clear()
|
|
164
168
|
} catch {
|
|
165
|
-
//
|
|
169
|
+
// Write failed — counters are preserved for the next flush attempt
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|