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 +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 +13 -4
- 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
|
@@ -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 (
|
|
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 (
|
|
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;">' +
|
|
864
|
-
(ev.location ? '<div style="color:#8b949e;font-size:calc(11px * var(--font-scale, 1));margin-top:2px;">📍 ' +
|
|
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
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;
|