lobsterboard 0.2.4 → 0.2.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,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.5 - 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.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.2.4
2
+ * LobsterBoard v0.2.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.2.4
2
+ * LobsterBoard v0.2.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.2.4
2
+ * LobsterBoard v0.2.5
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
package/js/widgets.js CHANGED
@@ -856,12 +856,13 @@ const WIDGETS = {
856
856
  return;
857
857
  }
858
858
  function _escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
859
+ 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
860
  container.innerHTML = events.map(function(ev) {
860
861
  var timeStr = ev.allDay ? 'All Day' : new Date(ev.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
861
862
  return '<div style="padding:4px 0;border-bottom:1px solid #21262d;font-size:calc(13px * var(--font-scale, 1));">' +
862
863
  '<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>' : '') +
864
+ '<span style="color:#e6edf3;">' + _linkify(ev.summary || 'Untitled') + '</span>' +
865
+ (ev.location ? '<div style="color:#8b949e;font-size:calc(11px * var(--font-scale, 1));margin-top:2px;">📍 ' + _linkify(ev.location) + '</div>' : '') +
865
866
  '</div>';
866
867
  }).join('');
867
868
  } 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.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",
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;