diff --git a/events/2025-10-01-sundbyberg.json b/events/2025-10-01-sundbyberg.json index 1ae35e9..870d51c 100644 --- a/events/2025-10-01-sundbyberg.json +++ b/events/2025-10-01-sundbyberg.json @@ -1,9 +1,104 @@ { "messagePattern": "AW 1/10", "description": "Meetup 1 Oktober - Sundbyberg", + "exportedAt": "2025-09-28T10:59:49.506Z", "attendees": { - "yes": [], - "maybe": [], - "no": [] + "yes": [ + { + "nodeId": "2665944228", + "shortName": "TPH1", + "longName": "TPH-1 Täby", + "response": "yes", + "timestamp": "2025-09-27T11:18:40.725Z" + }, + { + "nodeId": "1422344156", + "shortName": "TELE", + "longName": "MDG Telefonplan", + "response": "yes", + "timestamp": "2025-09-26T23:07:19.802Z" + }, + { + "nodeId": "2718571204", + "shortName": "DXD3", + "longName": "DXD3 Ruggen", + "response": "yes", + "timestamp": "2025-09-26T20:14:58.613Z" + }, + { + "nodeId": "2947306466", + "shortName": "CVK", + "longName": "SA0CVK Primary", + "response": "yes", + "timestamp": "2025-09-26T19:53:44.302Z" + }, + { + "nodeId": "2879616005", + "shortName": "JlyT", + "longName": "Jelly Test", + "response": "yes", + "timestamp": "2025-09-26T14:25:11.116Z" + }, + { + "nodeId": "1128157388", + "shortName": "varg", + "longName": "vargtastic", + "response": "yes", + "timestamp": "2025-09-25T16:47:09.777Z" + }, + { + "nodeId": "3681979732", + "shortName": "Ros", + "longName": "Ros Mobil", + "response": "yes", + "timestamp": "2025-09-24T16:02:08.895Z" + }, + { + "nodeId": "1978245180", + "shortName": "CVK", + "longName": "SA0CVK 0 (Ny Router)", + "response": "yes", + "timestamp": "2025-09-22T17:06:11.296Z" + }, + { + "nodeId": "3677336824", + "shortName": "Chip", + "longName": "Chip", + "response": "yes", + "timestamp": "2025-09-22T17:00:53.857Z" + }, + { + "nodeId": "3655315140", + "shortName": "Ros9", + "longName": "Roslund9", + "response": "yes", + "timestamp": "2025-09-22T05:37:43.576Z" + }, + { + "nodeId": "1222480744", + "shortName": "JwKL", + "longName": "JwK LF", + "response": "yes", + "timestamp": "2025-09-21T20:54:17.235Z" + } + ], + "maybe": [ + { + "nodeId": "364861217", + "shortName": "KAKd", + "longName": "Kladdkakd", + "response": "maybe", + "timestamp": "2025-09-22T22:00:55.197Z" + } + ], + "no": [ + { + "nodeId": "3946133666", + "shortName": "phPi", + "longName": "ph Raspberry Pi", + "response": "no", + "timestamp": "2025-09-26T20:11:29.025Z" + } + ] } } \ No newline at end of file diff --git a/js/rsvp-tracker.js b/js/rsvp-tracker.js index 80020da..4fb035e 100644 --- a/js/rsvp-tracker.js +++ b/js/rsvp-tracker.js @@ -24,7 +24,7 @@ function parseRSVPMessage(messageText, messagePattern) { } async function fetchMessages() { - const response = await fetch('https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=1000'); + const response = await fetch('https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=3000'); const data = await response.json(); return data.text_messages; } @@ -159,6 +159,49 @@ function generateRSVPHTML(summary) { } +function mergeRSVPSummaries(primarySummary, manualAttendees) { + const latestByNode = new Map(); + + const addFrom = (collection, fallbackType) => { + if (!Array.isArray(collection)) return; + for (const attendee of collection) { + const nodeId = attendee.nodeId ?? attendee.node_id ?? attendee.id; + if (!nodeId) continue; + const responseType = attendee.response ?? fallbackType; + if (!responseType) continue; + const timestampString = attendee.timestamp ?? null; + const ts = timestampString ? new Date(timestampString).getTime() : -Infinity; + + const existing = latestByNode.get(nodeId); + if (!existing || ts > existing.ts) { + latestByNode.set(nodeId, { + nodeId: nodeId, + shortName: attendee.shortName ?? attendee.short_name ?? '?', + longName: attendee.longName ?? attendee.long_name ?? `!${parseInt(nodeId).toString(16)}`, + response: responseType, + timestamp: timestampString ?? existing?.timestamp ?? new Date(0).toISOString(), + ts + }); + } + } + }; + + ['yes', 'maybe', 'no'].forEach(type => addFrom(primarySummary?.[type], type)); + ['yes', 'maybe', 'no'].forEach(type => addFrom(manualAttendees?.[type], type)); + + const merged = { yes: [], maybe: [], no: [] }; + for (const { ts, ...attendee } of latestByNode.values()) { + if (merged[attendee.response]) merged[attendee.response].push(attendee); + } + + Object.values(merged).forEach(arr => + arr.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) + ); + + return merged; +} + + /** * Initialize RSVP tracking for a specific event * Supports both active events (parse messages + JSON) and archived events (JSON only) @@ -192,10 +235,8 @@ async function initRSVPTracker(eventId) { const responses = parseRSVPResponses(messages, eventData.messagePattern); summary = createRSVPSummary(responses); - // Add manual attendees - Object.entries(eventData.attendees).forEach(([type, attendees]) => { - summary[type].push(...attendees); - }); + // Merge manual attendees with message-derived ones, keeping the latest response per attendee + summary = mergeRSVPSummaries(summary, eventData.attendees); } // Display results diff --git a/js/status/channel-utilization.js b/js/status/channel-utilization.js index 354ad52..57d9e48 100644 --- a/js/status/channel-utilization.js +++ b/js/status/channel-utilization.js @@ -71,8 +71,8 @@ async function channelUtilizationHourlyAverage() { }, }, y: { - min: 10, - max: 24, + min: 8, + max: 26, title: { display: true, text: 'Avg. Channel Utilization (%)' } } }, @@ -90,20 +90,20 @@ async function channelUtilizationHourlyAverage() { annotations: { bad: { type: 'box', - yMin: 20, - yMax: 24, + yMin: 22, + yMax: 100, backgroundColor: 'rgba(255, 0, 0, 0.2)' }, medium: { type: 'box', - yMin: 15, - yMax: 20, + yMin: 16, + yMax: 22, backgroundColor: 'rgba(234, 184, 57, 0.2)', //'rgba(255, 165, 0, 0.1)' }, good: { type: 'box', yMin: 0, - yMax: 15, + yMax: 16, backgroundColor: 'rgba(1, 163, 23, 0.1)', //'rgba(0, 128, 0, 0.1)' } }