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 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
@@ -1,4 +1,4 @@
1
- /* LobsterBoard v0.8.3 - Dashboard Styles */
1
+ /* LobsterBoard v0.8.5 - Dashboard Styles */
2
2
  /* LobsterBoard Dashboard - Generated Styles */
3
3
 
4
4
  :root {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.8.3
2
+ * LobsterBoard v0.8.5
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.8.3
2
+ * LobsterBoard v0.8.5
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.8.3
2
+ * LobsterBoard v0.8.5
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.8.3
2
+ * LobsterBoard v0.8.5
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -356,39 +356,71 @@
356
356
  </div>
357
357
  </section>`,
358
358
  generateJs: (props) => `
359
- async function update_${props.id.replace(/-/g, '_')}() {
360
- const el = document.getElementById('${props.id}-ticker');
361
- if (!el) return;
362
- const apiKey = '${props.apiKey || ''}';
363
- if (!apiKey) {
364
- el.innerHTML = 'Set API key in Edit Mode — <a href="https://finnhub.io/register" target="_blank" style="color:#58a6ff;">get free key →</a>';
365
- return;
366
- }
367
- const symbols = '${props.symbol || 'AAPL'}'.split(',').map(s => s.trim()).filter(Boolean);
368
- try {
369
- const results = await Promise.all(symbols.map(async (sym) => {
370
- try {
371
- const res = await fetch('https://finnhub.io/api/v1/quote?symbol=' + sym + '&token=' + apiKey);
372
- const data = await res.json();
373
- if (data.c === 0 && data.h === 0) return '<span class="ticker-link" style="color:#8b949e;">' + sym + ' —</span>';
374
- const change = ((data.c - data.pc) / data.pc * 100).toFixed(2);
375
- const color = change >= 0 ? '#3fb950' : '#f85149';
376
- const arrow = change >= 0 ? '▲' : '▼';
377
- return '<span class="ticker-link" style="cursor:default;">' +
378
- '<strong>' + sym + '</strong> $' + data.c.toFixed(2) +
379
- ' <span style="color:' + color + ';">' + arrow + ' ' + (change >= 0 ? '+' : '') + change + '%</span></span>';
380
- } catch (_) {
381
- return '<span class="ticker-link" style="color:#8b949e;">' + sym + ' —</span>';
382
- }
383
- }));
384
- el.innerHTML = results.join('<span class="ticker-sep"> \\u2022\\u2022\\u2022 </span>');
385
- } catch (e) {
386
- if (!el.dataset.loaded) el.textContent = 'Failed to load stocks';
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
- el.dataset.loaded = '1';
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 res = await fetch(`/api/servers/${serverId}/stats`, {
50
- signal: AbortSignal.timeout(10000) // 10s timeout
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.forEach(cb => cb(normalized));
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.forEach(cb => cb(offlineData));
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",
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/",