mirror of
https://github.com/Roslund/sthlm-mesh.git
synced 2026-08-06 08:53:24 +02:00
deploy: 12b302dea8
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"messagePattern": "AW 19/8",
|
||||
"archived": true,
|
||||
"exportedAt": "2025-08-20T19:11:59.376Z",
|
||||
"description": "AW Telefonplan meetup",
|
||||
"attendees": {
|
||||
"yes": [
|
||||
{
|
||||
"nodeId": "364861217",
|
||||
"shortName": "KAKd",
|
||||
"longName": "Kladdkakd",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T21:55:44.225Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "910104769",
|
||||
"shortName": "TXD0",
|
||||
"longName": "Wibbe",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T20:01:19.763Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2745076674",
|
||||
"shortName": "TWK5",
|
||||
"longName": "TWK-Mobil T114",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T18:13:36.385Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1258726557",
|
||||
"shortName": "JwKC",
|
||||
"longName": "JwK Car",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T11:51:05.986Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3681979732",
|
||||
"shortName": "Ros",
|
||||
"longName": "Ros Mobil",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-15T05:19:51.846Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "762864438",
|
||||
"shortName": "TUFx",
|
||||
"longName": "Lasse",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-15T04:15:40.832Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2718571204",
|
||||
"shortName": "DXD3",
|
||||
"longName": "DXD3 Ruggen",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:33:19.153Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1422344156",
|
||||
"shortName": "TELE",
|
||||
"longName": "MDG Telefonplan",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:19:56.688Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2152997845",
|
||||
"shortName": "MDG5",
|
||||
"longName": "MDG5 /R1",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:16:29.975Z"
|
||||
}
|
||||
],
|
||||
"maybe": [
|
||||
{
|
||||
"nodeId": "220489446",
|
||||
"shortName": "SolS",
|
||||
"longName": "SolarStation",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-17T07:26:51.406Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2892741592",
|
||||
"shortName": "bbd8",
|
||||
"longName": "Meshtastic bbd8",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-16T11:48:12.544Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "4102046320",
|
||||
"shortName": "DLTA",
|
||||
"longName": "Delta Flyer",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-14T21:23:49.942Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2879616005",
|
||||
"shortName": "JlyT",
|
||||
"longName": "Jelly Test",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-14T20:48:10.687Z"
|
||||
}
|
||||
],
|
||||
"no": [
|
||||
{
|
||||
"nodeId": "2956846808",
|
||||
"shortName": "LSD",
|
||||
"longName": "R∆dioW∆ve🇸🇪",
|
||||
"response": "no",
|
||||
"timestamp": "2025-08-19T11:39:03.262Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2947306466",
|
||||
"shortName": "CVK",
|
||||
"longName": "SA0CVK Primary",
|
||||
"response": "no",
|
||||
"timestamp": "2025-08-18T12:51:47.594Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+206
-280
@@ -1,295 +1,170 @@
|
||||
/**
|
||||
* RSVP Tracker for Meshtastic Events
|
||||
* Tracks attendance responses from LoRa mesh messages
|
||||
*/
|
||||
// RSVP response patterns (order matters - check longer phrases first!)
|
||||
const RESPONSE_PATTERNS = {
|
||||
'no': ['kommer inte', 'no', 'nej', 'not attending', 'kan inte', 'cannot attend'],
|
||||
'maybe': ['kanske', 'maybe', 'unsure', 'oklart', 'tvekar'],
|
||||
'yes': ['kommer', 'yes', 'ja', 'attending', 'deltar']
|
||||
};
|
||||
|
||||
class RSVPTracker {
|
||||
constructor(eventLabel) {
|
||||
this.eventLabel = eventLabel;
|
||||
this.state = {
|
||||
messages: [],
|
||||
nodesById: {},
|
||||
rsvpResponses: new Map() // nodeId -> latest response
|
||||
};
|
||||
function parseRSVPMessage(messageText, messagePattern) {
|
||||
const text = messageText.trim().toLowerCase();
|
||||
const pattern = messagePattern.toLowerCase();
|
||||
|
||||
if (!text.includes(pattern)) return null;
|
||||
|
||||
// Find response type by checking patterns
|
||||
for (const [responseType, patterns] of Object.entries(RESPONSE_PATTERNS)) {
|
||||
for (const p of patterns) {
|
||||
if (text.includes(p)) {
|
||||
return { type: responseType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchMessages() {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=500');
|
||||
const data = await response.json();
|
||||
return data.text_messages;
|
||||
}
|
||||
|
||||
function parseRSVPResponses(messages, messagePattern) {
|
||||
const responses = new Map();
|
||||
|
||||
for (const message of messages) {
|
||||
const rsvp = parseRSVPMessage(message.text, messagePattern);
|
||||
if (!rsvp) continue;
|
||||
|
||||
// RSVP response patterns (order matters - check longer phrases first!)
|
||||
this.responsePatterns = {
|
||||
'kommer inte': ['kommer inte', 'no', 'nej', 'not attending', 'kan inte', 'cannot attend'],
|
||||
'kanske': ['kanske', 'maybe', 'unsure', 'oklart', 'tvekar'],
|
||||
'kommer': ['kommer', 'yes', 'ja', 'attending', 'deltar']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RSVP message format: "EventLabel - Response"
|
||||
* Example: "AW 21/8 - Kommer"
|
||||
*/
|
||||
parseRSVPMessage(messageText) {
|
||||
const text = messageText.trim().toLowerCase();
|
||||
|
||||
// Check if message matches our event pattern
|
||||
const eventPattern = this.eventLabel.toLowerCase();
|
||||
if (!text.includes(eventPattern)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the response part after the separator
|
||||
const separatorIndex = text.indexOf(' - ');
|
||||
if (separatorIndex === -1) {
|
||||
// Try alternative separators
|
||||
const altSeparators = [' -', '- ', '-', ':'];
|
||||
let response = null;
|
||||
|
||||
for (const sep of altSeparators) {
|
||||
const sepIndex = text.indexOf(sep);
|
||||
if (sepIndex !== -1 && text.substring(0, sepIndex).includes(eventPattern)) {
|
||||
response = text.substring(sepIndex + sep.length).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response) return null;
|
||||
} else {
|
||||
const response = text.substring(separatorIndex + 3).trim();
|
||||
}
|
||||
|
||||
// Determine response type
|
||||
for (const [responseType, patterns] of Object.entries(this.responsePatterns)) {
|
||||
for (const pattern of patterns) {
|
||||
if (text.includes(pattern)) {
|
||||
return {
|
||||
type: responseType,
|
||||
originalText: messageText,
|
||||
isValid: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch messages from the API and parse RSVP responses
|
||||
*/
|
||||
async fetchRSVPData() {
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=500');
|
||||
const data = await response.json();
|
||||
|
||||
// Filter duplicate messages and sort by timestamp
|
||||
this.state.messages = Array.from(
|
||||
new Map(data.text_messages.map(msg => [msg.packet_id, msg])).values()
|
||||
).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
// Parse RSVP responses
|
||||
this.parseRSVPResponses();
|
||||
|
||||
// Fetch node information for all RSVP responders
|
||||
await this.fetchNodeInfoForRSVPs();
|
||||
|
||||
return this.getRSVPSummary();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching RSVP data:', error);
|
||||
throw error;
|
||||
const existing = responses.get(message.from);
|
||||
if (!existing || new Date(message.created_at) > new Date(existing.timestamp)) {
|
||||
responses.set(message.from, {
|
||||
nodeId: message.from,
|
||||
response: rsvp.type,
|
||||
timestamp: message.created_at
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all messages for RSVP responses
|
||||
*/
|
||||
parseRSVPResponses() {
|
||||
this.state.rsvpResponses.clear();
|
||||
|
||||
for (const message of this.state.messages) {
|
||||
const rsvp = this.parseRSVPMessage(message.text);
|
||||
if (rsvp) {
|
||||
// Only keep the latest response from each node
|
||||
const existingResponse = this.state.rsvpResponses.get(message.from);
|
||||
if (!existingResponse || new Date(message.created_at) > new Date(existingResponse.timestamp)) {
|
||||
this.state.rsvpResponses.set(message.from, {
|
||||
nodeId: message.from,
|
||||
response: rsvp.type,
|
||||
originalText: rsvp.originalText,
|
||||
timestamp: message.created_at,
|
||||
messageId: message.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findNodeById(id) {
|
||||
return nodes.find(node => node.node_id.toString() === id.toString()) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch node information for all RSVP responders
|
||||
*/
|
||||
async fetchNodeInfoForRSVPs() {
|
||||
const nodeIds = Array.from(this.state.rsvpResponses.keys());
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
if (!this.state.nodesById[nodeId]) {
|
||||
try {
|
||||
const response = await fetch(`https://map.sthlm-mesh.se/api/v1/nodes/${nodeId}`);
|
||||
const data = await response.json();
|
||||
if (data.node) {
|
||||
this.state.nodesById[nodeId] = data.node;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch node info for ${nodeId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function createRSVPSummary(responses) {
|
||||
const summary = { yes: [], maybe: [], no: [] };
|
||||
|
||||
/**
|
||||
* Get summary of RSVP responses
|
||||
*/
|
||||
getRSVPSummary() {
|
||||
const summary = {
|
||||
kommer: [],
|
||||
kanske: [],
|
||||
'kommer inte': [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
for (const [nodeId, rsvp] of this.state.rsvpResponses) {
|
||||
const node = this.state.nodesById[nodeId];
|
||||
const attendee = {
|
||||
nodeId: nodeId,
|
||||
shortName: node?.short_name || '?',
|
||||
longName: node?.long_name || `!${parseInt(nodeId).toString(16)}`,
|
||||
response: rsvp.response,
|
||||
timestamp: rsvp.timestamp,
|
||||
originalText: rsvp.originalText
|
||||
};
|
||||
|
||||
if (summary[rsvp.response]) {
|
||||
summary[rsvp.response].push(attendee);
|
||||
}
|
||||
summary.total++;
|
||||
}
|
||||
|
||||
// Sort each category by timestamp (most recent first)
|
||||
Object.keys(summary).forEach(key => {
|
||||
if (Array.isArray(summary[key])) {
|
||||
summary[key].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
}
|
||||
for (const [nodeId, rsvp] of responses) {
|
||||
const node = findNodeById(nodeId);
|
||||
summary[rsvp.response]?.push({
|
||||
nodeId,
|
||||
shortName: node?.short_name || '?',
|
||||
longName: node?.long_name || `!${parseInt(nodeId).toString(16)}`,
|
||||
response: rsvp.response,
|
||||
timestamp: rsvp.timestamp
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML for displaying RSVP results
|
||||
*/
|
||||
generateRSVPHTML(summary) {
|
||||
const html = `
|
||||
<div class="rsvp-tracker mt-4">
|
||||
<h4>📋 Anmälningsstatus</h4>
|
||||
<p class="text-muted">Skicka meddelande över LoRa-meshen: "<strong>${this.eventLabel} - [Kommer/Kanske/Kommer inte]</strong>"</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0">✅ Kommer (${summary.kommer.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${this.generateAttendeeList(summary.kommer)}
|
||||
</div>
|
||||
// Sort by timestamp (most recent first)
|
||||
Object.values(summary).forEach(arr =>
|
||||
arr.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function getNodeColour(nodeId) {
|
||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||
}
|
||||
|
||||
function generateAttendeeList(attendees) {
|
||||
if (attendees.length === 0) {
|
||||
return '<p class="text-muted small">Inga svar än</p>';
|
||||
}
|
||||
|
||||
return attendees.map(attendee => `
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="rounded-circle d-flex justify-content-center align-items-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: ${getNodeColour(attendee.nodeId)}; color: white; font-size: 12px;">
|
||||
${attendee.shortName.substring(0, 4)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="small">
|
||||
<a href="https://map.sthlm-mesh.se/?node_id=${attendee.nodeId}"
|
||||
target="_blank" class="text-decoration-none">
|
||||
${attendee.longName}
|
||||
</a>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${new Date(attendee.timestamp).toLocaleString('sv-SE', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function generateRSVPHTML(summary) {
|
||||
return `
|
||||
<div class="rsvp-tracker mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0">✅ Kommer (${summary.yes.length})</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h6 class="mb-0">❓ Kanske (${summary.kanske.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${this.generateAttendeeList(summary.kanske)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h6 class="mb-0">❌ Kommer inte (${summary['kommer inte'].length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${this.generateAttendeeList(summary['kommer inte'])}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.yes)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<small class="text-muted">
|
||||
Totalt ${summary.total} svar •
|
||||
Senast uppdaterad: ${new Date().toLocaleString('sv-SE')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate attendee list HTML
|
||||
*/
|
||||
generateAttendeeList(attendees) {
|
||||
if (attendees.length === 0) {
|
||||
return '<p class="text-muted small">Inga svar än</p>';
|
||||
}
|
||||
|
||||
return attendees.map(attendee => `
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="rounded-circle d-flex justify-content-center align-items-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: ${this.getNodeColour(attendee.nodeId)}; color: white; font-size: 12px;">
|
||||
${attendee.shortName.substring(0, 4)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="small">
|
||||
<a href="https://map.sthlm-mesh.se/?node_id=${attendee.nodeId}"
|
||||
target="_blank" class="text-decoration-none">
|
||||
${attendee.longName}
|
||||
</a>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h6 class="mb-0">❓ Kanske (${summary.maybe.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.maybe)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${new Date(attendee.timestamp).toLocaleString('sv-SE', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h6 class="mb-0">❌ Kommer inte (${summary.no.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.no)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node color based on ID
|
||||
*/
|
||||
getNodeColour(nodeId) {
|
||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<small class="text-muted">
|
||||
Totalt ${summary.yes.length + summary.maybe.length + summary.no.length} svar •
|
||||
Senast uppdaterad: ${new Date().toLocaleString('sv-SE')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate container ID from event label
|
||||
*/
|
||||
function generateContainerId(eventLabel) {
|
||||
return 'rsvp-tracker-' + eventLabel.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize RSVP tracking for a specific event
|
||||
* Supports both active events (parse messages + JSON) and archived events (JSON only)
|
||||
*/
|
||||
async function initRSVPTracker(eventLabel) {
|
||||
const tracker = new RSVPTracker(eventLabel);
|
||||
const containerId = generateContainerId(eventLabel);
|
||||
async function initRSVPTracker(eventId) {
|
||||
const containerId = 'rsvp-tracker-' + eventId;
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
@@ -301,19 +176,30 @@ async function initRSVPTracker(eventLabel) {
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="text-center"><div class="spinner-border" role="status"></div><p>Laddar anmälningar...</p></div>';
|
||||
|
||||
// Fetch and display RSVP data
|
||||
const summary = await tracker.fetchRSVPData();
|
||||
container.innerHTML = tracker.generateRSVPHTML(summary);
|
||||
const eventData = await loadEventDataFromJSON(eventId);
|
||||
|
||||
// Set up auto-refresh every 60 seconds
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const summary = await tracker.fetchRSVPData();
|
||||
container.innerHTML = tracker.generateRSVPHTML(summary);
|
||||
} catch (error) {
|
||||
console.error('Error refreshing RSVP data:', error);
|
||||
}
|
||||
}, 60000);
|
||||
if (!eventData) {
|
||||
throw new Error(`Event data file not found: /events/${eventId}.json`);
|
||||
}
|
||||
|
||||
let summary;
|
||||
|
||||
if (eventData.archived) {
|
||||
summary = eventData.attendees;
|
||||
} else {
|
||||
await fetchNodes();
|
||||
const messages = await fetchMessages();
|
||||
const responses = parseRSVPResponses(messages, eventData.messagePattern);
|
||||
summary = createRSVPSummary(responses);
|
||||
|
||||
// Add manual attendees
|
||||
Object.entries(eventData.attendees).forEach(([type, attendees]) => {
|
||||
summary[type].push(...attendees);
|
||||
});
|
||||
}
|
||||
|
||||
// Display results
|
||||
container.innerHTML = generateRSVPHTML(summary);
|
||||
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div class="alert alert-danger">Kunde inte ladda anmälningar: ${error.message}</div>`;
|
||||
@@ -321,6 +207,46 @@ async function initRSVPTracker(eventLabel) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current RSVP data as JSON (for manual browser use)
|
||||
* Call this function in browser console: exportRSVPAsJSON('event-id', 'Message Pattern')
|
||||
*/
|
||||
async function exportRSVPAsJSON(eventId, messagePattern) {
|
||||
try {
|
||||
console.log('Fetching RSVP data for export...');
|
||||
|
||||
await fetchNodes(); // Load nodes cache
|
||||
const messages = await fetchMessages();
|
||||
const responses = parseRSVPResponses(messages, messagePattern);
|
||||
const summary = createRSVPSummary(responses);
|
||||
|
||||
const exportData = {
|
||||
messagePattern,
|
||||
archived: true,
|
||||
exportedAt: new Date().toISOString(),
|
||||
attendees: summary
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(exportData, null, 2));
|
||||
return exportData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error exporting RSVP data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEventDataFromJSON(eventId) {
|
||||
try {
|
||||
const response = await fetch(`/events/${eventId}.json`);
|
||||
if (!response.ok) throw new Error(`Event data not found: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Could not load event data for ${eventId}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.RSVPTracker = RSVPTracker;
|
||||
window.initRSVPTracker = initRSVPTracker;
|
||||
window.exportRSVPAsJSON = exportRSVPAsJSON;
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"><url><loc>https://sthlm-mesh.se/docs/settings/</loc><lastmod>2025-02-26T18:39:36+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/device_role/</loc><lastmod>2025-02-26T18:42:15+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/position/</loc><lastmod>2025-02-22T09:11:26+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/mqtt/</loc><lastmod>2025-03-31T19:16:34+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/neighbor_info/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/kartor/</loc><lastmod>2025-03-08T20:11:28+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/hardware/</loc><lastmod>2025-04-21T12:00:04+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/solar_nodes/</loc><lastmod>2025-04-21T01:54:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/communities/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/categories/</loc></url><url><loc>https://sthlm-mesh.se/docs/</loc><lastmod>2025-03-03T22:48:04+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/messages/</loc><lastmod>2025-07-13T10:01:31+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/meetups/</loc><lastmod>2025-08-14T20:42:21+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/about/</loc><lastmod>2025-02-23T18:02:23+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/status/</loc><lastmod>2025-08-10T11:53:58+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/</loc><lastmod>2025-06-10T01:31:45+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/tags/</loc></url></urlset>
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"><url><loc>https://sthlm-mesh.se/docs/settings/</loc><lastmod>2025-02-26T18:39:36+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/device_role/</loc><lastmod>2025-02-26T18:42:15+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/position/</loc><lastmod>2025-02-22T09:11:26+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/mqtt/</loc><lastmod>2025-03-31T19:16:34+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/neighbor_info/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/kartor/</loc><lastmod>2025-03-08T20:11:28+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/hardware/</loc><lastmod>2025-04-21T12:00:04+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/solar_nodes/</loc><lastmod>2025-04-21T01:54:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/communities/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/categories/</loc></url><url><loc>https://sthlm-mesh.se/docs/</loc><lastmod>2025-03-03T22:48:04+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/messages/</loc><lastmod>2025-07-13T10:01:31+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/meetups/</loc><lastmod>2025-08-22T20:09:05+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/about/</loc><lastmod>2025-02-23T18:02:23+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/status/</loc><lastmod>2025-08-10T11:53:58+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/</loc><lastmod>2025-06-10T01:31:45+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/tags/</loc></url></urlset>
|
||||
Reference in New Issue
Block a user