@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,58 @@
|
|
|
1
|
+
<%- include('partials/head', { title: 'Notifications' }) %>
|
|
2
|
+
<div class="app-shell">
|
|
3
|
+
<%- include('partials/sidebar') %>
|
|
4
|
+
<div class="main-area">
|
|
5
|
+
<header class="top-header">
|
|
6
|
+
<div class="page-title">Notifications</div>
|
|
7
|
+
<div class="header-actions">
|
|
8
|
+
<button class="btn btn-secondary btn-sm" onclick="markAllRead()">Mark all read</button>
|
|
9
|
+
</div>
|
|
10
|
+
</header>
|
|
11
|
+
<div class="page-content">
|
|
12
|
+
<div class="card">
|
|
13
|
+
<% if (!notifications || !notifications.length) { %>
|
|
14
|
+
<div class="empty-state" style="padding:50px 0;">
|
|
15
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>
|
|
16
|
+
<p>No notifications yet</p>
|
|
17
|
+
<p style="font-size:11px;margin-top:4px;color:var(--text3)">Alert rule triggers and report completions will appear here</p>
|
|
18
|
+
</div>
|
|
19
|
+
<% } else { %>
|
|
20
|
+
<% notifications.forEach(function(n){ %>
|
|
21
|
+
<div class="log-line" id="notif-<%=n.id%>" style="padding:10px 14px;opacity:<%=n.read?'.55':'1'%>;align-items:flex-start;">
|
|
22
|
+
<div style="flex:1;min-width:0;">
|
|
23
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:3px;">
|
|
24
|
+
<span style="font-size:12px;font-weight:600;color:var(--text);"><%=n.title%></span>
|
|
25
|
+
<% if(!n.read){%><span style="width:7px;height:7px;background:var(--accent);border-radius:50%;flex-shrink:0;"></span><%}%>
|
|
26
|
+
<span class="badge <%=n.type==='alert'?'badge-error':n.type==='report'?'badge-info':'badge-debug'%>" style="font-size:9px;"><%=n.type%></span>
|
|
27
|
+
</div>
|
|
28
|
+
<div style="font-size:12px;color:var(--text2);margin-bottom:4px;"><%=n.body%></div>
|
|
29
|
+
<div style="font-size:10px;color:var(--text3);"><%=new Date(n.ts).toLocaleString()%><%if(n.service&&n.service!=='all'){%> · <%=n.service%><%}%></div>
|
|
30
|
+
</div>
|
|
31
|
+
<div style="display:flex;gap:6px;flex-shrink:0;margin-top:2px;">
|
|
32
|
+
<%if(!n.read){%><button class="btn btn-secondary btn-xs" onclick="markOne('<%=n.id%>')">Read</button><%}%>
|
|
33
|
+
<button class="btn btn-danger btn-xs" onclick="deleteNotif('<%=n.id%>')">✕</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<% }) %>
|
|
37
|
+
<% } %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<script>
|
|
43
|
+
async function markAllRead() {
|
|
44
|
+
await fetch('/api/notifications/read',{method:'PUT'});
|
|
45
|
+
document.querySelectorAll('[id^="notif-"]').forEach(el=>{el.style.opacity='.55';el.querySelector('.btn-secondary')?.remove();el.querySelector('span[style*="background:var(--accent)"]')?.remove();});
|
|
46
|
+
toast('All marked as read','success');
|
|
47
|
+
}
|
|
48
|
+
async function markOne(id) {
|
|
49
|
+
await fetch('/api/notifications/read/'+id,{method:'PUT'});
|
|
50
|
+
const el=document.getElementById('notif-'+id);
|
|
51
|
+
if(el){el.style.opacity='.55';el.querySelector('.btn-secondary')?.remove();el.querySelector('span[style*="background:var(--accent)"]')?.remove();}
|
|
52
|
+
}
|
|
53
|
+
async function deleteNotif(id) {
|
|
54
|
+
const r=await fetch('/api/notifications/'+id,{method:'DELETE'});
|
|
55
|
+
if(r.ok){const el=document.getElementById('notif-'+id);if(el)el.remove();toast('Deleted','success');}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
</body></html>
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<!-- Favicon — uses custom URL from branding settings, falls back to default SVG -->
|
|
6
|
+
<% if (typeof settings !== 'undefined' && settings && settings.faviconUrl) { %>
|
|
7
|
+
<link rel="icon" href="<%= settings.faviconUrl %>"/>
|
|
8
|
+
<% } else { %>
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><polyline points='6,20 12,12 18,17 26,8' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/></svg>"/>
|
|
10
|
+
<% } %>
|
|
11
|
+
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
13
|
+
<title><%= title %> — <%= typeof appName !== "undefined" ? appName : "LogBoard" %></title>
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
|
16
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
|
17
|
+
<style>
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #0d0d14;
|
|
20
|
+
--surface: #13131e;
|
|
21
|
+
--surface2: #1a1a2e;
|
|
22
|
+
--surface3: #1f2035;
|
|
23
|
+
--border: #2a2a45;
|
|
24
|
+
--border2: #383860;
|
|
25
|
+
--text: #e2e2f2;
|
|
26
|
+
--text2: #9494b8;
|
|
27
|
+
--text3: #5a5a78;
|
|
28
|
+
--green: #22c55e;
|
|
29
|
+
--yellow: #f59e0b;
|
|
30
|
+
--red: #ef4444;
|
|
31
|
+
--blue: #3b82f6;
|
|
32
|
+
--gray: #6b7280;
|
|
33
|
+
--radius: 8px;
|
|
34
|
+
--radius-lg: 12px;
|
|
35
|
+
--sidebar-w: 230px;
|
|
36
|
+
--header-h: 56px;
|
|
37
|
+
--shadow: 0 4px 24px rgba(0,0,0,.4);
|
|
38
|
+
}
|
|
39
|
+
[data-theme="light"] {
|
|
40
|
+
--bg: #f4f4f8;
|
|
41
|
+
--surface: #ffffff;
|
|
42
|
+
--surface2: #f0f0f6;
|
|
43
|
+
--surface3: #e8e8f0;
|
|
44
|
+
--border: #dde0ef;
|
|
45
|
+
--border2: #c8cce0;
|
|
46
|
+
--text: #1a1a2e;
|
|
47
|
+
--text2: #4a4a6a;
|
|
48
|
+
--text3: #8a8aaa;
|
|
49
|
+
--shadow: 0 4px 16px rgba(0,0,0,.08);
|
|
50
|
+
}
|
|
51
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|
52
|
+
html,body{height:100%;}
|
|
53
|
+
body{font-family:'Inter',system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);line-height:1.6;}
|
|
54
|
+
a{color:var(--accent-l);text-decoration:none;}
|
|
55
|
+
a:hover{text-decoration:underline;}
|
|
56
|
+
button{cursor:pointer;font-family:inherit;font-size:13px;}
|
|
57
|
+
input,select,textarea{font-family:inherit;font-size:13px;}
|
|
58
|
+
svg{display:block;flex-shrink:0;}
|
|
59
|
+
.app-shell{display:flex;height:100vh;overflow:hidden;}
|
|
60
|
+
/* Sidebar */
|
|
61
|
+
.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;}
|
|
62
|
+
.sidebar-brand{display:flex;align-items:center;gap:10px;padding:0 16px;height:var(--header-h);border-bottom:1px solid var(--border);}
|
|
63
|
+
.brand-logo{height:28px;width:auto;object-fit:contain;}
|
|
64
|
+
.brand-name{font-weight:700;font-size:16px;color:var(--text);letter-spacing:-.4px;}
|
|
65
|
+
.sidebar-nav{flex:1;padding:12px 8px;overflow-y:auto;}
|
|
66
|
+
.nav-section{margin-bottom:20px;}
|
|
67
|
+
.nav-label{font-size:10px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;color:var(--text3);padding:0 10px 6px;}
|
|
68
|
+
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--radius);color:var(--text2);font-weight:500;transition:all .15s;text-decoration:none;user-select:none;}
|
|
69
|
+
.nav-item:hover{background:var(--surface2);color:var(--text);text-decoration:none;}
|
|
70
|
+
.nav-item.active{background:var(--accent-dim);color:var(--accent-l);}
|
|
71
|
+
.nav-item svg{opacity:.8;}
|
|
72
|
+
.nav-item.active svg{opacity:1;}
|
|
73
|
+
.sidebar-footer{padding:12px 8px;border-top:1px solid var(--border);display:flex;align-items:center;gap:8px;}
|
|
74
|
+
.user-avatar{width:28px;height:28px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff;flex-shrink:0;}
|
|
75
|
+
.user-info{flex:1;min-width:0;}
|
|
76
|
+
.user-name{font-weight:600;font-size:12px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
77
|
+
.user-role{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;}
|
|
78
|
+
.icon-btn{background:none;border:none;padding:5px;border-radius:6px;color:var(--text2);transition:all .15s;display:flex;align-items:center;justify-content:center;}
|
|
79
|
+
.icon-btn:hover{background:var(--surface2);color:var(--text);}
|
|
80
|
+
.main-area{flex:1;display:flex;flex-direction:column;overflow:hidden;}
|
|
81
|
+
.top-header{height:var(--header-h);padding:0 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--surface);flex-shrink:0;}
|
|
82
|
+
.page-title{font-weight:600;font-size:15px;flex:1;}
|
|
83
|
+
.header-actions{display:flex;align-items:center;gap:8px;}
|
|
84
|
+
.page-content{flex:1;overflow-y:auto;padding:20px;}
|
|
85
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;}
|
|
86
|
+
.card-title{font-weight:600;font-size:13px;color:var(--text2);margin-bottom:12px;display:flex;align-items:center;gap:8px;}
|
|
87
|
+
.card-title svg{color:var(--accent);}
|
|
88
|
+
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:16px;}
|
|
89
|
+
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px 18px;display:flex;align-items:flex-start;gap:14px;}
|
|
90
|
+
.stat-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
|
|
91
|
+
.stat-icon.green{background:rgba(34,197,94,.15);color:var(--green);}
|
|
92
|
+
.stat-icon.red{background:rgba(239,68,68,.15);color:var(--red);}
|
|
93
|
+
.stat-icon.blue{background:rgba(59,130,246,.15);color:var(--blue);}
|
|
94
|
+
.stat-icon.purple{background:var(--accent-dim);color:var(--accent-l);}
|
|
95
|
+
.stat-icon.yellow{background:rgba(245,158,11,.15);color:var(--yellow);}
|
|
96
|
+
.stat-value{font-size:24px;font-weight:700;color:var(--text);line-height:1.1;}
|
|
97
|
+
.stat-label{font-size:12px;color:var(--text2);margin-top:2px;}
|
|
98
|
+
.stat-sub{font-size:11px;color:var(--text3);margin-top:4px;}
|
|
99
|
+
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
|
|
100
|
+
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;}
|
|
101
|
+
.span-2{grid-column:span 2;}
|
|
102
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--radius);font-weight:500;border:none;transition:all .15s;line-height:1;white-space:nowrap;}
|
|
103
|
+
.btn-primary{background:var(--accent);color:#fff;}
|
|
104
|
+
.btn-primary:hover{background:#4f52d9;}
|
|
105
|
+
.btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border);}
|
|
106
|
+
.btn-secondary:hover{background:var(--surface3);}
|
|
107
|
+
.btn-danger{background:rgba(239,68,68,.15);color:var(--red);border:1px solid rgba(239,68,68,.3);}
|
|
108
|
+
.btn-danger:hover{background:rgba(239,68,68,.25);}
|
|
109
|
+
.btn-sm{padding:5px 10px;font-size:12px;}
|
|
110
|
+
.btn-xs{padding:3px 8px;font-size:11px;}
|
|
111
|
+
.btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
112
|
+
.form-group{margin-bottom:14px;}
|
|
113
|
+
.form-label{display:block;font-size:12px;font-weight:500;color:var(--text2);margin-bottom:5px;}
|
|
114
|
+
.form-input,.form-select{width:100%;padding:8px 12px;border-radius:var(--radius);background:var(--surface2);border:1px solid var(--border);color:var(--text);outline:none;transition:border-color .15s;}
|
|
115
|
+
.form-input:focus,.form-select:focus{border-color:var(--accent);}
|
|
116
|
+
.form-row{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;}
|
|
117
|
+
.form-row .form-group{flex:1;min-width:120px;margin-bottom:0;}
|
|
118
|
+
.log-list{font-family:'JetBrains Mono',monospace;font-size:12px;}
|
|
119
|
+
.log-line{display:flex;align-items:flex-start;gap:10px;padding:5px 10px;border-radius:5px;cursor:pointer;border-bottom:1px solid transparent;transition:background .1s;}
|
|
120
|
+
.log-line:hover{background:var(--surface2);}
|
|
121
|
+
.log-line.level-error{border-left:3px solid var(--red);padding-left:8px;}
|
|
122
|
+
.log-line.level-warn{border-left:3px solid var(--yellow);padding-left:8px;}
|
|
123
|
+
.log-line.level-info{border-left:3px solid var(--blue);padding-left:8px;}
|
|
124
|
+
.log-line.level-debug{border-left:3px solid var(--gray);padding-left:8px;}
|
|
125
|
+
.log-ts{color:var(--text3);white-space:nowrap;font-size:11px;padding-top:1px;min-width:170px;}
|
|
126
|
+
.log-body{flex:1;word-break:break-all;}
|
|
127
|
+
.log-expanded{padding:8px 10px 10px;font-family:'JetBrains Mono',monospace;font-size:11.5px;}
|
|
128
|
+
.log-json{background:var(--surface3);border-radius:6px;padding:10px 12px;overflow-x:auto;color:var(--text);white-space:pre;}
|
|
129
|
+
.badge{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;white-space:nowrap;}
|
|
130
|
+
.badge-error{background:rgba(239,68,68,.2);color:#ff6b6b;}
|
|
131
|
+
.badge-warn{background:rgba(245,158,11,.2);color:#fbbf24;}
|
|
132
|
+
.badge-info{background:rgba(59,130,246,.2);color:#60a5fa;}
|
|
133
|
+
.badge-debug{background:rgba(107,114,128,.2);color:#9ca3af;}
|
|
134
|
+
.badge-green{background:rgba(34,197,94,.15);color:var(--green);}
|
|
135
|
+
.badge-purple{background:var(--accent-dim);color:var(--accent-l);}
|
|
136
|
+
.table-wrap{overflow-x:auto;}
|
|
137
|
+
table{width:100%;border-collapse:collapse;}
|
|
138
|
+
th{padding:8px 12px;text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);border-bottom:1px solid var(--border);}
|
|
139
|
+
td{padding:9px 12px;border-bottom:1px solid var(--border);font-size:12px;vertical-align:top;}
|
|
140
|
+
tr:last-child td{border-bottom:none;}
|
|
141
|
+
tr:hover td{background:var(--surface2);}
|
|
142
|
+
.empty-state{text-align:center;padding:48px 20px;color:var(--text3);}
|
|
143
|
+
.empty-state svg{margin:0 auto 12px;opacity:.4;}
|
|
144
|
+
.empty-state p{font-size:13px;}
|
|
145
|
+
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;}
|
|
146
|
+
.dot-green{background:var(--green);box-shadow:0 0 6px var(--green);}
|
|
147
|
+
.dot-red{background:var(--red);box-shadow:0 0 6px var(--red);}
|
|
148
|
+
.dot-yellow{background:var(--yellow);}
|
|
149
|
+
.dot-gray{background:var(--gray);}
|
|
150
|
+
.dot-pulse{animation:pulse 1.5s infinite;}
|
|
151
|
+
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
|
|
152
|
+
#toast-container{position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
|
153
|
+
.toast{padding:10px 16px;border-radius:var(--radius);font-size:13px;background:var(--surface2);border:1px solid var(--border);color:var(--text);box-shadow:var(--shadow);min-width:260px;max-width:360px;display:flex;align-items:center;gap:8px;animation:slideIn .2s ease;pointer-events:none;}
|
|
154
|
+
.toast.success{border-color:var(--green);}
|
|
155
|
+
.toast.error{border-color:var(--red);}
|
|
156
|
+
@keyframes slideIn{from{transform:translateX(30px);opacity:0;}to{transform:none;opacity:1;}}
|
|
157
|
+
::-webkit-scrollbar{width:6px;height:6px;}
|
|
158
|
+
::-webkit-scrollbar-track{background:transparent;}
|
|
159
|
+
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px;}
|
|
160
|
+
.search-wrap{position:relative;}
|
|
161
|
+
.search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text3);pointer-events:none;}
|
|
162
|
+
.search-input{padding-left:34px!important;}
|
|
163
|
+
.progress-bar{height:4px;background:var(--surface3);border-radius:2px;overflow:hidden;}
|
|
164
|
+
.progress-fill{height:100%;border-radius:2px;background:var(--accent);transition:width .3s;}
|
|
165
|
+
.progress-fill.red{background:var(--red);}
|
|
166
|
+
.progress-fill.yellow{background:var(--yellow);}
|
|
167
|
+
.divider{border:none;border-top:1px solid var(--border);margin:14px 0;}
|
|
168
|
+
.chart-wrap{position:relative;height:200px;}
|
|
169
|
+
.chart-wrap-sm{position:relative;height:140px;}
|
|
170
|
+
.chart-wrap-lg{position:relative;height:260px;}
|
|
171
|
+
.login-bg{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,rgba(99,102,241,.08) 0,transparent 60%),radial-gradient(ellipse at 80% 20%,rgba(99,102,241,.06) 0,transparent 50%);}
|
|
172
|
+
.login-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:36px 32px;width:100%;max-width:400px;box-shadow:var(--shadow);}
|
|
173
|
+
.login-logo{display:flex;align-items:center;gap:12px;margin-bottom:28px;}
|
|
174
|
+
.login-logo img{height:36px;}
|
|
175
|
+
.login-logo span{font-size:22px;font-weight:700;letter-spacing:-.4px;}
|
|
176
|
+
.login-error{background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);border-radius:7px;padding:10px 14px;color:#ff6b6b;font-size:12.5px;margin-bottom:14px;}
|
|
177
|
+
.toggle{position:relative;width:38px;height:22px;}
|
|
178
|
+
.toggle input{opacity:0;width:0;height:0;position:absolute;}
|
|
179
|
+
.toggle-slider{position:absolute;inset:0;background:var(--surface3);border-radius:22px;cursor:pointer;transition:background .2s;border:1px solid var(--border2);}
|
|
180
|
+
.toggle-slider::before{content:'';position:absolute;width:16px;height:16px;left:2px;bottom:2px;background:var(--text3);border-radius:50%;transition:transform .2s,background .2s;}
|
|
181
|
+
.toggle input:checked + .toggle-slider{background:var(--accent);border-color:var(--accent);}
|
|
182
|
+
.toggle input:checked + .toggle-slider::before{transform:translateX(16px);background:#fff;}
|
|
183
|
+
.toggle input:disabled + .toggle-slider{opacity:.5;cursor:not-allowed;}
|
|
184
|
+
.flex{display:flex;}.items-center{align-items:center;}.justify-between{justify-content:space-between;}.gap-2{gap:8px;}.gap-3{gap:12px;}.mt-2{margin-top:8px;}.mt-3{margin-top:12px;}.mb-3{margin-bottom:12px;}.text-muted{color:var(--text3);}.text-sm{font-size:12px;}.font-mono{font-family:'JetBrains Mono',monospace;}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.w-full{width:100%;}
|
|
185
|
+
/* Modal */
|
|
186
|
+
.modal-overlay{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);align-items:center;justify-content:center;}
|
|
187
|
+
.modal-overlay.open{display:flex;}
|
|
188
|
+
.modal-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:28px;width:100%;max-width:480px;box-shadow:0 24px 60px rgba(0,0,0,.5);animation:modalIn .2s ease;}
|
|
189
|
+
@keyframes modalIn{from{transform:translateY(16px) scale(.97);opacity:0;}to{transform:none;opacity:1;}}
|
|
190
|
+
.modal-title{font-size:15px;font-weight:700;margin-bottom:4px;color:var(--text);}
|
|
191
|
+
.modal-sub{font-size:12px;color:var(--text2);margin-bottom:20px;}
|
|
192
|
+
/* Scope/checkbox pills */
|
|
193
|
+
.scope-grid{display:flex;flex-wrap:wrap;gap:6px;}
|
|
194
|
+
.scope-pill{display:flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:500;background:var(--surface2);border:1px solid var(--border);color:var(--text2);cursor:pointer;transition:all .15s;user-select:none;}
|
|
195
|
+
.scope-pill:hover{border-color:var(--border2);color:var(--text);}
|
|
196
|
+
.scope-pill input{display:none;}
|
|
197
|
+
.scope-pill.checked{border-color:var(--accent);background:var(--accent-dim);color:var(--accent-l);}
|
|
198
|
+
|
|
199
|
+
/* ── Level chips / filter pills ─────────────────────────────────────── */
|
|
200
|
+
.chip-bar { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
|
201
|
+
.chip {
|
|
202
|
+
display:inline-flex; align-items:center; gap:5px;
|
|
203
|
+
padding:4px 10px; border-radius:20px; font-size:11px; font-weight:600;
|
|
204
|
+
cursor:pointer; transition:all .15s; user-select:none; text-decoration:none;
|
|
205
|
+
background:var(--surface2); border:1px solid var(--border); color:var(--text2);
|
|
206
|
+
white-space:nowrap;
|
|
207
|
+
}
|
|
208
|
+
.chip:hover { border-color:var(--border2); color:var(--text); text-decoration:none; }
|
|
209
|
+
.chip.active { color:var(--text); background:var(--surface3); border-color:var(--border2); }
|
|
210
|
+
.chip.error { border-color:rgba(239,68,68,.35); color:#f87171; background:rgba(239,68,68,.08); }
|
|
211
|
+
.chip.error.active { background:rgba(239,68,68,.2); border-color:#ef4444; color:#ef4444; }
|
|
212
|
+
.chip.warn { border-color:rgba(245,158,11,.35); color:#fbbf24; background:rgba(245,158,11,.08); }
|
|
213
|
+
.chip.warn.active { background:rgba(245,158,11,.2); border-color:#f59e0b; color:#f59e0b; }
|
|
214
|
+
.chip.info { border-color:rgba(59,130,246,.35); color:#60a5fa; background:rgba(59,130,246,.08); }
|
|
215
|
+
.chip.info.active { background:rgba(59,130,246,.2); border-color:#3b82f6; color:#3b82f6; }
|
|
216
|
+
.chip.debug { border-color:rgba(107,114,128,.35); color:#9ca3af; background:rgba(107,114,128,.08); }
|
|
217
|
+
.chip.debug.active { background:rgba(107,114,128,.2); border-color:#6b7280; color:#9ca3af; }
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/* ── Global ⌘K search ───────────────────────────────────────────── */
|
|
221
|
+
#cmd-overlay{display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);align-items:flex-start;justify-content:center;padding-top:10vh;backdrop-filter:blur(6px);}
|
|
222
|
+
#cmd-overlay.open{display:flex;}
|
|
223
|
+
#cmd-box{background:var(--surface);border:1px solid var(--border2);border-radius:14px;width:min(640px,94vw);overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.4);}
|
|
224
|
+
#cmd-input-wrap{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border);}
|
|
225
|
+
#cmd-input{flex:1;background:none;border:none;outline:none;font-size:15px;color:var(--text);}
|
|
226
|
+
#cmd-input::placeholder{color:var(--text3);}
|
|
227
|
+
#cmd-results{max-height:380px;overflow-y:auto;}
|
|
228
|
+
.cmd-section{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text3);padding:8px 16px 4px;}
|
|
229
|
+
.cmd-row{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;transition:background .1s;}
|
|
230
|
+
.cmd-row:hover,.cmd-row.focused{background:var(--surface2);}
|
|
231
|
+
.cmd-row svg{flex-shrink:0;color:var(--text3);}
|
|
232
|
+
.cmd-row-main{flex:1;min-width:0;}
|
|
233
|
+
.cmd-row-title{font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
234
|
+
.cmd-row-sub{font-size:11px;color:var(--text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
235
|
+
.cmd-badge{font-size:9px;padding:1px 6px;border-radius:3px;background:var(--surface3);color:var(--text3);white-space:nowrap;}
|
|
236
|
+
#cmd-footer{padding:8px 16px;border-top:1px solid var(--border);display:flex;align-items:center;gap:12px;font-size:11px;color:var(--text3);}
|
|
237
|
+
.cmd-kbd{background:var(--surface2);border:1px solid var(--border2);border-radius:4px;padding:1px 6px;font-size:10px;font-family:monospace;}
|
|
238
|
+
|
|
239
|
+
@keyframes spin{to{transform:rotate(360deg);}}
|
|
240
|
+
</style>
|
|
241
|
+
<% if (typeof themeSnippet !== 'undefined' && themeSnippet) { %>
|
|
242
|
+
<%- themeSnippet %>
|
|
243
|
+
<% } %>
|
|
244
|
+
</head>
|
|
245
|
+
<body>
|
|
246
|
+
<div id="toast-container"></div>
|
|
247
|
+
<script>
|
|
248
|
+
function toast(msg,type='info'){
|
|
249
|
+
const c=document.getElementById('toast-container');
|
|
250
|
+
const el=document.createElement('div');
|
|
251
|
+
el.className='toast '+type;
|
|
252
|
+
const icon=type==='success'?'✅':type==='error'?'❌':'ℹ️';
|
|
253
|
+
el.innerHTML='<span>'+icon+'</span><span>'+msg+'</span>';
|
|
254
|
+
c.appendChild(el);
|
|
255
|
+
setTimeout(()=>{el.style.opacity='0';el.style.transition='opacity .3s';setTimeout(()=>el.remove(),300);},3000);
|
|
256
|
+
}
|
|
257
|
+
function toggleTheme(){
|
|
258
|
+
const h=document.documentElement;
|
|
259
|
+
const t=h.dataset.theme==='dark'?'light':'dark';
|
|
260
|
+
h.dataset.theme=t;localStorage.setItem('theme',t);
|
|
261
|
+
}
|
|
262
|
+
(function(){const t=localStorage.getItem('theme');if(t)document.documentElement.dataset.theme=t;})();
|
|
263
|
+
</script>
|
|
264
|
+
<!-- page-view-beacon -->
|
|
265
|
+
<script>
|
|
266
|
+
(function(){
|
|
267
|
+
var _t0 = Date.now();
|
|
268
|
+
var _page = (document.title||'').split(' — ')[0] || window.location.pathname;
|
|
269
|
+
function send(){
|
|
270
|
+
var sec = Math.round((Date.now()-_t0)/1000);
|
|
271
|
+
var body = JSON.stringify({page:_page, durationSec:sec});
|
|
272
|
+
try {
|
|
273
|
+
if(navigator.sendBeacon)
|
|
274
|
+
navigator.sendBeacon('/api/audit/page-view',new Blob([body],{type:'application/json'}));
|
|
275
|
+
else
|
|
276
|
+
fetch('/api/audit/page-view',{method:'POST',headers:{'Content-Type':'application/json'},body:body,keepalive:true}).catch(function(){});
|
|
277
|
+
} catch(e){}
|
|
278
|
+
}
|
|
279
|
+
window.addEventListener('beforeunload',send);
|
|
280
|
+
document.addEventListener('visibilitychange',function(){if(document.visibilityState==='hidden')send();});
|
|
281
|
+
})();
|
|
282
|
+
</script>
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
<span class="brand-name"><%= typeof appName!=='undefined'?appName:'LogBoard' %></span>
|
|
5
|
+
</div>
|
|
6
|
+
<nav class="sidebar-nav">
|
|
7
|
+
|
|
8
|
+
<%# ── Super-admin menu ───────────────────────────────────────────────── %>
|
|
9
|
+
<% if(typeof user!=='undefined' && user && user.role==='super-admin'){ %>
|
|
10
|
+
<div class="nav-section">
|
|
11
|
+
<div class="nav-label">Super Admin</div>
|
|
12
|
+
<a href="/super-admin/orgs" class="nav-item <%= title==='Manage Orgs'?'active':'' %>">
|
|
13
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>
|
|
14
|
+
Manage Orgs
|
|
15
|
+
</a>
|
|
16
|
+
<a href="/super-admin/analytics" class="nav-item <%= title==='Platform Analytics'?'active':'' %>">
|
|
17
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
18
|
+
Platform Analytics
|
|
19
|
+
</a>
|
|
20
|
+
<a href="/super-admin/system" class="nav-item <%= title==='System Health'?'active':'' %>">
|
|
21
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
|
22
|
+
System Health
|
|
23
|
+
</a>
|
|
24
|
+
<a href="/super-admin/settings" class="nav-item <%= title==='Global Settings'?'active':'' %>">
|
|
25
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
26
|
+
Global Settings
|
|
27
|
+
</a>
|
|
28
|
+
<a href="/super-admin/admins" class="nav-item <%= title==='Super Admins'?'active':'' %>">
|
|
29
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
30
|
+
Super Admins
|
|
31
|
+
</a>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<%# ── Regular user menu ──────────────────────────────────────────────── %>
|
|
35
|
+
<% } else { %>
|
|
36
|
+
|
|
37
|
+
<div class="nav-section">
|
|
38
|
+
<div class="nav-label">Main</div>
|
|
39
|
+
<% if(canSee('dashboard')){ %>
|
|
40
|
+
<a href="/dashboard" class="nav-item <%= title==='Dashboard'?'active':'' %>">
|
|
41
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
42
|
+
Dashboard
|
|
43
|
+
</a>
|
|
44
|
+
<% } %>
|
|
45
|
+
<% if(canSee('logs')){ %>
|
|
46
|
+
<a href="/logs" class="nav-item <%= title==='Logs'?'active':'' %>">
|
|
47
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
48
|
+
Logs
|
|
49
|
+
</a>
|
|
50
|
+
<% } %>
|
|
51
|
+
<% if(canSee('live')){ %>
|
|
52
|
+
<a href="/live" class="nav-item <%= title==='Live Stream'?'active':'' %>">
|
|
53
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
54
|
+
Live Stream
|
|
55
|
+
</a>
|
|
56
|
+
<% } %>
|
|
57
|
+
<% if(canSee('insights')){ %>
|
|
58
|
+
<a href="/insights" class="nav-item <%= title==='Insights'?'active':'' %>">
|
|
59
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
60
|
+
Insights
|
|
61
|
+
</a>
|
|
62
|
+
<% } %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="nav-section">
|
|
66
|
+
<div class="nav-label">Monitor</div>
|
|
67
|
+
<% if(canSee('health')){ %>
|
|
68
|
+
<a href="/health" class="nav-item <%= title==='Health'?'active':'' %>">
|
|
69
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
|
70
|
+
Health
|
|
71
|
+
</a>
|
|
72
|
+
<% } %>
|
|
73
|
+
<% if(canSee('alerts')){ %>
|
|
74
|
+
<a href="/alerts" class="nav-item <%= title==='Alert Rules'?'active':'' %>">
|
|
75
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
76
|
+
Alert Rules
|
|
77
|
+
</a>
|
|
78
|
+
<% } %>
|
|
79
|
+
<% if(canSee('archive')){ %>
|
|
80
|
+
<a href="/archive" class="nav-item <%= title==='Log Archive'?'active':'' %>">
|
|
81
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
82
|
+
Archive
|
|
83
|
+
</a>
|
|
84
|
+
<% } %>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="nav-section">
|
|
88
|
+
<div class="nav-label">Admin</div>
|
|
89
|
+
<% if(canSee('users')){ %>
|
|
90
|
+
<a href="/users" class="nav-item <%= title==='Users'?'active':'' %>">
|
|
91
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
92
|
+
Users
|
|
93
|
+
</a>
|
|
94
|
+
<% } %>
|
|
95
|
+
<% if(canSee('api-keys')){ %>
|
|
96
|
+
<a href="/api-keys" class="nav-item <%= title==='API Keys'?'active':'' %>">
|
|
97
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
98
|
+
API Keys
|
|
99
|
+
</a>
|
|
100
|
+
<% } %>
|
|
101
|
+
<% if(canSee('audit')){ %>
|
|
102
|
+
<a href="/audit" class="nav-item <%= title==='Audit Log'?'active':'' %>">
|
|
103
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
104
|
+
Audit Log
|
|
105
|
+
</a>
|
|
106
|
+
<% } %>
|
|
107
|
+
<a href="/bookmarks" class="nav-item <%= title==='Bookmarks'?'active':'' %>">
|
|
108
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
|
109
|
+
Bookmarks
|
|
110
|
+
</a>
|
|
111
|
+
<a href="/saved-searches" class="nav-item <%= title==='Saved Searches'?'active':'' %>">
|
|
112
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
113
|
+
Saved Searches
|
|
114
|
+
</a>
|
|
115
|
+
<% if(canSee('settings')){ %>
|
|
116
|
+
<a href="/settings" class="nav-item <%= title==='Settings'?'active':'' %>">
|
|
117
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
|
|
118
|
+
Settings
|
|
119
|
+
</a>
|
|
120
|
+
<% } %>
|
|
121
|
+
<% if(canSee('roles')){ %>
|
|
122
|
+
<a href="/roles" class="nav-item <%= title==='Role Config'?'active':'' %>">
|
|
123
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
124
|
+
Role Config
|
|
125
|
+
</a>
|
|
126
|
+
<% } %>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<% } %> <%# end role switch %>
|
|
130
|
+
|
|
131
|
+
</nav>
|
|
132
|
+
<div class="sidebar-footer">
|
|
133
|
+
<% if(typeof user!=='undefined' && user && user.role==='super-admin'){ %>
|
|
134
|
+
<a href="/super-admin/profile" style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;text-decoration:none;color:inherit;">
|
|
135
|
+
<div class="user-avatar"><%= user.username[0].toUpperCase() %></div>
|
|
136
|
+
<div class="user-info">
|
|
137
|
+
<div class="user-name"><%= user.username %></div>
|
|
138
|
+
<div class="user-role"><%= user.role %></div>
|
|
139
|
+
</div>
|
|
140
|
+
</a>
|
|
141
|
+
<% } else { %>
|
|
142
|
+
<a href="/settings#password" title="Click to change password" style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;text-decoration:none;color:inherit;cursor:pointer;" title="Change password">
|
|
143
|
+
<div class="user-avatar"><%= user&&user.username?user.username[0].toUpperCase():'U' %></div>
|
|
144
|
+
<div class="user-info">
|
|
145
|
+
<div class="user-name"><%= user?user.username:'User' %></div>
|
|
146
|
+
<div class="user-role" style="font-size:10px;color:var(--text3);">Click to change password</div>
|
|
147
|
+
</div>
|
|
148
|
+
</a>
|
|
149
|
+
<% } %>
|
|
150
|
+
<% if(typeof user!=='undefined' && user && user.role!=='super-admin'){ %>
|
|
151
|
+
<button class="icon-btn" onclick="openCmdSearch()" title="Search ⌘K"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>
|
|
152
|
+
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></button>
|
|
153
|
+
<% } %>
|
|
154
|
+
<button class="icon-btn" onclick="doLogout()" title="Logout"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></button>
|
|
155
|
+
</div>
|
|
156
|
+
</aside>
|
|
157
|
+
<script>
|
|
158
|
+
async function doLogout(){await fetch('/api/auth/logout',{method:'POST'});location.href='/login';}
|
|
159
|
+
(function pollAlerts(){
|
|
160
|
+
if(!document.getElementById('notif-badge')) return;
|
|
161
|
+
fetch('/api/alerts/history?limit=5').then(r=>r.ok?r.json():null).then(d=>{
|
|
162
|
+
if(!d) return;
|
|
163
|
+
const b=document.getElementById('notif-badge');
|
|
164
|
+
if(b&&d.length>0){b.textContent=d.length>9?'9+':d.length;b.style.display='';}
|
|
165
|
+
}).catch(()=>{});
|
|
166
|
+
setTimeout(pollAlerts,60000);
|
|
167
|
+
})();
|
|
168
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<%- include('partials/head', { title: 'Create your organisation' }) %>
|
|
2
|
+
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg);padding:20px;">
|
|
3
|
+
<div style="width:100%;max-width:460px;">
|
|
4
|
+
<div class="card" style="padding:32px;">
|
|
5
|
+
<div style="text-align:center;margin-bottom:28px;">
|
|
6
|
+
<% if(settings&&settings.appLogoUrl&&settings.appLogoUrl!=='/public/logo.png'){ %>
|
|
7
|
+
<img src="<%= settings.appLogoUrl %>" style="height:40px;margin-bottom:12px;" alt="Logo"/>
|
|
8
|
+
<% } else { %>
|
|
9
|
+
<div style="width:44px;height:44px;background:var(--accent);border-radius:10px;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;">
|
|
10
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
|
11
|
+
</div>
|
|
12
|
+
<% } %>
|
|
13
|
+
<div style="font-size:20px;font-weight:700;color:var(--text);">Create your organisation</div>
|
|
14
|
+
<div style="font-size:13px;color:var(--text2);margin-top:4px;">Set up your LogBoard workspace in seconds</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<% if(typeof error !== 'undefined' && error){ %>
|
|
18
|
+
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:var(--red);padding:10px 14px;border-radius:6px;font-size:13px;margin-bottom:16px;"><%= error %></div>
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
<!-- OAuth buttons -->
|
|
22
|
+
<% if(githubEnabled){ %>
|
|
23
|
+
<a href="/auth/github" style="display:flex;align-items:center;justify-content:center;gap:10px;padding:11px;border:1px solid var(--border2);border-radius:8px;text-decoration:none;color:var(--text);font-size:13px;font-weight:500;margin-bottom:10px;transition:background .15s;" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background=''">
|
|
24
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
25
|
+
Continue with GitHub
|
|
26
|
+
</a>
|
|
27
|
+
<% } %>
|
|
28
|
+
<% if(googleEnabled){ %>
|
|
29
|
+
<a href="/auth/google" style="display:flex;align-items:center;justify-content:center;gap:10px;padding:11px;border:1px solid var(--border2);border-radius:8px;text-decoration:none;color:var(--text);font-size:13px;font-weight:500;margin-bottom:10px;transition:background .15s;" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background=''">
|
|
30
|
+
<svg width="18" height="18" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
|
|
31
|
+
Continue with Google
|
|
32
|
+
</a>
|
|
33
|
+
<% } %>
|
|
34
|
+
|
|
35
|
+
<% if(githubEnabled || googleEnabled){ %>
|
|
36
|
+
<div style="display:flex;align-items:center;gap:10px;margin:14px 0;">
|
|
37
|
+
<div style="flex:1;height:1px;background:var(--border);"></div>
|
|
38
|
+
<span style="font-size:11px;color:var(--text3);">or create with email</span>
|
|
39
|
+
<div style="flex:1;height:1px;background:var(--border);"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<% } %>
|
|
42
|
+
|
|
43
|
+
<!-- Email registration form -->
|
|
44
|
+
<div id="reg-error" style="display:none;background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:var(--red);padding:10px 14px;border-radius:6px;font-size:13px;margin-bottom:14px;"></div>
|
|
45
|
+
<div class="form-group">
|
|
46
|
+
<label class="form-label">Organisation Name</label>
|
|
47
|
+
<input type="text" id="org-name" class="form-input" placeholder="Acme Inc" autocomplete="organization"/>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-group">
|
|
50
|
+
<label class="form-label">Your Name / Username</label>
|
|
51
|
+
<input type="text" id="reg-username" class="form-input" placeholder="johndoe" autocomplete="username"/>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="form-group">
|
|
54
|
+
<label class="form-label">Email</label>
|
|
55
|
+
<input type="email" id="reg-email" class="form-input" placeholder="you@company.com" autocomplete="email"/>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-group">
|
|
58
|
+
<label class="form-label">Password <span style="color:var(--text3);font-weight:400;">(min 8 chars)</span></label>
|
|
59
|
+
<input type="password" id="reg-password" class="form-input" placeholder="••••••••" autocomplete="new-password"/>
|
|
60
|
+
</div>
|
|
61
|
+
<button id="reg-btn" class="btn btn-primary" style="width:100%;margin-top:4px;" onclick="register()">Create Organisation</button>
|
|
62
|
+
|
|
63
|
+
<p style="text-align:center;font-size:12px;color:var(--text3);margin-top:16px;">
|
|
64
|
+
Already have an account? <a href="/login" style="color:var(--accent-l);">Sign in</a>
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<script>
|
|
70
|
+
async function register() {
|
|
71
|
+
const orgName = document.getElementById('org-name').value.trim();
|
|
72
|
+
const username = document.getElementById('reg-username').value.trim();
|
|
73
|
+
const email = document.getElementById('reg-email').value.trim();
|
|
74
|
+
const password = document.getElementById('reg-password').value;
|
|
75
|
+
const errEl = document.getElementById('reg-error');
|
|
76
|
+
const btn = document.getElementById('reg-btn');
|
|
77
|
+
errEl.style.display = 'none';
|
|
78
|
+
|
|
79
|
+
if (!orgName) { errEl.textContent='Organisation name required'; errEl.style.display=''; return; }
|
|
80
|
+
if (!username) { errEl.textContent='Username required'; errEl.style.display=''; return; }
|
|
81
|
+
if (!email) { errEl.textContent='Email required'; errEl.style.display=''; return; }
|
|
82
|
+
if (password.length<8) { errEl.textContent='Password must be at least 8 characters'; errEl.style.display=''; return; }
|
|
83
|
+
|
|
84
|
+
btn.disabled = true; btn.textContent = 'Creating…';
|
|
85
|
+
try {
|
|
86
|
+
const r = await fetch('/api/auth/register', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ orgName, username, email, password }),
|
|
90
|
+
});
|
|
91
|
+
const d = await r.json();
|
|
92
|
+
if (!r.ok) { errEl.textContent = d.error || 'Registration failed'; errEl.style.display = ''; btn.disabled=false; btn.textContent='Create Organisation'; return; }
|
|
93
|
+
// Login with returned token
|
|
94
|
+
document.cookie = '<%=sessionName%>=' + d.token + '; path=/; samesite=strict';
|
|
95
|
+
location.href = '/dashboard';
|
|
96
|
+
} catch(e) { errEl.textContent = 'Network error'; errEl.style.display = ''; btn.disabled=false; btn.textContent='Create Organisation'; }
|
|
97
|
+
}
|
|
98
|
+
document.addEventListener('keydown', e => { if(e.key==='Enter') register(); });
|
|
99
|
+
</script>
|
|
100
|
+
</body></html>
|