@logboard/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.env.example +37 -0
  2. package/README.md +200 -0
  3. package/bin/logboard +536 -0
  4. package/client/logger.js +309 -0
  5. package/config/index.js +142 -0
  6. package/config.js +2 -0
  7. package/controllers/AnalyticsController.js +46 -0
  8. package/controllers/ApiAnalyticsController.js +129 -0
  9. package/controllers/ApiKeyController.js +58 -0
  10. package/controllers/AuthController.js +131 -0
  11. package/controllers/HealthController.js +56 -0
  12. package/controllers/LogController.js +197 -0
  13. package/controllers/OrgController.js +152 -0
  14. package/controllers/RoleConfigController.js +20 -0
  15. package/controllers/SettingsController.js +39 -0
  16. package/controllers/StreamController.js +55 -0
  17. package/controllers/UiController.js +789 -0
  18. package/controllers/UserController.js +79 -0
  19. package/lib/batchWriter.js +57 -0
  20. package/lib/cleanup.js +67 -0
  21. package/lib/ejs.js +103 -0
  22. package/lib/emitter.js +5 -0
  23. package/lib/healthMonitor.js +245 -0
  24. package/lib/logger.js +21 -0
  25. package/lib/streams.js +32 -0
  26. package/lib/theme.js +77 -0
  27. package/lib/userStore.js +13 -0
  28. package/lib/utils.js +44 -0
  29. package/middleware/apiKey.js +82 -0
  30. package/middleware/auth.js +55 -0
  31. package/middleware/ipWhitelist.js +59 -0
  32. package/middleware/org.js +85 -0
  33. package/middleware/pageAccess.js +20 -0
  34. package/middleware/rateLimit.js +29 -0
  35. package/middleware/roles.js +11 -0
  36. package/package.json +77 -0
  37. package/routes/alerts.js +18 -0
  38. package/routes/analytics.js +26 -0
  39. package/routes/api-analytics.js +30 -0
  40. package/routes/api-keys.js +12 -0
  41. package/routes/archive.js +91 -0
  42. package/routes/audit.js +50 -0
  43. package/routes/auth.js +22 -0
  44. package/routes/bookmarks.js +13 -0
  45. package/routes/health.js +11 -0
  46. package/routes/logs.js +88 -0
  47. package/routes/metrics.js +66 -0
  48. package/routes/notifications.js +14 -0
  49. package/routes/orgs.js +98 -0
  50. package/routes/registration.js +202 -0
  51. package/routes/role-config.js +97 -0
  52. package/routes/saved-searches.js +12 -0
  53. package/routes/server.js +151 -0
  54. package/routes/settings.js +28 -0
  55. package/routes/status.js +21 -0
  56. package/routes/stream.js +11 -0
  57. package/routes/super.js +129 -0
  58. package/routes/ui.js +120 -0
  59. package/routes/users.js +13 -0
  60. package/server.js +172 -0
  61. package/services/AlertRulesService.js +323 -0
  62. package/services/AnalyticsService.js +665 -0
  63. package/services/ApiAnalyticsService.js +471 -0
  64. package/services/ApiKeyService.js +166 -0
  65. package/services/AuditService.js +249 -0
  66. package/services/AuthService.js +234 -0
  67. package/services/BookmarkService.js +49 -0
  68. package/services/GlobalSettingsService.js +44 -0
  69. package/services/LogService.js +1066 -0
  70. package/services/MetricsService.js +116 -0
  71. package/services/NotificationService.js +70 -0
  72. package/services/OrgService.js +217 -0
  73. package/services/ReportService.js +247 -0
  74. package/services/RoleConfigService.js +201 -0
  75. package/services/SavedSearchService.js +63 -0
  76. package/services/SettingsService.js +220 -0
  77. package/services/UserService.js +121 -0
  78. package/setup.js +132 -0
  79. package/views/404.ejs +8 -0
  80. package/views/alerts.ejs +190 -0
  81. package/views/analytics.ejs +209 -0
  82. package/views/api-analytics.ejs +660 -0
  83. package/views/api-keys.ejs +150 -0
  84. package/views/archive.ejs +123 -0
  85. package/views/audit.ejs +314 -0
  86. package/views/bookmarks.ejs +54 -0
  87. package/views/custom-dashboard.ejs +162 -0
  88. package/views/dashboard.ejs +186 -0
  89. package/views/diff.ejs +98 -0
  90. package/views/health.ejs +269 -0
  91. package/views/heatmap.ejs +126 -0
  92. package/views/insights.ejs +334 -0
  93. package/views/invite.ejs +74 -0
  94. package/views/live.ejs +299 -0
  95. package/views/login.ejs +64 -0
  96. package/views/logo.png +0 -0
  97. package/views/logs.ejs +754 -0
  98. package/views/notifications.ejs +58 -0
  99. package/views/partials/head.ejs +282 -0
  100. package/views/partials/sidebar.ejs +168 -0
  101. package/views/register.ejs +100 -0
  102. package/views/roles.ejs +279 -0
  103. package/views/saved-searches.ejs +51 -0
  104. package/views/service-map.ejs +142 -0
  105. package/views/settings.ejs +1159 -0
  106. package/views/sidebar.ejs +129 -0
  107. package/views/status.ejs +100 -0
  108. package/views/super-admin-admins.ejs +58 -0
  109. package/views/super-admin-analytics.ejs +49 -0
  110. package/views/super-admin-orgs.ejs +310 -0
  111. package/views/super-admin-profile.ejs +77 -0
  112. package/views/super-admin-settings.ejs +108 -0
  113. package/views/super-admin-system.ejs +46 -0
  114. package/views/users.ejs +153 -0
@@ -0,0 +1,789 @@
1
+ 'use strict';
2
+ const config = require('../config');
3
+ const { getToday } = require('../lib/utils');
4
+
5
+ class UiController {
6
+ constructor (logService, analyticsService) {
7
+ this.logs = logService;
8
+ this.analytics = analyticsService;
9
+ }
10
+
11
+ // Returns an org-scoped LogService for the current request
12
+ _logSvc (req) {
13
+ const LogService = require('../services/LogService');
14
+ return new LogService(this.logs.batchWriter, req.org);
15
+ }
16
+
17
+ // Returns an org-scoped AnalyticsService for the current request
18
+ _analyticsSvc (req) {
19
+ const AnalyticsService = require('../services/AnalyticsService');
20
+ return new AnalyticsService(req.org);
21
+ }
22
+
23
+ async _base (req, extra = {}) {
24
+ try {
25
+ const { RoleConfigService } = require('../services/RoleConfigService');
26
+ const SettingsService = require('../services/SettingsService');
27
+ const org = req.org || null;
28
+ const role = req.user?.role || 'viewer';
29
+
30
+ // Super-admin: no org context, use global settings only
31
+ if (role === 'super-admin') {
32
+ const { buildThemeSnippet } = require('../lib/theme');
33
+ let settings = {};
34
+ try { settings = await new SettingsService(null).get(); } catch {}
35
+ return {
36
+ user: req.user, allowedPages: [], allowedCards: [],
37
+ settings, appName: settings.appName || 'LogBoard',
38
+ appLogoUrl: settings.appLogoUrl || '/public/logo.png',
39
+ themeSnippet: buildThemeSnippet(settings), ...extra,
40
+ };
41
+ }
42
+
43
+ const rc = new RoleConfigService(org);
44
+ const [allowedPages, allowedCards, settings] = await Promise.all([
45
+ rc.getAllowedPages(role),
46
+ rc.getAllowedCards(role),
47
+ new SettingsService(org).get(),
48
+ ]);
49
+ const { buildThemeSnippet } = require('../lib/theme');
50
+ return {
51
+ user: req.user,
52
+ allowedPages,
53
+ allowedCards,
54
+ settings,
55
+ appName: settings.appName || 'LogBoard',
56
+ appLogoUrl: settings.appLogoUrl || '/public/logo.png',
57
+ themeSnippet: buildThemeSnippet(settings),
58
+ ...extra,
59
+ };
60
+ } catch {
61
+ return {
62
+ user: req.user,
63
+ allowedPages: null,
64
+ allowedCards: [],
65
+ settings: {},
66
+ appName: 'LogBoard',
67
+ appLogoUrl: '/public/logo.png',
68
+ themeSnippet: '',
69
+ ...extra,
70
+ };
71
+ }
72
+ }
73
+
74
+ async loginPage (req, res) {
75
+ try {
76
+ const SettingsService = require('../services/SettingsService');
77
+ const s = await new SettingsService(req.org).get();
78
+ res.render('login', {
79
+ title: 'Login',
80
+ error: null,
81
+ settings: s,
82
+ themeSnippet: require('../lib/theme').buildThemeSnippet(s),
83
+ appName: s.appName || 'LogBoard',
84
+ appLogoUrl: s.appLogoUrl || '/public/logo.png',
85
+ });
86
+ } catch {
87
+ res.render('login', {
88
+ title: 'Login',
89
+ error: null,
90
+ settings: {},
91
+ appName: 'LogBoard',
92
+ appLogoUrl: '/public/logo.png',
93
+ });
94
+ }
95
+ }
96
+
97
+ async dashboard (req, res) {
98
+ try {
99
+ const { RoleConfigService } = require('../services/RoleConfigService');
100
+ const AnalyticsService = require('../services/AnalyticsService');
101
+ const role = req.user?.role || 'viewer';
102
+ const allowedApps = await new RoleConfigService(req.org).getAllowedApps(
103
+ role,
104
+ );
105
+ const [overview, hourly, errors, trend] = await Promise.all([
106
+ new AnalyticsService(req.org).getOverview(allowedApps),
107
+ new AnalyticsService(req.org).getHourlyVolume(
108
+ null,
109
+ getToday(),
110
+ allowedApps,
111
+ ),
112
+ new AnalyticsService(req.org).getRecentErrors(10, allowedApps),
113
+ new AnalyticsService(req.org).getDailyTrend(null, 7, allowedApps),
114
+ ]);
115
+ const bp = await this._base(req);
116
+ // Load per-user dashboard order
117
+ let dashOrder = null;
118
+ try {
119
+ const path = require('path'),
120
+ fsp = require('fs').promises;
121
+ const orderFile = path.join(
122
+ require('../config').DATA_DIR,
123
+ `dashorder-${ req.user.username }.json`,
124
+ );
125
+ const raw = await fsp.readFile(orderFile, 'utf8');
126
+ dashOrder = JSON.parse(raw);
127
+ } catch {}
128
+ res.render('dashboard', {
129
+ ...bp,
130
+ title: 'Dashboard',
131
+ overview,
132
+ hourly,
133
+ errors,
134
+ trend,
135
+ today: getToday(),
136
+ dashOrder,
137
+ });
138
+ } catch (err) {
139
+ const bp = await this._base(req);
140
+ res.render('dashboard', {
141
+ ...bp,
142
+ title: 'Dashboard',
143
+ overview: null,
144
+ hourly: null,
145
+ errors: [],
146
+ trend: [],
147
+ today: getToday(),
148
+ dashOrder: null,
149
+ renderError: err.message,
150
+ });
151
+ }
152
+ }
153
+
154
+ async logsPage (req, res) {
155
+ try {
156
+ const allServices = await this._logSvc(req).getServices();
157
+ // Per-service access control: filter by role's allowedApps
158
+ const { RoleConfigService } = require('../services/RoleConfigService');
159
+ const role = req.user?.role || 'viewer';
160
+ const allowedApps = await new RoleConfigService(req.org).getAllowedApps(
161
+ role,
162
+ );
163
+ const services = allowedApps.length
164
+ ? allServices.filter((s) => allowedApps.includes(s.appName))
165
+ : allServices; // empty = all allowed
166
+ const { service, date, level, q, fromTime, toTime, fromDate, toDate }
167
+ = req.query;
168
+ let results = null;
169
+ // Enforce service access
170
+ if (service && allowedApps.length && !allowedApps.includes(service)) {
171
+ const bp2 = await this._base(req);
172
+ return res.render('logs', {
173
+ ...bp2,
174
+ title: 'Logs',
175
+ services,
176
+ selected: {
177
+ service: '',
178
+ date: date || getToday(),
179
+ level: level || '',
180
+ q: q || '',
181
+ fromTime: '',
182
+ toTime: '',
183
+ fromDate: '',
184
+ toDate: '',
185
+ },
186
+ results: null,
187
+ today: getToday(),
188
+ accessError: `You do not have access to service: ${ service}`,
189
+ });
190
+ }
191
+ const effectiveDate
192
+ = date || (service ? getToday() : !fromDate ? getToday() : '');
193
+ // Search across all services when none selected but query/level provided
194
+ if (!service && (q || level) && effectiveDate) {
195
+ // Search all accessible services for the query
196
+ const allLines = [];
197
+ for (const { appName: svcName } of services.slice(0, 20)) {
198
+ try {
199
+ const r = await this._logSvc(req).search({
200
+ service: svcName,
201
+ date: effectiveDate,
202
+ level,
203
+ q,
204
+ limit: 50,
205
+ });
206
+ if (r && r.lines) {
207
+ // Tag each line with service name if not already tagged
208
+ r.lines.forEach((line) => {
209
+ try {
210
+ const p = JSON.parse(line);
211
+ if (!p.appName) { p.appName = svcName; }
212
+ allLines.push(JSON.stringify(p));
213
+ } catch {
214
+ allLines.push(line);
215
+ }
216
+ });
217
+ }
218
+ } catch {}
219
+ }
220
+ results = { lines: allLines.slice(0, 500), total: allLines.length };
221
+ } else if (service && effectiveDate) {
222
+ if (fromTime || toTime) {
223
+ results = await this._logSvc(req).searchTimeRange(service, effectiveDate, {
224
+ fromTime,
225
+ toTime,
226
+ level,
227
+ q,
228
+ limit: 500,
229
+ });
230
+ } else if (level || q) {
231
+ results = await this._logSvc(req).search({
232
+ service,
233
+ date: effectiveDate,
234
+ level,
235
+ q,
236
+ limit: 500,
237
+ });
238
+ } else {
239
+ results = await this._logSvc(req).tail(service, effectiveDate, 200);
240
+ }
241
+ } else if (service && fromDate && toDate) {
242
+ results = await this._logSvc(req).searchMultiDate(service, fromDate, toDate, {
243
+ level,
244
+ q,
245
+ limit: 500,
246
+ });
247
+ }
248
+ const bp = await this._base(req);
249
+ res.render('logs', {
250
+ ...bp,
251
+ title: 'Logs',
252
+ services,
253
+ selected: {
254
+ service: service || '',
255
+ date: effectiveDate || getToday(),
256
+ level: level || '',
257
+ q: q || '',
258
+ fromTime: fromTime || '',
259
+ toTime: toTime || '',
260
+ fromDate: fromDate || '',
261
+ toDate: toDate || '',
262
+ },
263
+ results,
264
+ today: getToday(),
265
+ });
266
+ } catch (err) {
267
+ const bp = await this._base(req);
268
+ res.render('logs', {
269
+ ...bp,
270
+ title: 'Logs',
271
+ services: [],
272
+ selected: {
273
+ service: '',
274
+ date: getToday(),
275
+ level: '',
276
+ q: '',
277
+ fromTime: '',
278
+ toTime: '',
279
+ fromDate: '',
280
+ toDate: '',
281
+ },
282
+ results: null,
283
+ today: getToday(),
284
+ renderError: err.message,
285
+ });
286
+ }
287
+ }
288
+
289
+ async livePage (req, res) {
290
+ try {
291
+ const services = await this._logSvc(req).getServices();
292
+ const bp = await this._base(req);
293
+ res.render('live', { ...bp, title: 'Live Stream', services });
294
+ } catch {
295
+ const bp = await this._base(req);
296
+ res.render('live', { ...bp, title: 'Live Stream', services: [] });
297
+ }
298
+ }
299
+
300
+ async insightsPage (req, res) {
301
+ try {
302
+ const { service, date } = req.query;
303
+ const today = getToday();
304
+ const selDate = date || today;
305
+ const ApiAnalyticsService = require('../services/ApiAnalyticsService');
306
+ const AnalyticsService = require('../services/AnalyticsService');
307
+ const _allowedInsights = await (async () => {
308
+ try {
309
+ const {
310
+ RoleConfigService,
311
+ } = require('../services/RoleConfigService');
312
+ return await new RoleConfigService(req.org).getAllowedApps(
313
+ req.user?.role || 'viewer',
314
+ );
315
+ } catch {
316
+ return [];
317
+ }
318
+ })();
319
+ const [logServices, apiServicesList, hourly, breakdown, topSvcs, trend]
320
+ = await Promise.all([
321
+ this._logSvc(req).getServices(),
322
+ new ApiAnalyticsService().getApiServices().catch(() => []),
323
+ new AnalyticsService(req.org).getHourlyVolume(
324
+ service || null,
325
+ selDate,
326
+ _allowedInsights,
327
+ ),
328
+ new AnalyticsService(req.org).getLevelBreakdown(
329
+ service || null,
330
+ selDate,
331
+ ),
332
+ new AnalyticsService(req.org).getTopServices(
333
+ selDate,
334
+ 10,
335
+ _allowedInsights,
336
+ ),
337
+ new AnalyticsService(req.org).getDailyTrend(
338
+ service || null,
339
+ 7,
340
+ _allowedInsights,
341
+ ),
342
+ ]);
343
+ const bp = await this._base(req);
344
+ res.render('insights', {
345
+ ...bp,
346
+ title: 'Insights',
347
+ logServices,
348
+ apiServices: apiServicesList,
349
+ selected: { service: service || '', date: selDate },
350
+ hourly,
351
+ breakdown,
352
+ topSvcs,
353
+ trend,
354
+ today,
355
+ });
356
+ } catch (err) {
357
+ const bp = await this._base(req);
358
+ res.render('insights', {
359
+ ...bp,
360
+ title: 'Insights',
361
+ logServices: [],
362
+ apiServices: [],
363
+ selected: {},
364
+ hourly: null,
365
+ breakdown: null,
366
+ topSvcs: [],
367
+ trend: [],
368
+ today: getToday(),
369
+ renderError: err.message,
370
+ });
371
+ }
372
+ }
373
+
374
+ async healthPage (req, res) {
375
+ try {
376
+ const AnalyticsService = require('../services/AnalyticsService');
377
+ const { RoleConfigService } = require('../services/RoleConfigService');
378
+ let serviceHealth = [];
379
+ try {
380
+ const rawHealth = await new AnalyticsService(req.org).getServiceHealth();
381
+ const allowedApps2 = await new RoleConfigService(req.org).getAllowedApps(req.user?.role || 'viewer');
382
+ serviceHealth = allowedApps2.length ? rawHealth.filter(s => allowedApps2.includes(s.appName)) : rawHealth;
383
+ } catch {}
384
+ const bp = await this._base(req);
385
+ res.render('health', {
386
+ ...bp,
387
+ title: 'Health',
388
+ serviceHealth,
389
+ });
390
+ } catch (err) {
391
+ const bp = await this._base(req);
392
+ res.render('health', { ...bp, title: 'Health', serviceHealth: [], renderError: err.message });
393
+ }
394
+ }
395
+
396
+ async settingsPage (req, res) {
397
+ try {
398
+ const SettingsService = require('../services/SettingsService');
399
+ const AuthService = require('../services/AuthService');
400
+ const { PRESETS } = require('../lib/theme');
401
+ const [settings, totpStatus] = await Promise.all([
402
+ new SettingsService(req.org).get(),
403
+ new AuthService().getTotpStatus(req.user.username),
404
+ ]);
405
+ const bp = await this._base(req);
406
+ res.render('settings', {
407
+ ...bp,
408
+ title: 'Settings',
409
+ isAdmin: req.user.role === 'admin' || req.user.role === 'super-admin',
410
+ settings,
411
+ totpEnabled: totpStatus.enabled,
412
+ PRESETS,
413
+ });
414
+ } catch (err) {
415
+ res
416
+ .status(500)
417
+ .render('404', {
418
+ title: 'Error',
419
+ user: req.user || null,
420
+ settings: {},
421
+ appName: 'LogBoard',
422
+ appLogoUrl: '/public/logo.png',
423
+ });
424
+ }
425
+ }
426
+
427
+ async usersPage (req, res) {
428
+ try {
429
+ const UserService = require('../services/UserService');
430
+ const { RoleConfigService } = require('../services/RoleConfigService');
431
+ const [users, rolesData] = await Promise.all([
432
+ new UserService().getAll(),
433
+ new RoleConfigService(req.org).getRoles(),
434
+ ]);
435
+ const bp = await this._base(req);
436
+ res.render('users', {
437
+ ...bp,
438
+ title: 'Users',
439
+ users,
440
+ roles: Object.keys(rolesData),
441
+ rolesData,
442
+ });
443
+ } catch (err) {
444
+ res
445
+ .status(500)
446
+ .render('404', {
447
+ title: 'Error',
448
+ user: req.user || null,
449
+ settings: {},
450
+ appName: 'LogBoard',
451
+ appLogoUrl: '/public/logo.png',
452
+ });
453
+ }
454
+ }
455
+
456
+ async apiKeysPage (req, res) {
457
+ try {
458
+ const ApiKeyService = require('../services/ApiKeyService');
459
+ const keys = await new ApiKeyService().list();
460
+ const bp = await this._base(req);
461
+ const SCOPES = [
462
+ 'logs:write',
463
+ 'logs:read',
464
+ 'analytics:read',
465
+ 'health:read',
466
+ 'stream:read',
467
+ ];
468
+ res.render('api-keys', { ...bp, title: 'API Keys', keys, SCOPES });
469
+ } catch (err) {
470
+ res
471
+ .status(500)
472
+ .render('404', {
473
+ title: 'Error',
474
+ user: req.user || null,
475
+ settings: {},
476
+ appName: 'LogBoard',
477
+ appLogoUrl: '/public/logo.png',
478
+ });
479
+ }
480
+ }
481
+
482
+ async rolesPage (req, res) {
483
+ try {
484
+ const {
485
+ RoleConfigService,
486
+ ALL_PAGES,
487
+ ALL_CARDS,
488
+ } = require('../services/RoleConfigService');
489
+ const rolesData = await new RoleConfigService(req.org).getRoles();
490
+ const bp = await this._base(req);
491
+ res.render('roles', {
492
+ ...bp,
493
+ title: 'Role Config',
494
+ rolesData,
495
+ allPages: ALL_PAGES,
496
+ allCards: ALL_CARDS,
497
+ });
498
+ } catch (err) {
499
+ res
500
+ .status(500)
501
+ .render('404', {
502
+ title: 'Error',
503
+ user: req.user || null,
504
+ settings: {},
505
+ appName: 'LogBoard',
506
+ appLogoUrl: '/public/logo.png',
507
+ });
508
+ }
509
+ }
510
+
511
+ async auditPage (req, res) {
512
+ try {
513
+ const AuditService = require('../services/AuditService'); const audit = new AuditService(req.org); const { q = '', limit = 200, action = '' } = req.query;
514
+ const { entries, total } = await audit.list({
515
+ limit: Number(limit),
516
+ q,
517
+ action,
518
+ });
519
+ const bp = await this._base(req);
520
+ res.render('audit', {
521
+ ...bp,
522
+ title: 'Audit Log',
523
+ entries,
524
+ total,
525
+ q,
526
+ action,
527
+ });
528
+ } catch (err) {
529
+ res
530
+ .status(500)
531
+ .render('404', {
532
+ title: 'Error',
533
+ user: req.user || null,
534
+ settings: {},
535
+ appName: 'LogBoard',
536
+ appLogoUrl: '/public/logo.png',
537
+ });
538
+ }
539
+ }
540
+
541
+ async alertsPage (req, res) {
542
+ try {
543
+ const svc = require('../services/AlertRulesService');
544
+ const logSvcs = await this._logSvc(req).getServices();
545
+ const [rules, history] = await Promise.all([
546
+ svc.listRules(),
547
+ svc.getHistory(50),
548
+ ]);
549
+ const bp = await this._base(req);
550
+ res.render('alerts', {
551
+ ...bp,
552
+ title: 'Alert Rules',
553
+ rules,
554
+ history,
555
+ logServices: logSvcs,
556
+ });
557
+ } catch (err) {
558
+ console.log(err);
559
+ res
560
+ .status(500)
561
+ .render('404', {
562
+ title: 'Error',
563
+ user: req.user || null,
564
+ settings: {},
565
+ appName: 'LogBoard',
566
+ appLogoUrl: '/public/logo.png',
567
+ });
568
+ }
569
+ }
570
+
571
+ async archivePage (req, res) {
572
+ try {
573
+ const path = require('path'),
574
+ fs = require('fs');
575
+ const dir = config.ARCHIVE_DIR;
576
+ const files = fs.existsSync(dir)
577
+ ? fs
578
+ .readdirSync(dir)
579
+ .filter((f) => f.endsWith('.tar.gz') || f.endsWith('.zip'))
580
+ .map((f) => {
581
+ const fp = path.join(dir, f),
582
+ st = fs.statSync(fp);
583
+ return { name: f, size: st.size, created: st.birthtime };
584
+ })
585
+ .sort((a, b) => new Date(b.created) - new Date(a.created))
586
+ : [];
587
+ const logSvcs = await this._logSvc(req).getServices();
588
+ const bp = await this._base(req);
589
+ res.render('archive', {
590
+ ...bp,
591
+ title: 'Log Archive',
592
+ archives: files,
593
+ logServices: logSvcs,
594
+ });
595
+ } catch (err) {
596
+ res
597
+ .status(500)
598
+ .render('404', {
599
+ title: 'Error',
600
+ user: req.user || null,
601
+ settings: {},
602
+ appName: 'LogBoard',
603
+ appLogoUrl: '/public/logo.png',
604
+ });
605
+ }
606
+ }
607
+
608
+ // ── NEW PAGES ─────────────────────────────────────────────────────────────
609
+
610
+ async heatmapPage (req, res) {
611
+ try {
612
+ const { service } = req.query;
613
+ const services = await this._logSvc(req).getServices();
614
+ const AnalyticsService = require('../services/AnalyticsService');
615
+ const heatmapData = service
616
+ ? await new AnalyticsService(req.org).getHeatmap(service)
617
+ : null;
618
+ const bp = await this._base(req);
619
+ res.render('heatmap', {
620
+ ...bp,
621
+ title: 'Error Heatmap',
622
+ services,
623
+ service: service || '',
624
+ heatmapData,
625
+ });
626
+ } catch (err) {
627
+ const bp = await this._base(req);
628
+ res.render('heatmap', {
629
+ ...bp,
630
+ title: 'Error Heatmap',
631
+ services: [],
632
+ service: '',
633
+ heatmapData: null,
634
+ renderError: err.message,
635
+ });
636
+ }
637
+ }
638
+
639
+ async diffPage (req, res) {
640
+ try {
641
+ const { service, dateA, dateB } = req.query;
642
+ const services = await this._logSvc(req).getServices();
643
+ let diffData = null;
644
+ if (service && dateA && dateB) { diffData = await this._logSvc(req).getDiff(service, dateA, dateB); }
645
+ const bp = await this._base(req);
646
+ res.render('diff', {
647
+ ...bp,
648
+ title: 'Log Diff',
649
+ services,
650
+ service: service || '',
651
+ dateA: dateA || '',
652
+ dateB: dateB || '',
653
+ diffData,
654
+ today: getToday(),
655
+ });
656
+ } catch (err) {
657
+ const bp = await this._base(req);
658
+ res.render('diff', {
659
+ ...bp,
660
+ title: 'Log Diff',
661
+ services: [],
662
+ service: '',
663
+ dateA: '',
664
+ dateB: '',
665
+ diffData: null,
666
+ today: getToday(),
667
+ renderError: err.message,
668
+ });
669
+ }
670
+ }
671
+
672
+ async serviceMapPage (req, res) {
673
+ try {
674
+ const { date } = req.query;
675
+ const today = getToday();
676
+ const mapData = await this._logSvc(req).getServiceMap(date || today);
677
+ const bp = await this._base(req);
678
+ res.render('service-map', {
679
+ ...bp,
680
+ title: 'Service Map',
681
+ mapData,
682
+ date: date || today,
683
+ today,
684
+ });
685
+ } catch (err) {
686
+ const bp = await this._base(req);
687
+ res.render('service-map', {
688
+ ...bp,
689
+ title: 'Service Map',
690
+ mapData: null,
691
+ date: getToday(),
692
+ today: getToday(),
693
+ renderError: err.message,
694
+ });
695
+ }
696
+ }
697
+
698
+ async customDashboardPage (req, res) {
699
+ try {
700
+ const DashboardService = require('../services/DashboardService');
701
+ const logSvcs = await this._logSvc(req).getServices();
702
+ const layout = await new DashboardService().getLayout(req.user.username);
703
+ const bp = await this._base(req);
704
+ res.render('custom-dashboard', {
705
+ ...bp,
706
+ title: 'My Dashboard',
707
+ layout,
708
+ logServices: logSvcs,
709
+ today: getToday(),
710
+ });
711
+ } catch (err) {
712
+ const bp = await this._base(req);
713
+ res.render('custom-dashboard', {
714
+ ...bp,
715
+ title: 'My Dashboard',
716
+ layout: [],
717
+ logServices: [],
718
+ today: getToday(),
719
+ renderError: err.message,
720
+ });
721
+ }
722
+ }
723
+
724
+ async bookmarksPage (req, res) {
725
+ try {
726
+ const BookmarkService = require('../services/BookmarkService');
727
+ const bookmarks = await BookmarkService.getAll(req.user.username);
728
+ const bp = await this._base(req);
729
+ res.render('bookmarks', { ...bp, title: 'Bookmarks', bookmarks });
730
+ } catch (err) {
731
+ const bp = await this._base(req);
732
+ res.render('bookmarks', {
733
+ ...bp,
734
+ title: 'Bookmarks',
735
+ bookmarks: [],
736
+ renderError: err.message,
737
+ });
738
+ }
739
+ }
740
+
741
+ async savedSearchesPage (req, res) {
742
+ try {
743
+ const SavedSearchService = require('../services/SavedSearchService');
744
+ const services = await this._logSvc(req).getServices();
745
+ const searches = await SavedSearchService.getAll(req.user.username);
746
+ const bp = await this._base(req);
747
+ res.render('saved-searches', {
748
+ ...bp,
749
+ title: 'Saved Searches',
750
+ searches,
751
+ logServices: services,
752
+ today: getToday(),
753
+ });
754
+ } catch (err) {
755
+ const bp = await this._base(req);
756
+ res.render('saved-searches', {
757
+ ...bp,
758
+ title: 'Saved Searches',
759
+ searches: [],
760
+ logServices: [],
761
+ today: getToday(),
762
+ renderError: err.message,
763
+ });
764
+ }
765
+ }
766
+
767
+ async notificationsPage (req, res) {
768
+ try {
769
+ const NotifService = require('../services/NotificationService');
770
+ const notifs = await NotifService.getAll(100);
771
+ const bp = await this._base(req);
772
+ res.render('notifications', {
773
+ ...bp,
774
+ title: 'Notifications',
775
+ notifications: notifs,
776
+ });
777
+ } catch (err) {
778
+ const bp = await this._base(req);
779
+ res.render('notifications', {
780
+ ...bp,
781
+ title: 'Notifications',
782
+ notifications: [],
783
+ renderError: err.message,
784
+ });
785
+ }
786
+ }
787
+ }
788
+
789
+ module.exports = UiController;