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 +13 -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.js +3 -2
- package/package.json +1 -1
- package/server.cjs +42 -5
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
|
package/dist/lobsterboard.css
CHANGED
package/dist/lobsterboard.esm.js
CHANGED
package/dist/lobsterboard.umd.js
CHANGED
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;">' +
|
|
864
|
-
(ev.location ? '<div style="color:#8b949e;font-size:calc(11px * var(--font-scale, 1));margin-top:2px;">📍 ' +
|
|
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
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|