nestjs-profiler 1.0.25 → 1.0.27

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.
Files changed (40) hide show
  1. package/README.md +254 -147
  2. package/collectors/event-collector.d.ts +18 -0
  3. package/collectors/event-collector.js +254 -0
  4. package/collectors/event-collector.js.map +1 -0
  5. package/common/profiler-options.interface.d.ts +8 -0
  6. package/common/profiler.model.d.ts +25 -0
  7. package/controllers/profiler.controller.d.ts +17 -1
  8. package/controllers/profiler.controller.js +129 -17
  9. package/controllers/profiler.controller.js.map +1 -1
  10. package/middleware/profiler-auth.middleware.d.ts +10 -0
  11. package/middleware/profiler-auth.middleware.js +110 -0
  12. package/middleware/profiler-auth.middleware.js.map +1 -0
  13. package/package.json +2 -2
  14. package/profiler.module.js +103 -1
  15. package/profiler.module.js.map +1 -1
  16. package/services/cron-explorer.service.d.ts +48 -0
  17. package/services/cron-explorer.service.js +260 -0
  18. package/services/cron-explorer.service.js.map +1 -0
  19. package/services/entity-explorer.service.d.ts +1 -1
  20. package/services/entity-explorer.service.js +7 -7
  21. package/services/entity-explorer.service.js.map +1 -1
  22. package/services/health.service.js +23 -8
  23. package/services/health.service.js.map +1 -1
  24. package/services/profiler.service.d.ts +10 -1
  25. package/services/profiler.service.js +103 -35
  26. package/services/profiler.service.js.map +1 -1
  27. package/services/route-explorer.service.d.ts +23 -1
  28. package/services/route-explorer.service.js +136 -10
  29. package/services/route-explorer.service.js.map +1 -1
  30. package/services/template-builder.service.d.ts +1 -0
  31. package/services/template-builder.service.js +184 -83
  32. package/services/template-builder.service.js.map +1 -1
  33. package/services/view.service.d.ts +1 -1
  34. package/services/view.service.js +32 -23
  35. package/services/view.service.js.map +1 -1
  36. package/views/cron-jobs.html +635 -0
  37. package/views/events.html +763 -0
  38. package/views/layout.html +40 -9
  39. package/views/login.html +277 -0
  40. package/views/routes.html +405 -37
@@ -0,0 +1,635 @@
1
+ <script>const CRON_DATA = {{{ cronDataJson }}};</script>
2
+
3
+ <style>
4
+ /* ── Animations ─────────────────────────────────────────────────────────── */
5
+ @keyframes pulse-green {
6
+ 0%,100% { box-shadow: 0 0 0 0 rgba(16,185,129,.5); }
7
+ 50% { box-shadow: 0 0 0 6px rgba(16,185,129,0); }
8
+ }
9
+ @keyframes spin-ring {
10
+ to { stroke-dashoffset: 0; }
11
+ }
12
+ @keyframes fade-in {
13
+ from { opacity: 0; transform: translateY(4px); }
14
+ to { opacity: 1; transform: translateY(0); }
15
+ }
16
+ .pulse-green { animation: pulse-green 1.8s ease-in-out infinite; }
17
+ .fade-in { animation: fade-in .25s ease; }
18
+
19
+ /* ── Tab bar ─────────────────────────────────────────────────────────────── */
20
+ .ctab {
21
+ border-bottom: 2px solid transparent;
22
+ color: #6b7280;
23
+ transition: color .15s, border-color .15s;
24
+ }
25
+ .ctab.active {
26
+ border-bottom-color: #4f46e5;
27
+ color: #4f46e5;
28
+ font-weight: 600;
29
+ }
30
+ .ctab:not(.active):hover { color: #374151; }
31
+
32
+ /* ── Cards ───────────────────────────────────────────────────────────────── */
33
+ .job-card {
34
+ background: #fff;
35
+ border: 1px solid #e5e7eb;
36
+ border-radius: 12px;
37
+ border-left: 4px solid #e5e7eb;
38
+ transition: box-shadow .15s, transform .15s;
39
+ }
40
+ .job-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.06); transform: translateY(-1px); }
41
+ .job-card.cron-running { border-left-color: #10b981; }
42
+ .job-card.cron-idle { border-left-color: #6366f1; }
43
+ .job-card.interval-card { border-left-color: #8b5cf6; }
44
+ .job-card.timeout-pending { border-left-color: #f59e0b; }
45
+ .job-card.timeout-fired { border-left-color: #d1d5db; }
46
+
47
+ /* ── Code / mono ─────────────────────────────────────────────────────────── */
48
+ .mono { font-family: 'Fira Code','Cascadia Code',ui-monospace,SFMono-Regular,monospace; }
49
+ .tabnum { font-variant-numeric: tabular-nums; }
50
+
51
+ /* ── Copy button ─────────────────────────────────────────────────────────── */
52
+ .cpw .cpb { opacity: 0; transition: opacity .12s; }
53
+ .cpw:hover .cpb { opacity: 1; }
54
+
55
+ /* ── SVG ring for intervals ─────────────────────────────────────────────── */
56
+ .ring-bg { stroke: #ede9fe; }
57
+ .ring-fg { stroke: #8b5cf6; stroke-linecap: round; }
58
+ </style>
59
+
60
+ <div id="cronRoot" class="space-y-5">
61
+
62
+ <!-- ── Page header ───────────────────────────────────────────────────────── -->
63
+ <div class="flex items-center justify-between">
64
+ <div class="flex items-center gap-3">
65
+ <div class="w-10 h-10 rounded-xl bg-indigo-100 flex items-center justify-center flex-shrink-0">
66
+ <svg class="w-5 h-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
67
+ <circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/>
68
+ </svg>
69
+ </div>
70
+ <div>
71
+ <h1 class="text-xl font-bold text-gray-900">Scheduled Jobs</h1>
72
+ <p id="subTitle" class="text-sm text-gray-500 mt-0.5">Loading…</p>
73
+ </div>
74
+ </div>
75
+ <div class="flex items-center gap-2">
76
+ <span id="lastRefreshed" class="text-xs text-gray-400"></span>
77
+ <button id="refreshBtn"
78
+ class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 shadow-sm transition-colors">
79
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
80
+ <path stroke-linecap="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
81
+ </svg>
82
+ Refresh
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- ── Stat cards row ────────────────────────────────────────────────────── -->
88
+ <div id="statsRow" class="grid grid-cols-2 sm:grid-cols-4 gap-3"></div>
89
+
90
+ <!-- ── Timeline strip ────────────────────────────────────────────────────── -->
91
+ <div id="timelineSection" class="hidden bg-white rounded-xl border border-gray-200 shadow-sm p-4">
92
+ <div class="flex items-center gap-2 mb-4">
93
+ <svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
94
+ <path stroke-linecap="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
95
+ </svg>
96
+ <span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Upcoming — next 3 hours</span>
97
+ </div>
98
+ <!-- Jobs list as timeline items -->
99
+ <div id="timelineList" class="space-y-2"></div>
100
+ </div>
101
+
102
+ <!-- ── Tab bar ────────────────────────────────────────────────────────────── -->
103
+ <div class="flex border-b border-gray-200 gap-0">
104
+ <button class="ctab active px-5 py-2.5 text-sm flex items-center gap-2" data-tab="cron">
105
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
106
+ <circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/>
107
+ </svg>
108
+ Cron Jobs
109
+ <span id="tabCronCount" class="px-1.5 py-0.5 text-xs rounded-full bg-indigo-100 text-indigo-700 font-semibold"></span>
110
+ </button>
111
+ <button class="ctab px-5 py-2.5 text-sm flex items-center gap-2" data-tab="intervals">
112
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
113
+ <path stroke-linecap="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
114
+ </svg>
115
+ Intervals
116
+ <span id="tabIntervalCount" class="px-1.5 py-0.5 text-xs rounded-full bg-violet-100 text-violet-700 font-semibold"></span>
117
+ </button>
118
+ <button class="ctab px-5 py-2.5 text-sm flex items-center gap-2" data-tab="timeouts">
119
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
120
+ <path stroke-linecap="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
121
+ </svg>
122
+ Timeouts
123
+ <span id="tabTimeoutCount" class="px-1.5 py-0.5 text-xs rounded-full bg-amber-100 text-amber-700 font-semibold"></span>
124
+ </button>
125
+ </div>
126
+
127
+ <!-- ── Panels ─────────────────────────────────────────────────────────────── -->
128
+ <div id="panelCron"></div>
129
+ <div id="panelIntervals" class="hidden"></div>
130
+ <div id="panelTimeouts" class="hidden"></div>
131
+
132
+ </div>
133
+
134
+ <script>
135
+ (function () {
136
+ 'use strict';
137
+
138
+ var data = CRON_DATA;
139
+ var activeTab = 'cron';
140
+ var APP_START = Date.now();
141
+
142
+ // ── Utilities ───────────────────────────────────────────────────────────────
143
+
144
+ function esc(s) {
145
+ return String(s || '')
146
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
147
+ .replace(/"/g,'&quot;');
148
+ }
149
+
150
+ function fmtMs(ms) {
151
+ if (!ms) return '—';
152
+ if (ms < 1000) return ms + 'ms';
153
+ if (ms < 60000) {
154
+ var sv = ms / 1000;
155
+ return (sv === Math.floor(sv) ? sv : sv.toFixed(1)) + 's';
156
+ }
157
+ var m = Math.floor(ms / 60000), s = Math.round((ms % 60000) / 1000);
158
+ if (m < 60) return s > 0 ? m + 'm ' + s + 's' : m + 'm';
159
+ var h = Math.floor(m / 60), rm = m % 60;
160
+ return rm > 0 ? h + 'h ' + rm + 'm' : h + 'h';
161
+ }
162
+
163
+ function fmtCountdown(tsMs) {
164
+ if (!tsMs) return '—';
165
+ var diff = tsMs - Date.now();
166
+ if (diff <= 0) return 'due now';
167
+ var s = Math.floor(diff / 1000);
168
+ if (s < 60) return s + 's';
169
+ var m = Math.floor(s / 60), rs = s % 60;
170
+ if (m < 60) return m + 'm ' + (rs > 0 ? rs + 's' : '');
171
+ var h = Math.floor(m / 60), rm = m % 60;
172
+ return h + 'h ' + (rm > 0 ? rm + 'm' : '');
173
+ }
174
+
175
+ function fmtRelative(tsMs) {
176
+ if (!tsMs) return 'never';
177
+ var diff = Date.now() - tsMs;
178
+ if (diff < 10000) return 'just now';
179
+ var s = Math.floor(diff / 1000);
180
+ if (s < 60) return s + 's ago';
181
+ var m = Math.floor(s / 60);
182
+ if (m < 60) return m + 'm ago';
183
+ var h = Math.floor(m / 60);
184
+ if (h < 24) return h + 'h ago';
185
+ return Math.floor(h / 24) + 'd ago';
186
+ }
187
+
188
+ function shortenFile(f) {
189
+ if (!f) return '';
190
+ return f.replace(/^.*\/(src\/)/, 'src/');
191
+ }
192
+
193
+ // Cycle progress (0-1): how far through the current period until next run
194
+ function cycleFraction(nextRunMs, humanLabel) {
195
+ if (!nextRunMs) return null;
196
+ var periodMap = {
197
+ 'Every minute': 60000, 'Every hour': 3600000,
198
+ 'Daily': 86400000, 'Weekdays': 86400000,
199
+ };
200
+ var periodMs = 0;
201
+ for (var k in periodMap) {
202
+ if (humanLabel && humanLabel.indexOf(k) === 0) { periodMs = periodMap[k]; break; }
203
+ }
204
+ if (!periodMs) {
205
+ var mMin = humanLabel && humanLabel.match(/Every (\d+) min/);
206
+ if (mMin) periodMs = parseInt(mMin[1]) * 60000;
207
+ var mHr = humanLabel && humanLabel.match(/Every (\d+) hour/);
208
+ if (mHr) periodMs = parseInt(mHr[1]) * 3600000;
209
+ }
210
+ if (!periodMs) return null;
211
+ var timeUntil = nextRunMs - Date.now();
212
+ if (timeUntil < 0) return 1; // overdue = full
213
+ return Math.max(0, Math.min(1, 1 - timeUntil / periodMs));
214
+ }
215
+
216
+ // ── Shared components ───────────────────────────────────────────────────────
217
+
218
+ function methodBadge(className, methodName, handler) {
219
+ if (className && methodName) {
220
+ return '<span class="inline-flex items-baseline gap-0.5 px-2 py-0.5 rounded bg-gray-100 text-xs mono border border-gray-200">' +
221
+ '<span class="text-gray-500">' + esc(className) + '</span>' +
222
+ '<span class="text-gray-400">.</span>' +
223
+ '<span class="text-indigo-600 font-medium">' + esc(methodName) + '</span>' +
224
+ '</span>';
225
+ }
226
+ if (handler) return '<span class="px-2 py-0.5 rounded bg-gray-100 text-xs mono text-gray-600 border border-gray-200">' + esc(handler) + '</span>';
227
+ return '';
228
+ }
229
+
230
+ function fileChip(file) {
231
+ if (!file) return '';
232
+ var short = shortenFile(file);
233
+ return '<span class="cpw inline-flex items-center gap-1">' +
234
+ '<svg class="w-3 h-3 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>' +
235
+ '<span class="text-xs text-gray-400 mono">' + esc(short) + '</span>' +
236
+ '<button class="cpb w-4 h-4 text-gray-400 hover:text-gray-600 flex items-center justify-center" title="Copy" onclick="(function(e){e.stopPropagation();try{navigator.clipboard.writeText(\'' + file.replace(/\\/g,'\\\\').replace(/'/g,"\\'") + '\');}catch(x){};})(event)">' +
237
+ '<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>' +
238
+ '</button></span>';
239
+ }
240
+
241
+ // ── Cron panel ──────────────────────────────────────────────────────────────
242
+
243
+ function renderCronPanel(jobs) {
244
+ if (!data.hasScheduler) return noScheduler();
245
+ if (!jobs || !jobs.length) return emptyState('No cron jobs', 'Decorate a method with <code class="bg-gray-100 px-1 rounded text-indigo-600 mono text-xs">@Cron(expression, { name })</code>');
246
+ return jobs.map(function(job) {
247
+ var isRunning = !!job.running;
248
+ var frac = cycleFraction(job.nextRun, job.expressionHuman);
249
+ var pct = frac !== null ? Math.round(frac * 100) : null;
250
+ var cardCls = 'job-card ' + (isRunning ? 'cron-running' : 'cron-idle');
251
+
252
+ return '<div class="' + cardCls + ' mb-3 fade-in">' +
253
+ '<div class="px-5 pt-4 pb-3">' +
254
+ '<div class="flex flex-col sm:flex-row sm:items-start gap-4">' +
255
+
256
+ // ── Left ──────────────────────────────────────────────────────
257
+ '<div class="flex items-start gap-3 flex-1 min-w-0">' +
258
+ // Status dot
259
+ '<div class="mt-1.5 flex-shrink-0">' +
260
+ (isRunning
261
+ ? '<span class="inline-block w-2.5 h-2.5 rounded-full bg-emerald-500 pulse-green"></span>'
262
+ : '<span class="inline-block w-2.5 h-2.5 rounded-full bg-indigo-400"></span>') +
263
+ '</div>' +
264
+
265
+ '<div class="min-w-0 flex-1">' +
266
+ // Name + badges
267
+ '<div class="flex flex-wrap items-center gap-2 mb-2">' +
268
+ '<span class="font-semibold text-gray-900">' + esc(job.name) + '</span>' +
269
+ (isRunning
270
+ ? '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700"><span class="w-1.5 h-1.5 rounded-full bg-emerald-500 pulse-green inline-block"></span>Running</span>'
271
+ : '<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-700">Scheduled</span>') +
272
+ methodBadge(job.className, job.methodName, job.handler) +
273
+ '</div>' +
274
+
275
+ // Cron expression + human label
276
+ '<div class="flex flex-wrap items-center gap-2 mb-2">' +
277
+ '<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-gray-100 border border-gray-200 text-xs mono text-gray-700">' +
278
+ '<svg class="w-3 h-3 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/></svg>' +
279
+ esc(job.expression) +
280
+ '</span>' +
281
+ (job.expressionHuman && job.expressionHuman !== job.expression
282
+ ? '<span class="px-2 py-1 rounded-lg bg-indigo-50 text-xs text-indigo-700 font-medium border border-indigo-100">' + esc(job.expressionHuman) + '</span>'
283
+ : '') +
284
+ '</div>' +
285
+
286
+ fileChip(job.file) +
287
+ '</div>' +
288
+ '</div>' +
289
+
290
+ // ── Right: timing ─────────────────────────────────────────────
291
+ '<div class="flex-shrink-0 grid grid-cols-2 gap-x-6 text-right sm:text-right mt-1">' +
292
+ '<div>' +
293
+ '<div class="text-xs text-gray-400 mb-1">Next run</div>' +
294
+ (isRunning
295
+ ? '<div class="text-sm font-semibold text-emerald-600">Running…</div>'
296
+ : '<div class="text-xl font-bold text-gray-900 tabnum countdown" data-next="' + (job.nextRun || 0) + '">' + fmtCountdown(job.nextRun) + '</div>') +
297
+ '</div>' +
298
+ '<div>' +
299
+ '<div class="text-xs text-gray-400 mb-1">Last run</div>' +
300
+ '<div class="text-sm font-medium text-gray-600 tabnum reltime" data-ts="' + (job.lastRun || 0) + '">' + fmtRelative(job.lastRun) + '</div>' +
301
+ '</div>' +
302
+ '</div>' +
303
+
304
+ '</div>' +
305
+ '</div>' +
306
+
307
+ // ── Cycle progress bar ─────────────────────────────────────────────
308
+ (pct !== null
309
+ ? '<div class="px-5 pb-3">' +
310
+ '<div class="flex items-center gap-2">' +
311
+ '<div class="flex-1 bg-gray-100 rounded-full h-1.5 overflow-hidden">' +
312
+ '<div class="h-1.5 rounded-full bg-gradient-to-r from-indigo-400 to-indigo-600 transition-all duration-1000" style="width:' + pct + '%"></div>' +
313
+ '</div>' +
314
+ '<span class="text-xs text-gray-400 tabnum w-8 text-right">' + pct + '%</span>' +
315
+ '</div></div>'
316
+ : '') +
317
+
318
+ '</div>';
319
+ }).join('');
320
+ }
321
+
322
+ // ── Intervals panel ─────────────────────────────────────────────────────────
323
+
324
+ function intervalRing(ms) {
325
+ var circ = 125.7; // 2π×20
326
+ var dur = Math.min(10, Math.max(0.5, ms / 1000)).toFixed(2) + 's';
327
+ return '<svg width="48" height="48" viewBox="0 0 48 48" class="flex-shrink-0">' +
328
+ '<circle class="ring-bg" cx="24" cy="24" r="20" fill="none" stroke-width="4"/>' +
329
+ '<circle class="ring-fg" cx="24" cy="24" r="20" fill="none" stroke-width="4"' +
330
+ ' stroke-dasharray="' + circ + '" stroke-dashoffset="' + circ + '"' +
331
+ ' transform="rotate(-90 24 24)">' +
332
+ '<animate attributeName="stroke-dashoffset" from="' + circ + '" to="0"' +
333
+ ' dur="' + dur + '" repeatCount="indefinite"/>' +
334
+ '</circle>' +
335
+ '<text x="24" y="28" text-anchor="middle" font-size="9" fill="#7c3aed" font-family="ui-monospace,monospace" font-weight="600">' +
336
+ (ms < 60000 ? (ms/1000).toFixed(0)+'s' : Math.floor(ms/60000)+'m') +
337
+ '</text>' +
338
+ '</svg>';
339
+ }
340
+
341
+ function renderIntervalsPanel(intervals) {
342
+ if (!data.hasScheduler) return noScheduler();
343
+ if (!intervals || !intervals.length) return emptyState('No intervals', 'Decorate a method with <code class="bg-gray-100 px-1 rounded text-indigo-600 mono text-xs">@Interval(\'name\', ms)</code>');
344
+ return intervals.map(function(iv) {
345
+ var freq = iv.ms > 0 ? fmtMs(iv.ms) : null;
346
+ var perMin = iv.ms > 0 ? +(60000 / iv.ms).toFixed(1) : null;
347
+
348
+ return '<div class="job-card interval-card mb-3 fade-in">' +
349
+ '<div class="px-5 py-4">' +
350
+ '<div class="flex items-center gap-4">' +
351
+
352
+ // Animated ring
353
+ (iv.ms > 0 ? intervalRing(iv.ms) : '<div class="w-12 h-12 rounded-full bg-violet-100 flex-shrink-0"></div>') +
354
+
355
+ // Info
356
+ '<div class="flex-1 min-w-0">' +
357
+ '<div class="flex flex-wrap items-center gap-2 mb-1.5">' +
358
+ '<span class="font-semibold text-gray-900">' + esc(iv.name) + '</span>' +
359
+ '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-violet-100 text-violet-700">' +
360
+ '<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>' +
361
+ 'Interval' +
362
+ '</span>' +
363
+ methodBadge(iv.className, iv.methodName, iv.handler) +
364
+ '</div>' +
365
+
366
+ '<div class="flex flex-wrap items-center gap-2 mb-2">' +
367
+ (freq
368
+ ? '<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-violet-50 border border-violet-200 text-sm font-semibold text-violet-800">' +
369
+ '<svg class="w-3.5 h-3.5 text-violet-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>' +
370
+ 'Every ' + freq + '</span>'
371
+ : '<span class="text-xs text-gray-400">interval not resolved</span>') +
372
+ (perMin && perMin > 0 && perMin < 1000
373
+ ? '<span class="text-xs text-gray-400">' + perMin + '× / min</span>'
374
+ : '') +
375
+ '</div>' +
376
+
377
+ fileChip(iv.file) +
378
+ '</div>' +
379
+
380
+ // Period display right side
381
+ (freq ? '<div class="hidden sm:block text-right flex-shrink-0">' +
382
+ '<div class="text-2xl font-bold text-violet-700 tabnum">' + freq + '</div>' +
383
+ '<div class="text-xs text-gray-400 mt-0.5">repeat period</div>' +
384
+ '</div>' : '') +
385
+
386
+ '</div>' +
387
+ '</div>' +
388
+ '</div>';
389
+ }).join('');
390
+ }
391
+
392
+ // ── Timeouts panel ──────────────────────────────────────────────────────────
393
+
394
+ function renderTimeoutsPanel(timeouts) {
395
+ if (!data.hasScheduler) return noScheduler();
396
+ if (!timeouts || !timeouts.length) return emptyState('No timeouts', 'Decorate a method with <code class="bg-gray-100 px-1 rounded text-indigo-600 mono text-xs">@Timeout(\'name\', ms)</code>');
397
+ return timeouts.map(function(to) {
398
+ var fireAt = APP_START + to.ms;
399
+ var fired = to.ms > 0 && Date.now() > fireAt;
400
+ var remain = fired ? 0 : fireAt - Date.now();
401
+ var cls = fired ? 'timeout-fired' : 'timeout-pending';
402
+
403
+ return '<div class="job-card ' + cls + ' mb-3 fade-in">' +
404
+ '<div class="px-5 py-4">' +
405
+ '<div class="flex items-center gap-4">' +
406
+
407
+ // Status icon box
408
+ '<div class="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ' +
409
+ (fired ? 'bg-gray-100 border border-gray-200' : 'bg-amber-50 border border-amber-200') + '">' +
410
+ (fired
411
+ ? '<svg class="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M5 13l4 4L19 7"/></svg>'
412
+ : '<svg class="w-5 h-5 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>') +
413
+ '</div>' +
414
+
415
+ // Info
416
+ '<div class="flex-1 min-w-0">' +
417
+ '<div class="flex flex-wrap items-center gap-2 mb-1.5">' +
418
+ '<span class="font-semibold ' + (fired ? 'text-gray-500' : 'text-gray-900') + '">' + esc(to.name) + '</span>' +
419
+ (fired
420
+ ? '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-500 border border-gray-200"><svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M5 13l4 4L19 7"/></svg>Fired</span>'
421
+ : '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-700"><span class="w-1.5 h-1.5 rounded-full bg-amber-500 pulse-green inline-block"></span>Pending</span>') +
422
+ methodBadge(to.className, to.methodName, to.handler) +
423
+ '</div>' +
424
+
425
+ '<div class="flex flex-wrap items-center gap-2 mb-2">' +
426
+ (to.ms > 0
427
+ ? '<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-sm font-semibold border ' +
428
+ (fired
429
+ ? 'bg-gray-50 border-gray-200 text-gray-500'
430
+ : 'bg-amber-50 border-amber-200 text-amber-800') + '">' +
431
+ '<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>' +
432
+ (fired ? 'Fired after ' : 'Fires in ') + fmtMs(to.ms) +
433
+ '</span>'
434
+ : '') +
435
+ (!fired && to.ms > 0
436
+ ? '<span class="text-xs text-gray-500 tabnum timeout-cd" data-fire-at="' + fireAt + '">~' + fmtMs(remain) + ' remaining</span>'
437
+ : (fired ? '<span class="text-xs text-gray-400">one-shot · removed from registry</span>' : '')) +
438
+ '</div>' +
439
+
440
+ fileChip(to.file) +
441
+ '</div>' +
442
+
443
+ // Right: delay
444
+ (to.ms > 0 ? '<div class="hidden sm:block text-right flex-shrink-0">' +
445
+ '<div class="text-2xl font-bold tabnum ' + (fired ? 'text-gray-400' : 'text-amber-600') + '">' + fmtMs(to.ms) + '</div>' +
446
+ '<div class="text-xs text-gray-400 mt-0.5">one-shot delay</div>' +
447
+ '</div>' : '') +
448
+
449
+ '</div>' +
450
+ '</div>' +
451
+ '</div>';
452
+ }).join('');
453
+ }
454
+
455
+ // ── Stat cards ──────────────────────────────────────────────────────────────
456
+
457
+ function renderStats(d) {
458
+ var running = (d.cronJobs||[]).filter(function(j){return j.running;}).length;
459
+ var cards = [
460
+ { v:(d.cronJobs||[]).length+(d.intervals||[]).length+(d.timeouts||[]).length,
461
+ label:'Total Jobs', bg:'bg-gray-50', border:'border-gray-200',
462
+ iconBg:'bg-gray-200', iconColor:'text-gray-600',
463
+ path:'<path stroke-linecap="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>' },
464
+ { v:(d.cronJobs||[]).length, label:'Cron Jobs',
465
+ bg:'bg-indigo-50', border:'border-indigo-200', iconBg:'bg-indigo-200', iconColor:'text-indigo-700',
466
+ path:'<circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/>' },
467
+ { v:(d.intervals||[]).length, label:'Intervals',
468
+ bg:'bg-violet-50', border:'border-violet-200', iconBg:'bg-violet-200', iconColor:'text-violet-700',
469
+ path:'<path stroke-linecap="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>' },
470
+ { v:(d.timeouts||[]).length, label:'Timeouts',
471
+ bg:'bg-amber-50', border:'border-amber-200', iconBg:'bg-amber-200', iconColor:'text-amber-700',
472
+ path:'<path stroke-linecap="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>' },
473
+ ];
474
+ if (running > 0) cards[0] = { v:running, label:'Running Now',
475
+ bg:'bg-emerald-50', border:'border-emerald-200', iconBg:'bg-emerald-200', iconColor:'text-emerald-700',
476
+ path:'<path stroke-linecap="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>' };
477
+
478
+ return cards.map(function(c) {
479
+ return '<div class="rounded-xl border ' + c.border + ' ' + c.bg + ' p-4 flex items-center gap-3">' +
480
+ '<div class="w-9 h-9 rounded-lg ' + c.iconBg + ' flex items-center justify-center flex-shrink-0">' +
481
+ '<svg class="w-4 h-4 ' + c.iconColor + '" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">' + c.path + '</svg>' +
482
+ '</div>' +
483
+ '<div><div class="text-2xl font-bold text-gray-900">' + c.v + '</div>' +
484
+ '<div class="text-xs text-gray-500">' + c.label + '</div></div>' +
485
+ '</div>';
486
+ }).join('');
487
+ }
488
+
489
+ // ── Timeline ────────────────────────────────────────────────────────────────
490
+
491
+ function renderTimeline(jobs) {
492
+ var section = document.getElementById('timelineSection');
493
+ var list = document.getElementById('timelineList');
494
+ if (!jobs || !jobs.length) { section.classList.add('hidden'); return; }
495
+
496
+ // Sort by nextRun (including overdue which we show as "due now")
497
+ var sorted = jobs.slice().sort(function(a,b) {
498
+ return (a.nextRun || 0) - (b.nextRun || 0);
499
+ });
500
+
501
+ var horizon = Date.now() + 3 * 3600000;
502
+ var visible = sorted.filter(function(j) {
503
+ return j.nextRun && j.nextRun <= horizon;
504
+ });
505
+
506
+ if (!visible.length) { section.classList.add('hidden'); return; }
507
+ section.classList.remove('hidden');
508
+
509
+ var colors = [
510
+ { dot:'bg-indigo-500', text:'text-indigo-700', bg:'bg-indigo-50', border:'border-indigo-200' },
511
+ { dot:'bg-violet-500', text:'text-violet-700', bg:'bg-violet-50', border:'border-violet-200' },
512
+ { dot:'bg-emerald-500', text:'text-emerald-700', bg:'bg-emerald-50', border:'border-emerald-200' },
513
+ { dot:'bg-rose-500', text:'text-rose-700', bg:'bg-rose-50', border:'border-rose-200' },
514
+ { dot:'bg-amber-500', text:'text-amber-700', bg:'bg-amber-50', border:'border-amber-200' },
515
+ { dot:'bg-cyan-500', text:'text-cyan-700', bg:'bg-cyan-50', border:'border-cyan-200' },
516
+ ];
517
+
518
+ list.innerHTML = visible.map(function(job, i) {
519
+ var c = colors[i % colors.length];
520
+ var isOverdue = job.nextRun < Date.now();
521
+ var timeLabel = isOverdue ? 'due now' : 'in ' + fmtCountdown(job.nextRun);
522
+ return '<div class="flex items-center gap-3 p-2.5 rounded-lg border ' + c.border + ' ' + c.bg + '">' +
523
+ '<span class="w-2.5 h-2.5 rounded-full flex-shrink-0 ' + c.dot + (isOverdue ? ' pulse-green' : '') + '"></span>' +
524
+ '<span class="font-medium text-sm text-gray-800 flex-1">' + esc(job.name) + '</span>' +
525
+ '<span class="text-xs font-semibold ' + c.text + ' tabnum countdown-tl" data-next="' + (job.nextRun||0) + '">' + timeLabel + '</span>' +
526
+ '<span class="text-xs text-gray-400 mono">' + esc(job.expressionHuman || job.expression) + '</span>' +
527
+ '</div>';
528
+ }).join('');
529
+ }
530
+
531
+ // ── Empty / no scheduler ─────────────────────────────────────────────────────
532
+
533
+ function emptyState(title, sub) {
534
+ return '<div class="bg-white rounded-xl border border-gray-200 p-12 text-center">' +
535
+ '<div class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-4">' +
536
+ '<svg class="w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/></svg>' +
537
+ '</div>' +
538
+ '<p class="font-semibold text-gray-700 mb-1">' + title + '</p>' +
539
+ '<p class="text-sm text-gray-400 max-w-xs mx-auto">' + sub + '</p>' +
540
+ '</div>';
541
+ }
542
+
543
+ function noScheduler() {
544
+ return '<div class="bg-white rounded-xl border border-dashed border-gray-300 p-12 text-center">' +
545
+ '<div class="w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-5">' +
546
+ '<svg class="w-7 h-7 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7v5l3 3"/></svg>' +
547
+ '</div>' +
548
+ '<p class="font-semibold text-gray-800 text-lg mb-1">@nestjs/schedule not detected</p>' +
549
+ '<p class="text-sm text-gray-500 mb-6 max-w-sm mx-auto">Install the package and add <code class="bg-gray-100 px-1 rounded mono text-xs">ScheduleModule.forRoot()</code> to your AppModule.</p>' +
550
+ '<div class="inline-block text-left bg-gray-900 rounded-xl px-5 py-4">' +
551
+ '<code class="text-xs text-emerald-400 mono block mb-1">npm install @nestjs/schedule</code>' +
552
+ '<code class="text-xs text-gray-400 mono block">ScheduleModule.forRoot() // AppModule imports</code>' +
553
+ '</div>' +
554
+ '</div>';
555
+ }
556
+
557
+ // ── Main render ─────────────────────────────────────────────────────────────
558
+
559
+ function render(d) {
560
+ data = d;
561
+ var total = (d.cronJobs||[]).length + (d.intervals||[]).length + (d.timeouts||[]).length;
562
+ var running = (d.cronJobs||[]).filter(function(j){return j.running;}).length;
563
+
564
+ document.getElementById('statsRow').innerHTML = renderStats(d);
565
+ document.getElementById('tabCronCount').textContent = (d.cronJobs||[]).length;
566
+ document.getElementById('tabIntervalCount').textContent = (d.intervals||[]).length;
567
+ document.getElementById('tabTimeoutCount').textContent = (d.timeouts||[]).length;
568
+ document.getElementById('subTitle').textContent = d.hasScheduler
569
+ ? total + ' job' + (total !== 1 ? 's' : '') + (running > 0 ? ' · ' + running + ' running now' : '')
570
+ : '@nestjs/schedule not detected';
571
+ document.getElementById('lastRefreshed').textContent = 'Updated ' + new Date(d.scannedAt).toLocaleTimeString();
572
+
573
+ document.getElementById('panelCron').innerHTML = renderCronPanel(d.cronJobs);
574
+ document.getElementById('panelIntervals').innerHTML = renderIntervalsPanel(d.intervals);
575
+ document.getElementById('panelTimeouts').innerHTML = renderTimeoutsPanel(d.timeouts);
576
+
577
+ if (activeTab === 'cron') renderTimeline(d.cronJobs);
578
+ }
579
+
580
+ // ── Ticker ──────────────────────────────────────────────────────────────────
581
+
582
+ function tick() {
583
+ document.querySelectorAll('.countdown[data-next]').forEach(function(el) {
584
+ var ts = parseInt(el.getAttribute('data-next'));
585
+ if (ts) el.textContent = fmtCountdown(ts);
586
+ });
587
+ document.querySelectorAll('.countdown-tl[data-next]').forEach(function(el) {
588
+ var ts = parseInt(el.getAttribute('data-next'));
589
+ if (!ts) return;
590
+ el.textContent = ts < Date.now() ? 'due now' : 'in ' + fmtCountdown(ts);
591
+ });
592
+ document.querySelectorAll('.reltime[data-ts]').forEach(function(el) {
593
+ var ts = parseInt(el.getAttribute('data-ts'));
594
+ if (ts) el.textContent = fmtRelative(ts);
595
+ });
596
+ document.querySelectorAll('.timeout-cd[data-fire-at]').forEach(function(el) {
597
+ var at = parseInt(el.getAttribute('data-fire-at'));
598
+ var r = at - Date.now();
599
+ el.textContent = r > 0 ? '~' + fmtMs(r) + ' remaining' : 'fired';
600
+ });
601
+ }
602
+
603
+ // ── Auto-refresh ────────────────────────────────────────────────────────────
604
+
605
+ function refresh() {
606
+ fetch('/__profiler/api/cron-jobs')
607
+ .then(function(r) { return r.json(); })
608
+ .then(function(d) { render(d); })
609
+ .catch(function() {});
610
+ }
611
+
612
+ // ── Tabs ────────────────────────────────────────────────────────────────────
613
+
614
+ document.querySelectorAll('.ctab').forEach(function(btn) {
615
+ btn.addEventListener('click', function() {
616
+ activeTab = btn.getAttribute('data-tab');
617
+ document.querySelectorAll('.ctab').forEach(function(b) { b.classList.remove('active'); });
618
+ btn.classList.add('active');
619
+ ['cron','intervals','timeouts'].forEach(function(t) {
620
+ document.getElementById('panel' + t.charAt(0).toUpperCase() + t.slice(1))
621
+ .classList.toggle('hidden', activeTab !== t);
622
+ });
623
+ document.getElementById('timelineSection').classList.toggle('hidden', activeTab !== 'cron');
624
+ if (activeTab === 'cron') renderTimeline(data.cronJobs);
625
+ });
626
+ });
627
+
628
+ document.getElementById('refreshBtn').addEventListener('click', refresh);
629
+
630
+ render(data);
631
+ setInterval(tick, 1000);
632
+ setInterval(refresh, 15000);
633
+
634
+ })();
635
+ </script>