@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.
- package/.env.example +37 -0
- package/README.md +200 -0
- package/bin/logboard +536 -0
- package/client/logger.js +309 -0
- package/config/index.js +142 -0
- package/config.js +2 -0
- package/controllers/AnalyticsController.js +46 -0
- package/controllers/ApiAnalyticsController.js +129 -0
- package/controllers/ApiKeyController.js +58 -0
- package/controllers/AuthController.js +131 -0
- package/controllers/HealthController.js +56 -0
- package/controllers/LogController.js +197 -0
- package/controllers/OrgController.js +152 -0
- package/controllers/RoleConfigController.js +20 -0
- package/controllers/SettingsController.js +39 -0
- package/controllers/StreamController.js +55 -0
- package/controllers/UiController.js +789 -0
- package/controllers/UserController.js +79 -0
- package/lib/batchWriter.js +57 -0
- package/lib/cleanup.js +67 -0
- package/lib/ejs.js +103 -0
- package/lib/emitter.js +5 -0
- package/lib/healthMonitor.js +245 -0
- package/lib/logger.js +21 -0
- package/lib/streams.js +32 -0
- package/lib/theme.js +77 -0
- package/lib/userStore.js +13 -0
- package/lib/utils.js +44 -0
- package/middleware/apiKey.js +82 -0
- package/middleware/auth.js +55 -0
- package/middleware/ipWhitelist.js +59 -0
- package/middleware/org.js +85 -0
- package/middleware/pageAccess.js +20 -0
- package/middleware/rateLimit.js +29 -0
- package/middleware/roles.js +11 -0
- package/package.json +77 -0
- package/routes/alerts.js +18 -0
- package/routes/analytics.js +26 -0
- package/routes/api-analytics.js +30 -0
- package/routes/api-keys.js +12 -0
- package/routes/archive.js +91 -0
- package/routes/audit.js +50 -0
- package/routes/auth.js +22 -0
- package/routes/bookmarks.js +13 -0
- package/routes/health.js +11 -0
- package/routes/logs.js +88 -0
- package/routes/metrics.js +66 -0
- package/routes/notifications.js +14 -0
- package/routes/orgs.js +98 -0
- package/routes/registration.js +202 -0
- package/routes/role-config.js +97 -0
- package/routes/saved-searches.js +12 -0
- package/routes/server.js +151 -0
- package/routes/settings.js +28 -0
- package/routes/status.js +21 -0
- package/routes/stream.js +11 -0
- package/routes/super.js +129 -0
- package/routes/ui.js +120 -0
- package/routes/users.js +13 -0
- package/server.js +172 -0
- package/services/AlertRulesService.js +323 -0
- package/services/AnalyticsService.js +665 -0
- package/services/ApiAnalyticsService.js +471 -0
- package/services/ApiKeyService.js +166 -0
- package/services/AuditService.js +249 -0
- package/services/AuthService.js +234 -0
- package/services/BookmarkService.js +49 -0
- package/services/GlobalSettingsService.js +44 -0
- package/services/LogService.js +1066 -0
- package/services/MetricsService.js +116 -0
- package/services/NotificationService.js +70 -0
- package/services/OrgService.js +217 -0
- package/services/ReportService.js +247 -0
- package/services/RoleConfigService.js +201 -0
- package/services/SavedSearchService.js +63 -0
- package/services/SettingsService.js +220 -0
- package/services/UserService.js +121 -0
- package/setup.js +132 -0
- package/views/404.ejs +8 -0
- package/views/alerts.ejs +190 -0
- package/views/analytics.ejs +209 -0
- package/views/api-analytics.ejs +660 -0
- package/views/api-keys.ejs +150 -0
- package/views/archive.ejs +123 -0
- package/views/audit.ejs +314 -0
- package/views/bookmarks.ejs +54 -0
- package/views/custom-dashboard.ejs +162 -0
- package/views/dashboard.ejs +186 -0
- package/views/diff.ejs +98 -0
- package/views/health.ejs +269 -0
- package/views/heatmap.ejs +126 -0
- package/views/insights.ejs +334 -0
- package/views/invite.ejs +74 -0
- package/views/live.ejs +299 -0
- package/views/login.ejs +64 -0
- package/views/logo.png +0 -0
- package/views/logs.ejs +754 -0
- package/views/notifications.ejs +58 -0
- package/views/partials/head.ejs +282 -0
- package/views/partials/sidebar.ejs +168 -0
- package/views/register.ejs +100 -0
- package/views/roles.ejs +279 -0
- package/views/saved-searches.ejs +51 -0
- package/views/service-map.ejs +142 -0
- package/views/settings.ejs +1159 -0
- package/views/sidebar.ejs +129 -0
- package/views/status.ejs +100 -0
- package/views/super-admin-admins.ejs +58 -0
- package/views/super-admin-analytics.ejs +49 -0
- package/views/super-admin-orgs.ejs +310 -0
- package/views/super-admin-profile.ejs +77 -0
- package/views/super-admin-settings.ejs +108 -0
- package/views/super-admin-system.ejs +46 -0
- package/views/users.ejs +153 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<aside class="sidebar">
|
|
2
|
+
<div class="sidebar-brand">
|
|
3
|
+
<img src="<%= typeof appLogoUrl!=='undefined'?appLogoUrl:'/public/logo.png' %>" alt="Logo" class="brand-logo" onerror="this.style.display='none'"/>
|
|
4
|
+
<div style="display:flex;flex-direction:column;min-width:0;">
|
|
5
|
+
<span class="brand-name"><%= typeof appName!=='undefined'?appName:'LogBoard' %></span>
|
|
6
|
+
<% if(typeof user!=='undefined'&&user&&user.orgSlug&&user.orgSlug!=='default'){ %>
|
|
7
|
+
<span style="font-size:9px;color:var(--text3);font-weight:500;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><%= user.orgSlug %></span>
|
|
8
|
+
<% } %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<nav class="sidebar-nav">
|
|
12
|
+
<div class="nav-section">
|
|
13
|
+
<div class="nav-label">Main</div>
|
|
14
|
+
<% if(canSee('dashboard')){ %>
|
|
15
|
+
<a href="/dashboard" class="nav-item <%= title==='Dashboard'?'active':'' %>">
|
|
16
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>
|
|
17
|
+
Dashboard
|
|
18
|
+
</a>
|
|
19
|
+
<% } %>
|
|
20
|
+
<% if(canSee('logs')){ %>
|
|
21
|
+
<a href="/logs" class="nav-item <%= title==='Logs'?'active':'' %>">
|
|
22
|
+
<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="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"/></svg>
|
|
23
|
+
Logs
|
|
24
|
+
</a>
|
|
25
|
+
<% } %>
|
|
26
|
+
<% if(canSee('live')){ %>
|
|
27
|
+
<a href="/live" class="nav-item <%= title==='Live Stream'?'active':'' %>">
|
|
28
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49"/><path d="M7.76 7.76a6 6 0 0 0 0 8.49"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>
|
|
29
|
+
Live Stream
|
|
30
|
+
</a>
|
|
31
|
+
<% } %>
|
|
32
|
+
<% if(canSee('insights')){ %>
|
|
33
|
+
<a href="/insights" class="nav-item <%= title==='Insights'?'active':'' %>">
|
|
34
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
|
35
|
+
Insights
|
|
36
|
+
</a>
|
|
37
|
+
<% } %>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="nav-section">
|
|
40
|
+
<div class="nav-label">Monitor</div>
|
|
41
|
+
<% if(canSee('health')){ %>
|
|
42
|
+
<a href="/health" class="nav-item <%= title==='Health'?'active':'' %>">
|
|
43
|
+
<svg width="15" height="15" 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>
|
|
44
|
+
Health
|
|
45
|
+
</a>
|
|
46
|
+
<% } %>
|
|
47
|
+
<% if(canSee('alerts')){ %>
|
|
48
|
+
<a href="/alerts" class="nav-item <%= title==='Alert Rules'?'active':'' %>">
|
|
49
|
+
<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="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>
|
|
50
|
+
Alert Rules
|
|
51
|
+
<span id="notif-badge" style="display:none;margin-left:auto;background:var(--red);color:#fff;border-radius:10px;padding:1px 6px;font-size:9px;font-weight:700;">0</span>
|
|
52
|
+
</a>
|
|
53
|
+
<% } %>
|
|
54
|
+
<% if(canSee('archive')){ %>
|
|
55
|
+
<a href="/archive" class="nav-item <%= title==='Log Archive'?'active':'' %>">
|
|
56
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21,8 21,21 3,21 3,8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
|
|
57
|
+
Archive
|
|
58
|
+
</a>
|
|
59
|
+
<% } %>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="nav-section">
|
|
62
|
+
<div class="nav-label">Admin</div>
|
|
63
|
+
<% if(canSee('users')){ %>
|
|
64
|
+
<a href="/users" class="nav-item <%= title==='Users'?'active':'' %>">
|
|
65
|
+
<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="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
66
|
+
Users
|
|
67
|
+
</a>
|
|
68
|
+
<% } %>
|
|
69
|
+
<% if(canSee('api-keys')){ %>
|
|
70
|
+
<a href="/api-keys" class="nav-item <%= title==='API Keys'?'active':'' %>">
|
|
71
|
+
<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 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
|
72
|
+
API Keys
|
|
73
|
+
</a>
|
|
74
|
+
<% } %>
|
|
75
|
+
<% if(canSee('audit')){ %>
|
|
76
|
+
<a href="/audit" class="nav-item <%= title==='Audit Log'?'active':'' %>">
|
|
77
|
+
<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="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"/></svg>
|
|
78
|
+
Audit Log
|
|
79
|
+
</a>
|
|
80
|
+
<% } %>
|
|
81
|
+
<a href="/bookmarks" class="nav-item <%= title==='Bookmarks'?'active':'' %>">
|
|
82
|
+
<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="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
|
83
|
+
Bookmarks
|
|
84
|
+
</a>
|
|
85
|
+
<a href="/saved-searches" class="nav-item <%= title==='Saved Searches'?'active':'' %>">
|
|
86
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/></svg>
|
|
87
|
+
Saved Searches
|
|
88
|
+
</a>
|
|
89
|
+
<% if(canSee('settings')){ %>
|
|
90
|
+
<a href="/settings" class="nav-item <%= title==='Settings'?'active':'' %>">
|
|
91
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
92
|
+
Settings
|
|
93
|
+
</a>
|
|
94
|
+
<% } %>
|
|
95
|
+
<% if(canSee('roles')){ %>
|
|
96
|
+
<a href="/roles" class="nav-item <%= title==='Role Config'?'active':'' %>">
|
|
97
|
+
<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="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
98
|
+
Role Config
|
|
99
|
+
</a>
|
|
100
|
+
<% } %>
|
|
101
|
+
</div>
|
|
102
|
+
</nav>
|
|
103
|
+
<div class="sidebar-footer">
|
|
104
|
+
<div class="user-avatar"><%= user&&user.username?user.username[0].toUpperCase():'U' %></div>
|
|
105
|
+
<div class="user-info">
|
|
106
|
+
<div class="user-name"><%= user?user.username:'User' %></div>
|
|
107
|
+
<div class="user-role"><%= user?user.role:'viewer' %></div>
|
|
108
|
+
</div>
|
|
109
|
+
<button class="icon-btn" onclick="openCmdSearch()" title="Global search ⌘K"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>
|
|
110
|
+
<button class="icon-btn" onclick="toggleTheme()" title="Toggle light/dark">
|
|
111
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/></svg>
|
|
112
|
+
</button>
|
|
113
|
+
<button class="icon-btn" onclick="doLogout()" title="Logout">
|
|
114
|
+
<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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16,17 21,12 16,7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</aside>
|
|
118
|
+
<script>
|
|
119
|
+
async function doLogout(){await fetch('/api/auth/logout',{method:'POST'});location.href='/login';}
|
|
120
|
+
// Poll for unread alerts
|
|
121
|
+
(function pollAlerts(){
|
|
122
|
+
fetch('/api/alerts/history?limit=5').then(r=>r.ok?r.json():null).then(d=>{
|
|
123
|
+
if(!d) return;
|
|
124
|
+
const badge=document.getElementById('notif-badge');
|
|
125
|
+
if(badge&&d.length>0){badge.textContent=d.length>9?'9+':d.length;badge.style.display='';}
|
|
126
|
+
}).catch(()=>{});
|
|
127
|
+
setTimeout(pollAlerts, 60000);
|
|
128
|
+
})();
|
|
129
|
+
</script>
|
package/views/status.ejs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
6
|
+
<title>Service Status</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
|
8
|
+
<style>
|
|
9
|
+
:root{--bg:#0d0d14;--surface:#13131e;--border:#2a2a45;--text:#e2e2f2;--text2:#9494b8;--text3:#5a5a78;--green:#22c55e;--yellow:#f59e0b;--red:#ef4444;}
|
|
10
|
+
[data-theme="light"]{--bg:#f4f4f8;--surface:#fff;--border:#dde0ef;--text:#1a1a2e;--text2:#4a4a6a;--text3:#8a8aaa;}
|
|
11
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
12
|
+
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;}
|
|
13
|
+
.wrap{max-width:860px;margin:0 auto;padding:40px 20px;}
|
|
14
|
+
header{text-align:center;margin-bottom:40px;}
|
|
15
|
+
header h1{font-size:28px;font-weight:700;margin-bottom:8px;}
|
|
16
|
+
header p{font-size:14px;color:var(--text2);}
|
|
17
|
+
.overall{display:inline-flex;align-items:center;gap:10px;padding:12px 24px;border-radius:24px;font-size:15px;font-weight:600;margin-bottom:32px;}
|
|
18
|
+
.overall.operational{background:rgba(34,197,94,.12);color:var(--green);border:1px solid rgba(34,197,94,.3);}
|
|
19
|
+
.overall.degraded{background:rgba(239,68,68,.12);color:var(--red);border:1px solid rgba(239,68,68,.3);}
|
|
20
|
+
.overall.warning{background:rgba(245,158,11,.12);color:var(--yellow);border:1px solid rgba(245,158,11,.3);}
|
|
21
|
+
.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
|
|
22
|
+
.dot.operational{background:var(--green);}
|
|
23
|
+
.dot.degraded{background:var(--red);}
|
|
24
|
+
.dot.warning{background:var(--yellow);}
|
|
25
|
+
.dot.no-data{background:var(--text3);}
|
|
26
|
+
.service-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:12px;}
|
|
27
|
+
.svc-header{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
|
|
28
|
+
.svc-name{font-weight:600;font-size:14px;flex:1;}
|
|
29
|
+
.svc-status{font-size:12px;font-weight:500;}
|
|
30
|
+
.svc-uptime{font-size:12px;color:var(--text2);margin-left:auto;}
|
|
31
|
+
.history-dots{display:flex;gap:3px;flex-wrap:nowrap;overflow:hidden;}
|
|
32
|
+
.hd{width:10px;height:28px;border-radius:2px;flex-shrink:0;cursor:default;transition:opacity .15s;}
|
|
33
|
+
.hd:hover{opacity:.75;}
|
|
34
|
+
.hd.operational{background:var(--green);}
|
|
35
|
+
.hd.warning{background:var(--yellow);}
|
|
36
|
+
.hd.degraded{background:var(--red);}
|
|
37
|
+
.hd.no-data{background:var(--border);}
|
|
38
|
+
.history-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px;}
|
|
39
|
+
footer{text-align:center;margin-top:40px;font-size:12px;color:var(--text3);}
|
|
40
|
+
.theme-btn{position:fixed;top:16px;right:16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:6px 12px;color:var(--text2);cursor:pointer;font-size:12px;}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
|
45
|
+
<div class="wrap">
|
|
46
|
+
<header>
|
|
47
|
+
<h1>Service Status</h1>
|
|
48
|
+
<p>Real-time uptime for all monitored services</p>
|
|
49
|
+
<p style="margin-top:6px;font-size:12px;color:var(--text3);">Generated: <%= new Date(data.generatedAt).toLocaleString() %></p>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<div style="text-align:center;margin-bottom:32px;">
|
|
53
|
+
<div class="overall <%= data.overall %>">
|
|
54
|
+
<div class="dot <%= data.overall %>"></div>
|
|
55
|
+
<% if(data.overall==='operational'){%>All Systems Operational<%}else if(data.overall==='degraded'){%>Partial Outage<%}else{%>Degraded Performance<%}%>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<% if(!data.services||!data.services.length){ %>
|
|
60
|
+
<div style="text-align:center;padding:60px 20px;color:var(--text2);">
|
|
61
|
+
<div style="font-size:16px;font-weight:600;margin-bottom:8px;">No services monitored yet</div>
|
|
62
|
+
<div style="font-size:13px;">Start ingesting logs via the API to see status here.</div>
|
|
63
|
+
</div>
|
|
64
|
+
<% } else { %>
|
|
65
|
+
<% data.services.forEach(function(svc){ %>
|
|
66
|
+
<div class="service-card">
|
|
67
|
+
<div class="svc-header">
|
|
68
|
+
<div class="dot <%= svc.status %>"></div>
|
|
69
|
+
<div class="svc-name"><%= svc.appName %></div>
|
|
70
|
+
<div class="svc-status" style="color:<%= svc.status==='operational'?'var(--green)':svc.status==='warning'?'var(--yellow)':'var(--red)' %>">
|
|
71
|
+
<%= svc.status.charAt(0).toUpperCase()+svc.status.slice(1) %>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="svc-uptime"><%= svc.uptime %>% uptime</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="history-dots">
|
|
76
|
+
<% svc.history.forEach(function(h){ %>
|
|
77
|
+
<div class="hd <%= h.status %>" title="<%= h.date %>: <%= h.status %><% if(h.total){ %> (<%=h.total%> logs, <%=h.errors||0%> errors)<% } %>"></div>
|
|
78
|
+
<% }) %>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="history-labels">
|
|
81
|
+
<span>90 days ago</span>
|
|
82
|
+
<span>Today</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<% }) %>
|
|
86
|
+
<% } %>
|
|
87
|
+
|
|
88
|
+
<div style="margin-top:20px;display:flex;gap:16px;justify-content:center;flex-wrap:wrap;font-size:12px;color:var(--text2);">
|
|
89
|
+
<span style="display:flex;align-items:center;gap:5px;"><span class="dot operational"></span> Operational</span>
|
|
90
|
+
<span style="display:flex;align-items:center;gap:5px;"><span class="dot warning"></span> Warning (>3% errors)</span>
|
|
91
|
+
<span style="display:flex;align-items:center;gap:5px;"><span class="dot degraded"></span> Degraded (>10% errors)</span>
|
|
92
|
+
<span style="display:flex;align-items:center;gap:5px;"><span class="dot no-data"></span> No data</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<footer>Powered by LogBoard</footer>
|
|
96
|
+
<script>
|
|
97
|
+
function toggleTheme(){const h=document.documentElement;h.dataset.theme=h.dataset.theme==='dark'?'light':'dark';localStorage.setItem('theme',h.dataset.theme);}
|
|
98
|
+
(function(){const t=localStorage.getItem('theme');if(t)document.documentElement.dataset.theme=t;})();
|
|
99
|
+
</script>
|
|
100
|
+
</body></html>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<%- include('partials/head', { title: 'Super Admins' }) %>
|
|
2
|
+
<div class="app-shell">
|
|
3
|
+
<%- include('partials/sidebar') %>
|
|
4
|
+
<div class="main-area">
|
|
5
|
+
<header class="top-header">
|
|
6
|
+
<div class="page-title">Super Admins <span style="font-size:11px;background:rgba(239,68,68,.15);color:#f87171;padding:2px 8px;border-radius:4px;margin-left:8px;font-weight:600;">SUPER ADMIN</span></div>
|
|
7
|
+
</header>
|
|
8
|
+
<div class="page-content">
|
|
9
|
+
<div class="card" style="margin-bottom:12px;">
|
|
10
|
+
<div class="card-title" style="margin-bottom:12px;">Add Super Admin</div>
|
|
11
|
+
<div style="display:flex;gap:8px;align-items:flex-end;">
|
|
12
|
+
<div class="form-group" style="margin:0;flex:1"><label class="form-label">Username</label><input type="text" id="sa-user" class="form-input" placeholder="username"/></div>
|
|
13
|
+
<div class="form-group" style="margin:0;flex:1"><label class="form-label">Password <span style="color:var(--text3);">(min 8)</span></label><input type="password" id="sa-pass" class="form-input" placeholder="••••••••"/></div>
|
|
14
|
+
<button class="btn btn-primary btn-sm" onclick="addAdmin()">Add</button>
|
|
15
|
+
</div>
|
|
16
|
+
<div id="add-result" style="font-size:12px;margin-top:8px;"></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card">
|
|
19
|
+
<div class="card-title" style="margin-bottom:12px;">Current Super Admins</div>
|
|
20
|
+
<div id="admins-list">Loading…</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<script>
|
|
26
|
+
async function loadAdmins() {
|
|
27
|
+
const r = await fetch('/api/super/admins', {credentials:'include'});
|
|
28
|
+
const d = await r.json();
|
|
29
|
+
document.getElementById('admins-list').innerHTML = d.length
|
|
30
|
+
? d.map(a=>`<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border);">
|
|
31
|
+
<span style="font-weight:500;">${a.username}</span>
|
|
32
|
+
<span style="font-size:11px;color:var(--text3);margin-left:auto;">${a.createdAt?new Date(a.createdAt).toLocaleDateString():''}</span>
|
|
33
|
+
<button class="btn btn-danger btn-xs" onclick="removeAdmin('${a.username}')">Remove</button>
|
|
34
|
+
</div>`).join('')
|
|
35
|
+
: '<div style="color:var(--text3);font-size:12px;">No super admins found</div>';
|
|
36
|
+
}
|
|
37
|
+
async function addAdmin() {
|
|
38
|
+
const u=document.getElementById('sa-user').value.trim(), p=document.getElementById('sa-pass').value;
|
|
39
|
+
const el=document.getElementById('add-result');
|
|
40
|
+
if(!u||p.length<8){el.innerHTML='<span style="color:var(--red)">Username and password (8+) required</span>';return;}
|
|
41
|
+
const r=await fetch('/api/orgs/super-admin/create',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})});
|
|
42
|
+
const d=await r.json();
|
|
43
|
+
if(!r.ok){el.innerHTML='<span style="color:var(--red)">'+d.error+'</span>';return;}
|
|
44
|
+
el.innerHTML='<span style="color:var(--green)">✓ Added</span>';
|
|
45
|
+
document.getElementById('sa-user').value='';document.getElementById('sa-pass').value='';
|
|
46
|
+
loadAdmins();
|
|
47
|
+
}
|
|
48
|
+
async function removeAdmin(u) {
|
|
49
|
+
if(u === '<%=user.username%>') { toast('You cannot remove your own account','error'); return; }
|
|
50
|
+
if(!confirm('Remove super-admin "'+u+'"?')) return;
|
|
51
|
+
const r=await fetch('/api/super/admins/'+encodeURIComponent(u),{method:'DELETE',credentials:'include'});
|
|
52
|
+
const d=await r.json();
|
|
53
|
+
if(!r.ok){toast(d.error||'Failed','error');return;}
|
|
54
|
+
toast('Super-admin removed','success'); loadAdmins();
|
|
55
|
+
}
|
|
56
|
+
loadAdmins();
|
|
57
|
+
</script>
|
|
58
|
+
</body></html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%- include('partials/head', { title: 'Platform Analytics' }) %>
|
|
2
|
+
<div class="app-shell">
|
|
3
|
+
<%- include('partials/sidebar') %>
|
|
4
|
+
<div class="main-area">
|
|
5
|
+
<header class="top-header">
|
|
6
|
+
<div class="page-title">Platform Analytics <span style="font-size:11px;background:rgba(239,68,68,.15);color:#f87171;padding:2px 8px;border-radius:4px;margin-left:8px;font-weight:600;">SUPER ADMIN</span></div>
|
|
7
|
+
<button class="btn btn-secondary btn-sm" onclick="loadStats()">Refresh</button>
|
|
8
|
+
</header>
|
|
9
|
+
<div class="page-content">
|
|
10
|
+
<div class="stats-grid" id="platform-stats" style="margin-bottom:14px;">
|
|
11
|
+
<div class="stat-card"><div><div class="stat-value" id="s-orgs">—</div><div class="stat-label">Total Orgs</div></div></div>
|
|
12
|
+
<div class="stat-card"><div><div class="stat-value" id="s-users">—</div><div class="stat-label">Total Users</div></div></div>
|
|
13
|
+
<div class="stat-card"><div><div class="stat-value" id="s-services">—</div><div class="stat-label">Total Services</div></div></div>
|
|
14
|
+
<div class="stat-card"><div><div class="stat-value" id="s-storage">—</div><div class="stat-label">Total Storage</div></div></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card">
|
|
17
|
+
<div class="card-title" style="margin-bottom:12px;">Per-Org Breakdown</div>
|
|
18
|
+
<div id="org-table">Loading…</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<script>
|
|
24
|
+
async function loadStats() {
|
|
25
|
+
try {
|
|
26
|
+
const r = await fetch('/api/super/platform-stats', {credentials:'include'});
|
|
27
|
+
if (!r.ok) { document.getElementById('org-table').textContent='Error: '+(await r.json()).error; return; }
|
|
28
|
+
const d = await r.json();
|
|
29
|
+
document.getElementById('s-orgs').textContent = d.totalOrgs;
|
|
30
|
+
document.getElementById('s-users').textContent = d.totalUsers;
|
|
31
|
+
document.getElementById('s-services').textContent = d.totalServices;
|
|
32
|
+
document.getElementById('s-storage').textContent = d.totalStorageHuman;
|
|
33
|
+
const orgs = d.orgs || [];
|
|
34
|
+
document.getElementById('org-table').innerHTML = orgs.length ? `
|
|
35
|
+
<div class="table-wrap"><table>
|
|
36
|
+
<thead><tr><th>Org</th><th>Users</th><th>Services</th><th>Storage</th><th>Plan</th><th>Created</th></tr></thead>
|
|
37
|
+
<tbody>${orgs.map(o=>`<tr>
|
|
38
|
+
<td><code style="font-size:11px;">${o.slug}</code> <span style="color:var(--text2);">${o.name}</span></td>
|
|
39
|
+
<td>${o.userCount}</td><td>${o.serviceCount}</td>
|
|
40
|
+
<td style="font-size:11px;color:var(--text3);">${o.storageHuman}</td>
|
|
41
|
+
<td><span class="badge badge-${o.plan==='pro'?'green':'debug'}" style="font-size:10px;">${o.plan}</span></td>
|
|
42
|
+
<td style="font-size:11px;color:var(--text3);">${new Date(o.createdAt).toLocaleDateString()}</td>
|
|
43
|
+
</tr>`).join('')}</tbody>
|
|
44
|
+
</table></div>` : '<div class="empty-state" style="padding:24px">No orgs</div>';
|
|
45
|
+
} catch(e) { document.getElementById('org-table').textContent = 'Error: '+e.message; }
|
|
46
|
+
}
|
|
47
|
+
loadStats();
|
|
48
|
+
</script>
|
|
49
|
+
</body></html>
|