lobsterboard 0.2.4 → 0.2.6

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5] - 2026-02-19
4
+
5
+ ### Fixed
6
+ - **iCal timezone parsing** — calendar events now display at correct times regardless of timezone (TZID parameter support) — thanks @jlgrimes!
7
+
8
+ ### Added
9
+ - **Clickable URLs in calendar** — Zoom/Teams links in event summaries and locations are now hyperlinks — thanks @jlgrimes!
10
+
11
+ ## [0.2.4] - 2026-02-17
12
+
13
+ ### Fixed
14
+ - SSRF vulnerability in RSS feed proxy (thanks @jlgrimes for the security report!)
15
+
3
16
  ## [0.2.3] - 2026-02-16
4
17
 
5
18
  ### Added
@@ -1,4 +1,4 @@
1
- /* LobsterBoard v0.2.4 - Dashboard Styles */
1
+ /* LobsterBoard v0.2.6 - Dashboard Styles */
2
2
  /* LobsterBoard Dashboard - Generated Styles */
3
3
 
4
4
  :root {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.2.4
2
+ * LobsterBoard v0.2.6
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.2.4
2
+ * LobsterBoard v0.2.6
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.2.4
2
+ * LobsterBoard v0.2.6
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.2.4
2
+ * LobsterBoard v0.2.6
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
package/js/widgets.js CHANGED
@@ -346,12 +346,16 @@ const WIDGETS = {
346
346
 
347
347
  const cur = (data.current || '').replace(/^v/, '');
348
348
  const lat = (data.latest || '').replace(/^v/, '');
349
+ // Strip -N suffixes for comparison (e.g. 2026.2.22-2 matches 2026.2.22)
350
+ const curBase = cur.replace(/-\d+$/, '');
351
+ const latBase = lat.replace(/-\d+$/, '');
352
+ const isUpToDate = cur === lat || curBase === latBase || cur.startsWith(latBase + '-');
349
353
 
350
354
  if (!cur || cur === 'unknown') {
351
355
  currentEl.textContent = 'v' + lat;
352
356
  statusEl.textContent = 'Latest release';
353
357
  statusEl.style.color = '#8b949e';
354
- } else if (cur === lat) {
358
+ } else if (isUpToDate) {
355
359
  currentEl.textContent = 'v' + cur;
356
360
  currentEl.style.color = '#3fb950';
357
361
  statusEl.innerHTML = '✓ Up to date';
@@ -424,12 +428,16 @@ const WIDGETS = {
424
428
 
425
429
  const cur = (data.current || '').replace(/^v/, '');
426
430
  const lat = (data.latest || '').replace(/^v/, '');
431
+ // Strip -N suffixes for comparison (e.g. 2026.2.22-2 matches 2026.2.22)
432
+ const curBase = cur.replace(/-\d+$/, '');
433
+ const latBase = lat.replace(/-\d+$/, '');
434
+ const isUpToDate = cur === lat || curBase === latBase || cur.startsWith(latBase + '-');
427
435
 
428
436
  if (!cur || cur === 'unknown') {
429
437
  currentEl.textContent = 'v' + lat;
430
438
  statusEl.textContent = 'Latest release';
431
439
  statusEl.style.color = '#8b949e';
432
- } else if (cur === lat) {
440
+ } else if (isUpToDate) {
433
441
  currentEl.textContent = 'v' + cur;
434
442
  currentEl.style.color = '#3fb950';
435
443
  statusEl.innerHTML = '✓ Up to date';
@@ -856,12 +864,13 @@ const WIDGETS = {
856
864
  return;
857
865
  }
858
866
  function _escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
867
+ function _linkify(s) { return _escHtml(s).replace(/(https?:\\/\\/[^\\s<]+)/g, '<a href="$1" target="_blank" rel="noopener" style="color:#58a6ff;text-decoration:underline;">$1</a>'); }
859
868
  container.innerHTML = events.map(function(ev) {
860
869
  var timeStr = ev.allDay ? 'All Day' : new Date(ev.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
861
870
  return '<div style="padding:4px 0;border-bottom:1px solid #21262d;font-size:calc(13px * var(--font-scale, 1));">' +
862
871
  '<span style="color:#58a6ff;">' + timeStr + '</span> ' +
863
- '<span style="color:#e6edf3;">' + _escHtml(ev.summary || 'Untitled') + '</span>' +
864
- (ev.location ? '<div style="color:#8b949e;font-size:calc(11px * var(--font-scale, 1));margin-top:2px;">📍 ' + _escHtml(ev.location) + '</div>' : '') +
872
+ '<span style="color:#e6edf3;">' + _linkify(ev.summary || 'Untitled') + '</span>' +
873
+ (ev.location ? '<div style="color:#8b949e;font-size:calc(11px * var(--font-scale, 1));margin-top:2px;">📍 ' + _linkify(ev.location) + '</div>' : '') +
865
874
  '</div>';
866
875
  }).join('');
867
876
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobsterboard",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
package/server.cjs CHANGED
@@ -420,22 +420,59 @@ function parseIcal(text, maxEvents) {
420
420
  const block = blocks[i].split('END:VEVENT')[0];
421
421
  if (!block) continue;
422
422
  const get = (key) => { const m = block.match(new RegExp('^' + key + '(?:;[^:]*)?:(.*)$', 'm')); return m ? m[1].trim() : ''; };
423
+ const getWithParams = (key) => { const m = block.match(new RegExp('^' + key + '((?:;[^:]*)?):(.*)$', 'm')); return m ? { params: m[1], value: m[2].trim() } : { params: '', value: '' }; };
423
424
  const summary = get('SUMMARY').replace(/\\,/g, ',').replace(/\\n/g, ' ');
424
425
  const location = get('LOCATION').replace(/\\,/g, ',').replace(/\\n/g, ' ');
425
426
  const dtstart = get('DTSTART');
426
- const dtend = get('DTEND');
427
+ const dtstartFull = getWithParams('DTSTART');
428
+ const dtendFull = getWithParams('DTEND');
427
429
  if (!dtstart) continue;
428
430
  // Parse iCal date: 20260210T150000Z or 20260210 (all-day)
429
431
  const allDay = dtstart.length === 8;
430
- const parseIcalDate = (s) => {
432
+ // Map Windows/iCal TZID names to UTC offset (hours). Covers common zones.
433
+ const tzOffsets = {
434
+ 'eastern standard time': -5, 'eastern daylight time': -4, 'us/eastern': -5, 'america/new_york': -5,
435
+ 'central standard time': -6, 'central daylight time': -5, 'us/central': -6, 'america/chicago': -6,
436
+ 'central america standard time': -6,
437
+ 'mountain standard time': -7, 'mountain daylight time': -6, 'us/mountain': -7, 'america/denver': -7,
438
+ 'pacific standard time': -8, 'pacific daylight time': -7, 'us/pacific': -8, 'america/los_angeles': -8,
439
+ 'pacific standard time (mexico)': -8,
440
+ 'india standard time': 5.5, 'asia/kolkata': 5.5,
441
+ 'sri lanka standard time': 5.5,
442
+ 'singapore standard time': 8, 'asia/singapore': 8,
443
+ 'china standard time': 8, 'asia/shanghai': 8,
444
+ 'tokyo standard time': 9, 'asia/tokyo': 9,
445
+ 'e. africa standard time': 3,
446
+ 'romance standard time': 1,
447
+ 'gmt standard time': 0, 'utc': 0, 'gmt': 0,
448
+ 'w. europe standard time': 1, 'europe/berlin': 1, 'europe/paris': 1,
449
+ };
450
+ const parseIcalDate = (s, params) => {
431
451
  if (!s) return null;
432
452
  if (s.length === 8) return new Date(s.slice(0,4) + '-' + s.slice(4,6) + '-' + s.slice(6,8) + 'T00:00:00');
433
453
  // 20260210T150000Z or 20260210T150000
434
454
  const d = s.replace(/Z$/, '');
435
- return new Date(d.slice(0,4) + '-' + d.slice(4,6) + '-' + d.slice(6,8) + 'T' + d.slice(9,11) + ':' + d.slice(11,13) + ':' + d.slice(13,15) + (s.endsWith('Z') ? 'Z' : ''));
455
+ const iso = d.slice(0,4) + '-' + d.slice(4,6) + '-' + d.slice(6,8) + 'T' + d.slice(9,11) + ':' + d.slice(11,13) + ':' + d.slice(13,15);
456
+ if (s.endsWith('Z')) return new Date(iso + 'Z');
457
+ // Check for TZID parameter
458
+ const tzMatch = (params || '').match(/TZID=([^;:]+)/i);
459
+ if (tzMatch) {
460
+ const tzName = tzMatch[1].trim().toLowerCase();
461
+ const offsetHours = tzOffsets[tzName];
462
+ if (offsetHours !== undefined) {
463
+ // Convert from source timezone to UTC by appending the UTC offset
464
+ const sign = offsetHours >= 0 ? '+' : '-';
465
+ const absH = Math.floor(Math.abs(offsetHours));
466
+ const absM = Math.round((Math.abs(offsetHours) - absH) * 60);
467
+ const offsetStr = sign + String(absH).padStart(2, '0') + ':' + String(absM).padStart(2, '0');
468
+ return new Date(iso + offsetStr);
469
+ }
470
+ }
471
+ // No timezone info — treat as local
472
+ return new Date(iso);
436
473
  };
437
- const start = parseIcalDate(dtstart);
438
- const end = parseIcalDate(dtend);
474
+ const start = parseIcalDate(dtstart, dtstartFull.params);
475
+ const end = parseIcalDate(dtendFull.value, dtendFull.params);
439
476
  if (!start || isNaN(start.getTime())) continue;
440
477
  // Only future events (for all-day, include today)
441
478
  const cutoff = allDay ? new Date(now.getFullYear(), now.getMonth(), now.getDate()) : now;