diff --git a/static/events/2025-10-01-sundbyberg.json b/static/events/2025-10-01-sundbyberg.json index 1ae35e9..870d51c 100644 --- a/static/events/2025-10-01-sundbyberg.json +++ b/static/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/static/js/rsvp-tracker.js b/static/js/rsvp-tracker.js index 80020da..4fb035e 100644 --- a/static/js/rsvp-tracker.js +++ b/static/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