lobsterboard 0.1.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/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/lobsterboard.css +347 -0
- package/dist/lobsterboard.esm.js +1195 -0
- package/dist/lobsterboard.esm.js.map +1 -0
- package/dist/lobsterboard.esm.min.js +8 -0
- package/dist/lobsterboard.esm.min.js.map +1 -0
- package/dist/lobsterboard.umd.js +1219 -0
- package/dist/lobsterboard.umd.js.map +1 -0
- package/dist/lobsterboard.umd.min.js +8 -0
- package/dist/lobsterboard.umd.min.js.map +1 -0
- package/package.json +67 -0
- package/src/builder.js +723 -0
- package/src/index.js +53 -0
- package/src/widgets.js +435 -0
package/src/builder.js
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LobsterBoard - Dashboard Builder Core
|
|
3
|
+
* Provides utilities for generating dashboard HTML, CSS, and JS
|
|
4
|
+
*
|
|
5
|
+
* @module lobsterboard/builder
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WIDGETS } from './widgets.js';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────
|
|
11
|
+
// SECURITY HELPERS
|
|
12
|
+
// ─────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escape HTML to prevent XSS attacks
|
|
16
|
+
* @param {string} str - String to escape
|
|
17
|
+
* @returns {string} Escaped string
|
|
18
|
+
*/
|
|
19
|
+
export function escapeHtml(str) {
|
|
20
|
+
if (!str) return '';
|
|
21
|
+
if (typeof document !== 'undefined') {
|
|
22
|
+
const div = document.createElement('div');
|
|
23
|
+
div.textContent = str;
|
|
24
|
+
return div.innerHTML;
|
|
25
|
+
}
|
|
26
|
+
// Fallback for Node.js
|
|
27
|
+
return str
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, ''');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────
|
|
36
|
+
// HTML PROCESSING
|
|
37
|
+
// ─────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Process widget HTML to conditionally remove header
|
|
41
|
+
* @param {string} html - Widget HTML
|
|
42
|
+
* @param {boolean} showHeader - Whether to show the header
|
|
43
|
+
* @returns {string} Processed HTML
|
|
44
|
+
*/
|
|
45
|
+
export function processWidgetHtml(html, showHeader) {
|
|
46
|
+
if (showHeader !== false) return html;
|
|
47
|
+
const headerRegex = /<div\s+class="dash-card-head"[^>]*>[\s\S]*?<\/div>/i;
|
|
48
|
+
return html.replace(headerRegex, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────
|
|
52
|
+
// CSS GENERATION
|
|
53
|
+
// ─────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate the base dashboard CSS
|
|
57
|
+
* @returns {string} CSS styles
|
|
58
|
+
*/
|
|
59
|
+
export function generateDashboardCss() {
|
|
60
|
+
return `/* LobsterBoard Dashboard - Generated Styles */
|
|
61
|
+
|
|
62
|
+
:root {
|
|
63
|
+
--bg-primary: #0d1117;
|
|
64
|
+
--bg-secondary: #161b22;
|
|
65
|
+
--bg-tertiary: #21262d;
|
|
66
|
+
--bg-hover: #30363d;
|
|
67
|
+
--border: #30363d;
|
|
68
|
+
--text-primary: #e6edf3;
|
|
69
|
+
--text-secondary: #8b949e;
|
|
70
|
+
--text-muted: #6e7681;
|
|
71
|
+
--accent-blue: #58a6ff;
|
|
72
|
+
--accent-green: #3fb950;
|
|
73
|
+
--accent-orange: #d29922;
|
|
74
|
+
--accent-red: #f85149;
|
|
75
|
+
--accent-purple: #a371f7;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
* {
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
body {
|
|
85
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
86
|
+
background: var(--bg-primary);
|
|
87
|
+
color: var(--text-primary);
|
|
88
|
+
min-height: 100vh;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dashboard {
|
|
92
|
+
margin: 0 auto;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.widget-container {
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* KPI Cards */
|
|
101
|
+
.kpi-card {
|
|
102
|
+
background: var(--bg-secondary);
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
padding: 16px;
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 12px;
|
|
109
|
+
height: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.kpi-sm {
|
|
113
|
+
padding: 12px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.kpi-icon {
|
|
117
|
+
font-size: 24px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.kpi-data {
|
|
121
|
+
flex: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.kpi-value {
|
|
125
|
+
font-size: 20px;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.kpi-value.blue { color: var(--accent-blue); }
|
|
130
|
+
.kpi-value.green { color: var(--accent-green); }
|
|
131
|
+
.kpi-value.orange { color: var(--accent-orange); }
|
|
132
|
+
.kpi-value.red { color: var(--accent-red); }
|
|
133
|
+
|
|
134
|
+
.kpi-label {
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
color: var(--text-secondary);
|
|
137
|
+
margin-top: 2px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.kpi-indicator {
|
|
141
|
+
width: 10px;
|
|
142
|
+
height: 10px;
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
background: var(--text-muted);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.kpi-indicator.green { background: var(--accent-green); }
|
|
148
|
+
.kpi-indicator.yellow { background: var(--accent-orange); }
|
|
149
|
+
.kpi-indicator.red { background: var(--accent-red); }
|
|
150
|
+
|
|
151
|
+
/* Dash Cards */
|
|
152
|
+
.dash-card {
|
|
153
|
+
background: var(--bg-secondary);
|
|
154
|
+
border: 1px solid var(--border);
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
height: 100%;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.dash-card-head {
|
|
163
|
+
display: flex;
|
|
164
|
+
justify-content: space-between;
|
|
165
|
+
align-items: center;
|
|
166
|
+
padding: 12px 16px;
|
|
167
|
+
border-bottom: 1px solid var(--border);
|
|
168
|
+
background: var(--bg-tertiary);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.dash-card-title {
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.dash-card-badge {
|
|
177
|
+
font-size: 11px;
|
|
178
|
+
color: var(--text-secondary);
|
|
179
|
+
background: var(--bg-primary);
|
|
180
|
+
padding: 2px 8px;
|
|
181
|
+
border-radius: 10px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.dash-card-body {
|
|
185
|
+
flex: 1;
|
|
186
|
+
padding: 12px 16px;
|
|
187
|
+
overflow-y: auto;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.compact-list {
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.syslog-scroll {
|
|
195
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
196
|
+
font-size: 11px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Top Bar */
|
|
200
|
+
.topbar {
|
|
201
|
+
display: flex;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
align-items: center;
|
|
204
|
+
padding: 8px 20px;
|
|
205
|
+
background: var(--bg-secondary);
|
|
206
|
+
border-bottom: 1px solid var(--border);
|
|
207
|
+
height: 100%;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.topbar-left {
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 20px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.topbar-brand {
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
font-size: 14px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.topbar-link {
|
|
222
|
+
color: var(--text-secondary);
|
|
223
|
+
text-decoration: none;
|
|
224
|
+
font-size: 13px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.topbar-link:hover,
|
|
228
|
+
.topbar-link.active {
|
|
229
|
+
color: var(--accent-blue);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.topbar-right {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 12px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.topbar-meta {
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
color: var(--text-muted);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.topbar-refresh {
|
|
244
|
+
background: var(--bg-tertiary);
|
|
245
|
+
border: 1px solid var(--border);
|
|
246
|
+
color: var(--text-secondary);
|
|
247
|
+
padding: 4px 8px;
|
|
248
|
+
border-radius: 4px;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* List Items */
|
|
253
|
+
.list-item {
|
|
254
|
+
padding: 6px 0;
|
|
255
|
+
border-bottom: 1px solid var(--border);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.list-item:last-child {
|
|
259
|
+
border-bottom: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.cron-item {
|
|
263
|
+
display: flex;
|
|
264
|
+
justify-content: space-between;
|
|
265
|
+
padding: 6px 0;
|
|
266
|
+
border-bottom: 1px solid var(--border);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.cron-name {
|
|
270
|
+
color: var(--text-primary);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.cron-next {
|
|
274
|
+
color: var(--text-muted);
|
|
275
|
+
font-size: 11px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.log-line {
|
|
279
|
+
padding: 2px 0;
|
|
280
|
+
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Weather */
|
|
284
|
+
.weather-row {
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 10px;
|
|
288
|
+
padding: 8px 0;
|
|
289
|
+
border-bottom: 1px solid var(--border);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.weather-row:last-child {
|
|
293
|
+
border-bottom: none;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.weather-icon {
|
|
297
|
+
font-size: 18px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.weather-loc {
|
|
301
|
+
flex: 1;
|
|
302
|
+
color: var(--text-primary);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.weather-temp {
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
color: var(--accent-blue);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Utilities */
|
|
311
|
+
.loading-sm {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
justify-content: center;
|
|
315
|
+
padding: 20px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.spinner-sm {
|
|
319
|
+
width: 20px;
|
|
320
|
+
height: 20px;
|
|
321
|
+
border: 2px solid var(--bg-tertiary);
|
|
322
|
+
border-top-color: var(--accent-blue);
|
|
323
|
+
border-radius: 50%;
|
|
324
|
+
animation: spin 1s linear infinite;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@keyframes spin {
|
|
328
|
+
to { transform: rotate(360deg); }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.error {
|
|
332
|
+
color: var(--accent-red);
|
|
333
|
+
padding: 10px;
|
|
334
|
+
text-align: center;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
::-webkit-scrollbar {
|
|
338
|
+
width: 6px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
::-webkit-scrollbar-track {
|
|
342
|
+
background: var(--bg-primary);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
::-webkit-scrollbar-thumb {
|
|
346
|
+
background: var(--bg-tertiary);
|
|
347
|
+
border-radius: 3px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Post-Export Edit Mode */
|
|
351
|
+
.edit-mode .widget-container {
|
|
352
|
+
cursor: move;
|
|
353
|
+
outline: 2px dashed #3b82f6;
|
|
354
|
+
outline-offset: -2px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.edit-mode .widget-container:hover {
|
|
358
|
+
outline-color: #60a5fa;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.edit-mode .widget-container.dragging {
|
|
362
|
+
opacity: 0.8;
|
|
363
|
+
z-index: 1000;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.resize-handle-edit {
|
|
367
|
+
display: none;
|
|
368
|
+
position: absolute;
|
|
369
|
+
bottom: 0;
|
|
370
|
+
right: 0;
|
|
371
|
+
width: 16px;
|
|
372
|
+
height: 16px;
|
|
373
|
+
cursor: se-resize;
|
|
374
|
+
background: #3b82f6;
|
|
375
|
+
border-radius: 2px 0 0 0;
|
|
376
|
+
z-index: 10;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.edit-mode .resize-handle-edit {
|
|
380
|
+
display: block;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#edit-toggle {
|
|
384
|
+
position: fixed;
|
|
385
|
+
bottom: 20px;
|
|
386
|
+
right: 20px;
|
|
387
|
+
z-index: 9999;
|
|
388
|
+
padding: 8px 16px;
|
|
389
|
+
background: #1e293b;
|
|
390
|
+
color: white;
|
|
391
|
+
border: none;
|
|
392
|
+
border-radius: 6px;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
font-size: 13px;
|
|
395
|
+
font-weight: 500;
|
|
396
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#edit-toggle:hover {
|
|
400
|
+
background: #334155;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#edit-toggle.active {
|
|
404
|
+
background: #3b82f6;
|
|
405
|
+
}
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─────────────────────────────────────────────
|
|
410
|
+
// JS GENERATION
|
|
411
|
+
// ─────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Generate the post-export edit mode JS
|
|
415
|
+
* @returns {string} JavaScript code
|
|
416
|
+
*/
|
|
417
|
+
export function generateEditJs() {
|
|
418
|
+
return `
|
|
419
|
+
// ─────────────────────────────────────────────
|
|
420
|
+
// POST-EXPORT LAYOUT EDITING
|
|
421
|
+
// ─────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
(function() {
|
|
424
|
+
const STORAGE_KEY = 'lobsterboard-layout';
|
|
425
|
+
const GRID_SIZE = 20;
|
|
426
|
+
const MIN_WIDTH = 100;
|
|
427
|
+
const MIN_HEIGHT = 60;
|
|
428
|
+
|
|
429
|
+
let editMode = false;
|
|
430
|
+
let activeWidget = null;
|
|
431
|
+
let startX, startY, origLeft, origTop, origWidth, origHeight;
|
|
432
|
+
let isResizing = false;
|
|
433
|
+
|
|
434
|
+
document.addEventListener('DOMContentLoaded', initEditMode);
|
|
435
|
+
|
|
436
|
+
function initEditMode() {
|
|
437
|
+
const btn = document.createElement('button');
|
|
438
|
+
btn.id = 'edit-toggle';
|
|
439
|
+
btn.textContent = '✏️ Edit Layout';
|
|
440
|
+
btn.onclick = toggleEditMode;
|
|
441
|
+
document.body.appendChild(btn);
|
|
442
|
+
document.querySelectorAll('.widget-container').forEach(initWidget);
|
|
443
|
+
loadPositions();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function initWidget(widget) {
|
|
447
|
+
const handle = document.createElement('div');
|
|
448
|
+
handle.className = 'resize-handle-edit';
|
|
449
|
+
widget.appendChild(handle);
|
|
450
|
+
widget.addEventListener('mousedown', onWidgetMouseDown);
|
|
451
|
+
handle.addEventListener('mousedown', onResizeMouseDown);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function toggleEditMode() {
|
|
455
|
+
editMode = !editMode;
|
|
456
|
+
document.body.classList.toggle('edit-mode', editMode);
|
|
457
|
+
document.getElementById('edit-toggle').classList.toggle('active', editMode);
|
|
458
|
+
document.getElementById('edit-toggle').textContent = editMode ? '💾 Save Layout' : '✏️ Edit Layout';
|
|
459
|
+
if (!editMode) savePositions();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function onWidgetMouseDown(e) {
|
|
463
|
+
if (!editMode) return;
|
|
464
|
+
if (e.target.classList.contains('resize-handle-edit')) return;
|
|
465
|
+
if (e.button !== 0) return;
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
activeWidget = e.currentTarget;
|
|
468
|
+
isResizing = false;
|
|
469
|
+
startX = e.clientX;
|
|
470
|
+
startY = e.clientY;
|
|
471
|
+
origLeft = activeWidget.offsetLeft;
|
|
472
|
+
origTop = activeWidget.offsetTop;
|
|
473
|
+
activeWidget.classList.add('dragging');
|
|
474
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
475
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function onResizeMouseDown(e) {
|
|
479
|
+
if (!editMode) return;
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
e.stopPropagation();
|
|
482
|
+
activeWidget = e.target.parentElement;
|
|
483
|
+
isResizing = true;
|
|
484
|
+
startX = e.clientX;
|
|
485
|
+
startY = e.clientY;
|
|
486
|
+
origWidth = activeWidget.offsetWidth;
|
|
487
|
+
origHeight = activeWidget.offsetHeight;
|
|
488
|
+
activeWidget.classList.add('dragging');
|
|
489
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
490
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function onMouseMove(e) {
|
|
494
|
+
if (!activeWidget) return;
|
|
495
|
+
const dx = e.clientX - startX;
|
|
496
|
+
const dy = e.clientY - startY;
|
|
497
|
+
if (isResizing) {
|
|
498
|
+
activeWidget.style.width = Math.max(MIN_WIDTH, origWidth + dx) + 'px';
|
|
499
|
+
activeWidget.style.height = Math.max(MIN_HEIGHT, origHeight + dy) + 'px';
|
|
500
|
+
} else {
|
|
501
|
+
activeWidget.style.left = Math.max(0, origLeft + dx) + 'px';
|
|
502
|
+
activeWidget.style.top = Math.max(0, origTop + dy) + 'px';
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function onMouseUp() {
|
|
507
|
+
if (!activeWidget) return;
|
|
508
|
+
if (isResizing) {
|
|
509
|
+
activeWidget.style.width = snapToGrid(activeWidget.offsetWidth) + 'px';
|
|
510
|
+
activeWidget.style.height = snapToGrid(activeWidget.offsetHeight) + 'px';
|
|
511
|
+
} else {
|
|
512
|
+
activeWidget.style.left = snapToGrid(activeWidget.offsetLeft) + 'px';
|
|
513
|
+
activeWidget.style.top = snapToGrid(activeWidget.offsetTop) + 'px';
|
|
514
|
+
}
|
|
515
|
+
activeWidget.classList.remove('dragging');
|
|
516
|
+
activeWidget = null;
|
|
517
|
+
isResizing = false;
|
|
518
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
519
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function snapToGrid(value) {
|
|
523
|
+
return Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function savePositions() {
|
|
527
|
+
const positions = {};
|
|
528
|
+
document.querySelectorAll('.widget-container').forEach(widget => {
|
|
529
|
+
const id = widget.dataset.widgetId;
|
|
530
|
+
if (id) {
|
|
531
|
+
positions[id] = {
|
|
532
|
+
left: widget.offsetLeft,
|
|
533
|
+
top: widget.offsetTop,
|
|
534
|
+
width: widget.offsetWidth,
|
|
535
|
+
height: widget.offsetHeight
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
try {
|
|
540
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
541
|
+
} catch (e) {}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function loadPositions() {
|
|
545
|
+
try {
|
|
546
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
547
|
+
if (!saved) return;
|
|
548
|
+
const positions = JSON.parse(saved);
|
|
549
|
+
document.querySelectorAll('.widget-container').forEach(widget => {
|
|
550
|
+
const id = widget.dataset.widgetId;
|
|
551
|
+
const pos = positions[id];
|
|
552
|
+
if (pos) {
|
|
553
|
+
widget.style.left = pos.left + 'px';
|
|
554
|
+
widget.style.top = pos.top + 'px';
|
|
555
|
+
widget.style.width = pos.width + 'px';
|
|
556
|
+
widget.style.height = pos.height + 'px';
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
} catch (e) {}
|
|
560
|
+
}
|
|
561
|
+
})();
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ─────────────────────────────────────────────
|
|
566
|
+
// DASHBOARD GENERATION
|
|
567
|
+
// ─────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Generate widget HTML for a widget configuration
|
|
571
|
+
* @param {Object} widget - Widget configuration
|
|
572
|
+
* @returns {string} Widget HTML
|
|
573
|
+
*/
|
|
574
|
+
export function generateWidgetHtml(widget) {
|
|
575
|
+
const template = WIDGETS[widget.type];
|
|
576
|
+
if (!template) return '';
|
|
577
|
+
|
|
578
|
+
const props = { ...widget.properties, id: widget.id };
|
|
579
|
+
let html = processWidgetHtml(template.generateHtml(props), widget.properties.showHeader);
|
|
580
|
+
|
|
581
|
+
return `
|
|
582
|
+
<div class="widget-container" data-widget-id="${widget.id}" style="position:absolute;left:${widget.x}px;top:${widget.y}px;width:${widget.width}px;height:${widget.height}px;">
|
|
583
|
+
${html}
|
|
584
|
+
</div>`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate widget JavaScript for a widget configuration
|
|
589
|
+
* @param {Object} widget - Widget configuration
|
|
590
|
+
* @returns {string} Widget JavaScript
|
|
591
|
+
*/
|
|
592
|
+
export function generateWidgetJs(widget) {
|
|
593
|
+
const template = WIDGETS[widget.type];
|
|
594
|
+
if (!template || !template.generateJs) return '';
|
|
595
|
+
|
|
596
|
+
const props = { ...widget.properties, id: widget.id };
|
|
597
|
+
return template.generateJs(props);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Generate complete dashboard HTML
|
|
602
|
+
* @param {Object} config - Dashboard configuration
|
|
603
|
+
* @param {Object} config.canvas - Canvas dimensions { width, height }
|
|
604
|
+
* @param {Array} config.widgets - Array of widget configurations
|
|
605
|
+
* @returns {string} Complete HTML document
|
|
606
|
+
*/
|
|
607
|
+
export function generateDashboardHtml(config) {
|
|
608
|
+
const { canvas, widgets } = config;
|
|
609
|
+
const widgetHtml = widgets.map(generateWidgetHtml).join('\n');
|
|
610
|
+
|
|
611
|
+
return `<!DOCTYPE html>
|
|
612
|
+
<html lang="en">
|
|
613
|
+
<head>
|
|
614
|
+
<meta charset="UTF-8">
|
|
615
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
616
|
+
<title>My LobsterBoard Dashboard</title>
|
|
617
|
+
<link rel="stylesheet" href="css/style.css">
|
|
618
|
+
</head>
|
|
619
|
+
<body>
|
|
620
|
+
<main class="dashboard" style="width:${canvas.width}px;height:${canvas.height}px;position:relative;">
|
|
621
|
+
${widgetHtml}
|
|
622
|
+
</main>
|
|
623
|
+
<script src="js/dashboard.js"></script>
|
|
624
|
+
</body>
|
|
625
|
+
</html>`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Generate complete dashboard JavaScript
|
|
630
|
+
* @param {Array} widgets - Array of widget configurations
|
|
631
|
+
* @returns {string} Complete JavaScript
|
|
632
|
+
*/
|
|
633
|
+
export function generateDashboardJs(widgets) {
|
|
634
|
+
const widgetJs = widgets.map(generateWidgetJs).filter(Boolean).join('\n\n');
|
|
635
|
+
const editJs = generateEditJs();
|
|
636
|
+
|
|
637
|
+
return `/**
|
|
638
|
+
* LobsterBoard Dashboard - Generated JavaScript
|
|
639
|
+
* Replace YOUR_*_API_KEY placeholders with your actual API keys
|
|
640
|
+
*/
|
|
641
|
+
|
|
642
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
643
|
+
console.log('Dashboard loaded');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
${widgetJs}
|
|
647
|
+
|
|
648
|
+
${editJs}
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Generate README for exported dashboard
|
|
654
|
+
* @param {Array} widgets - Array of widget configurations
|
|
655
|
+
* @returns {string} README markdown
|
|
656
|
+
*/
|
|
657
|
+
export function generateReadme(widgets) {
|
|
658
|
+
const apiKeys = [];
|
|
659
|
+
const needsOpenClaw = widgets.some(w =>
|
|
660
|
+
['openclaw-release', 'auth-status', 'activity-list', 'cron-jobs', 'system-log', 'session-count', 'token-gauge'].includes(w.type)
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
widgets.forEach(widget => {
|
|
664
|
+
const template = WIDGETS[widget.type];
|
|
665
|
+
if (template?.hasApiKey && template.apiKeyName) {
|
|
666
|
+
if (!apiKeys.includes(template.apiKeyName)) {
|
|
667
|
+
apiKeys.push(template.apiKeyName);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return `# LobsterBoard Dashboard
|
|
673
|
+
|
|
674
|
+
This dashboard was generated with LobsterBoard Dashboard Builder.
|
|
675
|
+
|
|
676
|
+
## Quick Start
|
|
677
|
+
|
|
678
|
+
${needsOpenClaw ? `### Running with OpenClaw widgets
|
|
679
|
+
|
|
680
|
+
Your dashboard includes widgets that connect to OpenClaw. Run the included server:
|
|
681
|
+
|
|
682
|
+
\`\`\`bash
|
|
683
|
+
node server.js
|
|
684
|
+
\`\`\`
|
|
685
|
+
|
|
686
|
+
Open http://localhost:8080 in your browser.
|
|
687
|
+
` : ''}
|
|
688
|
+
### Static mode
|
|
689
|
+
|
|
690
|
+
Open \`index.html\` directly in a browser.
|
|
691
|
+
|
|
692
|
+
## Files
|
|
693
|
+
|
|
694
|
+
| File | Description |
|
|
695
|
+
|------|-------------|
|
|
696
|
+
| \`index.html\` | Dashboard page |
|
|
697
|
+
| \`css/style.css\` | Styles |
|
|
698
|
+
| \`js/dashboard.js\` | Widget logic |
|
|
699
|
+
| \`server.js\` | Server with OpenClaw API proxy |
|
|
700
|
+
|
|
701
|
+
${apiKeys.length > 0 ? `## API Keys
|
|
702
|
+
|
|
703
|
+
Edit \`js/dashboard.js\` and replace these placeholders:
|
|
704
|
+
${apiKeys.map(key => `- \`YOUR_${key}\``).join('\n')}
|
|
705
|
+
` : ''}
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
Generated with LobsterBoard - https://github.com/curbob/LobsterBoard
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export default {
|
|
714
|
+
escapeHtml,
|
|
715
|
+
processWidgetHtml,
|
|
716
|
+
generateDashboardCss,
|
|
717
|
+
generateEditJs,
|
|
718
|
+
generateWidgetHtml,
|
|
719
|
+
generateWidgetJs,
|
|
720
|
+
generateDashboardHtml,
|
|
721
|
+
generateDashboardJs,
|
|
722
|
+
generateReadme
|
|
723
|
+
};
|