lobsterboard 0.8.3 → 0.8.5
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 +5 -0
- package/dist/lobsterboard.css +1 -1
- package/dist/lobsterboard.esm.js +1 -1
- package/dist/lobsterboard.esm.min.js +1 -1
- package/dist/lobsterboard.umd.js +1 -1
- package/dist/lobsterboard.umd.min.js +1 -1
- package/js/widgets/productivity.js +64 -32
- package/js/widgets/shared/stats.js +44 -8
- package/package.json +2 -1
- package/server/ai-providers.cjs +1164 -0
- package/server/auth.cjs +79 -0
- package/server/config.cjs +120 -0
- package/server/crypto.cjs +44 -0
- package/server/pages.cjs +169 -0
- package/server/response.cjs +84 -0
- package/server/routes/ai-usage.cjs +228 -0
- package/server/routes/auth.cjs +200 -0
- package/server/routes/config.cjs +69 -0
- package/server/routes/data.cjs +82 -0
- package/server/routes/files.cjs +66 -0
- package/server/routes/proxy.cjs +230 -0
- package/server/routes/servers.cjs +265 -0
- package/server/routes/system.cjs +286 -0
- package/server/routes/templates.cjs +220 -0
- package/server/secrets.cjs +66 -0
- package/server/stats.cjs +102 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.5] - 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **npm package contents** — restored missing `server/` runtime files in the published package so `npx lobsterboard` and `node server.cjs` no longer crash with `Cannot find module './server/config.cjs'`
|
|
7
|
+
|
|
3
8
|
## [0.8.3] - 2026-04-02
|
|
4
9
|
|
|
5
10
|
### Fixed
|
package/dist/lobsterboard.css
CHANGED
package/dist/lobsterboard.esm.js
CHANGED
package/dist/lobsterboard.umd.js
CHANGED
|
@@ -356,39 +356,71 @@
|
|
|
356
356
|
</div>
|
|
357
357
|
</section>`,
|
|
358
358
|
generateJs: (props) => `
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
el
|
|
365
|
-
return;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
359
|
+
(function() {
|
|
360
|
+
const widgetId = '${props.id}';
|
|
361
|
+
const tickerId = '${props.id}-ticker';
|
|
362
|
+
const inputSelector = '#widget-' + widgetId + ' input[type="search"], #widget-' + widgetId + ' input[data-search-widget]';
|
|
363
|
+
const updateFn = async function() {
|
|
364
|
+
const el = document.getElementById(tickerId);
|
|
365
|
+
if (!el) return;
|
|
366
|
+
const apiKey = '${props.apiKey || ''}';
|
|
367
|
+
if (!apiKey) {
|
|
368
|
+
el.innerHTML = 'Set API key in Edit Mode — <a href="https://finnhub.io/register" target="_blank" style="color:#58a6ff;">get free key →</a>';
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const symbols = '${props.symbol || 'AAPL'}'.split(',').map(s => s.trim()).filter(Boolean);
|
|
372
|
+
try {
|
|
373
|
+
const results = await Promise.all(symbols.map(async (sym) => {
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch('/api/finance/quote?symbol=' + encodeURIComponent(sym) + '&token=' + encodeURIComponent(apiKey));
|
|
376
|
+
const data = await res.json();
|
|
377
|
+
if (data.c === 0 && data.h === 0) return '<span class="ticker-link" style="color:#8b949e;">' + sym + ' —</span>';
|
|
378
|
+
const prevClose = Number(data.pc) || 0;
|
|
379
|
+
const currentPrice = Number(data.c) || 0;
|
|
380
|
+
const change = prevClose > 0 ? ((currentPrice - prevClose) / prevClose * 100).toFixed(2) : '0.00';
|
|
381
|
+
const color = change >= 0 ? '#3fb950' : '#f85149';
|
|
382
|
+
const arrow = change >= 0 ? '▲' : '▼';
|
|
383
|
+
return '<span class="ticker-link" style="cursor:default;">' +
|
|
384
|
+
'<strong>' + sym + '</strong> $' + currentPrice.toFixed(2) +
|
|
385
|
+
' <span style="color:' + color + ';">' + arrow + ' ' + (change >= 0 ? '+' : '') + change + '%</span></span>';
|
|
386
|
+
} catch (_) {
|
|
387
|
+
return '<span class="ticker-link" style="color:#8b949e;">' + sym + ' —</span>';
|
|
388
|
+
}
|
|
389
|
+
}));
|
|
390
|
+
el.innerHTML = results.join('<span class="ticker-sep"> \\u2022\\u2022\\u2022 </span>');
|
|
391
|
+
} catch (e) {
|
|
392
|
+
if (!el.dataset.loaded) el.textContent = 'Failed to load stocks';
|
|
393
|
+
}
|
|
394
|
+
el.dataset.loaded = '1';
|
|
395
|
+
};
|
|
396
|
+
updateFn();
|
|
397
|
+
const intervalId = setInterval(updateFn, ${(props.refreshInterval || 60) * 1000});
|
|
398
|
+
const previousCleanup = window.__lbWidgetCleanup && window.__lbWidgetCleanup[widgetId];
|
|
399
|
+
if (typeof previousCleanup === 'function') previousCleanup();
|
|
400
|
+
window.__lbWidgetCleanup = window.__lbWidgetCleanup || {};
|
|
401
|
+
window.__lbWidgetCleanup[widgetId] = function() {
|
|
402
|
+
clearInterval(intervalId);
|
|
403
|
+
const input = document.querySelector(inputSelector);
|
|
404
|
+
const handler = window.__lbSearchKeydownHandlers && window.__lbSearchKeydownHandlers[widgetId];
|
|
405
|
+
if (input && handler) input.removeEventListener('keydown', handler);
|
|
406
|
+
if (window.__lbSearchKeydownHandlers) delete window.__lbSearchKeydownHandlers[widgetId];
|
|
407
|
+
delete window.__lbWidgetCleanup[widgetId];
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const input = document.querySelector(inputSelector);
|
|
411
|
+
if (input) {
|
|
412
|
+
window.__lbSearchKeydownHandlers = window.__lbSearchKeydownHandlers || {};
|
|
413
|
+
if (!window.__lbSearchKeydownHandlers[widgetId]) {
|
|
414
|
+
const handler = function(e) {
|
|
415
|
+
if (e.key === 'Enter' && document.activeElement === input) {
|
|
416
|
+
e.stopPropagation();
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
window.__lbSearchKeydownHandlers[widgetId] = handler;
|
|
420
|
+
input.addEventListener('keydown', handler);
|
|
421
|
+
}
|
|
387
422
|
}
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
update_${props.id.replace(/-/g, '_')}();
|
|
391
|
-
setInterval(update_${props.id.replace(/-/g, '_')}, ${(props.refreshInterval || 60) * 1000});
|
|
423
|
+
})();
|
|
392
424
|
`
|
|
393
425
|
};
|
|
394
426
|
|
|
@@ -32,6 +32,14 @@ function onSystemStats(callback) {
|
|
|
32
32
|
// ─────────────────────────────────────────────
|
|
33
33
|
const _remotePollers = {}; // serverId -> { interval, callbacks, lastData, errors, lastSuccess }
|
|
34
34
|
|
|
35
|
+
function _teardownRemotePoller(serverId) {
|
|
36
|
+
const poller = _remotePollers[serverId];
|
|
37
|
+
if (!poller) return;
|
|
38
|
+
if (poller.interval) clearInterval(poller.interval);
|
|
39
|
+
if (poller.abortController) poller.abortController.abort();
|
|
40
|
+
delete _remotePollers[serverId];
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
function onRemoteStats(serverId, callback, refreshMs = 10000) {
|
|
36
44
|
if (!_remotePollers[serverId]) {
|
|
37
45
|
_remotePollers[serverId] = {
|
|
@@ -40,15 +48,22 @@ function onRemoteStats(serverId, callback, refreshMs = 10000) {
|
|
|
40
48
|
lastData: null,
|
|
41
49
|
errors: 0,
|
|
42
50
|
lastSuccess: null,
|
|
43
|
-
offline: false
|
|
51
|
+
offline: false,
|
|
52
|
+
abortController: null,
|
|
53
|
+
polling: false
|
|
44
54
|
};
|
|
45
55
|
|
|
46
56
|
const poll = async () => {
|
|
47
57
|
const poller = _remotePollers[serverId];
|
|
58
|
+
if (!poller || poller.polling) return;
|
|
59
|
+
poller.polling = true;
|
|
60
|
+
poller.abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
61
|
+
const timeoutId = poller.abortController
|
|
62
|
+
? setTimeout(() => poller.abortController && poller.abortController.abort(), 10000)
|
|
63
|
+
: null;
|
|
48
64
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
});
|
|
65
|
+
const fetchOptions = poller.abortController ? { signal: poller.abortController.signal } : {};
|
|
66
|
+
const res = await fetch(`/api/servers/${serverId}/stats`, fetchOptions);
|
|
52
67
|
if (res.ok) {
|
|
53
68
|
const data = await res.json();
|
|
54
69
|
const normalized = _normalizeRemoteStats(data);
|
|
@@ -56,15 +71,23 @@ function onRemoteStats(serverId, callback, refreshMs = 10000) {
|
|
|
56
71
|
poller.errors = 0;
|
|
57
72
|
poller.lastSuccess = Date.now();
|
|
58
73
|
poller.offline = false;
|
|
59
|
-
poller.callbacks.
|
|
74
|
+
poller.callbacks = poller.callbacks.filter(cb => {
|
|
75
|
+
if (cb && cb.isConnected === false) return false;
|
|
76
|
+
cb(normalized);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
if (poller.callbacks.length === 0) {
|
|
80
|
+
_teardownRemotePoller(serverId);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
60
83
|
} else {
|
|
61
84
|
throw new Error(`HTTP ${res.status}`);
|
|
62
85
|
}
|
|
63
86
|
} catch (e) {
|
|
87
|
+
if (!_remotePollers[serverId]) return;
|
|
64
88
|
poller.errors++;
|
|
65
89
|
console.warn(`Remote stats error (${serverId}, attempt ${poller.errors}):`, e.message);
|
|
66
90
|
|
|
67
|
-
// After 3 consecutive failures, mark as offline and notify widgets
|
|
68
91
|
if (poller.errors >= 3 && !poller.offline) {
|
|
69
92
|
poller.offline = true;
|
|
70
93
|
const offlineData = {
|
|
@@ -73,7 +96,21 @@ function onRemoteStats(serverId, callback, refreshMs = 10000) {
|
|
|
73
96
|
_lastSuccess: poller.lastSuccess,
|
|
74
97
|
_serverId: serverId
|
|
75
98
|
};
|
|
76
|
-
poller.callbacks.
|
|
99
|
+
poller.callbacks = poller.callbacks.filter(cb => {
|
|
100
|
+
if (cb && cb.isConnected === false) return false;
|
|
101
|
+
cb(offlineData);
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
if (poller.callbacks.length === 0) {
|
|
105
|
+
_teardownRemotePoller(serverId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
111
|
+
if (_remotePollers[serverId]) {
|
|
112
|
+
_remotePollers[serverId].polling = false;
|
|
113
|
+
_remotePollers[serverId].abortController = null;
|
|
77
114
|
}
|
|
78
115
|
}
|
|
79
116
|
};
|
|
@@ -84,7 +121,6 @@ function onRemoteStats(serverId, callback, refreshMs = 10000) {
|
|
|
84
121
|
|
|
85
122
|
_remotePollers[serverId].callbacks.push(callback);
|
|
86
123
|
|
|
87
|
-
// If we have cached data, call immediately
|
|
88
124
|
if (_remotePollers[serverId].lastData) {
|
|
89
125
|
callback(_remotePollers[serverId].lastData);
|
|
90
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lobsterboard",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Self-hosted drag-and-drop dashboard builder with 50 widgets, template gallery, and custom pages. Works standalone or with OpenClaw.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dashboard",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"server.cjs",
|
|
50
|
+
"server/",
|
|
50
51
|
"app.html",
|
|
51
52
|
"js/",
|
|
52
53
|
"css/",
|