@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,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>