/** * Glass Government Legistar Worker * Version: 1.3.7 * - JSON first, XML fallback * - Cache with debug bypass * - HARD 30-day lookback + future events * - Correct Legistar slugs (madison, dane) */ export default { async fetch(request, env, ctx) { const url = new URL(request.url); const debug = url.searchParams.get("debug") === "1"; let client; if (url.pathname === "/events/madison") client = "madison"; else if (url.pathname === "/events/dane") client = "dane"; // ✅ FIX else return new Response("Not found", { status: 404 }); const cacheKey = new Request(url.origin + url.pathname); const cache = caches.default; if (!debug) { const cached = await cache.match(cacheKey); if (cached) return cached; } // No OData filters. Pull everything. Filter locally. const legistarURL = `https://webapi.legistar.com/v1/${client}/events?$orderby=EventDate`; let events = []; try { const res = await fetch(legistarURL, { headers: { "Accept": "application/json", "User-Agent": "GlassGovernment/legistar-cache" } }); const rawText = await res.text(); if (debug) { return new Response(JSON.stringify({ source: client, url: legistarURL, raw_response: rawText }, null, 2), { headers: { "Content-Type": "application/json" } }); } let json; try { json = JSON.parse(rawText); } catch { json = null; } if (Array.isArray(json)) { events = json.map(e => normalizeEvent(e, client)).filter(Boolean); } else if (json && Array.isArray(json.value)) { events = json.value.map(e => normalizeEvent(e, client)).filter(Boolean); } else if (rawText.includes("")) { events = normalizeXML(rawText, client); } // HARD FILTER: last 30 days + future events = events.filter(e => isWithinLast30DaysOrFuture(e.datetime)); } catch (err) { events = []; } const response = new Response(JSON.stringify({ source: client, count: events.length, generated: new Date().toISOString(), events }), { headers: { "Content-Type": "application/json", "Cache-Control": debug ? "no-store" : "public, max-age=1800" } }); if (!debug) ctx.waitUntil(cache.put(cacheKey, response.clone())); return response; } }; /* ---------------- Normalizers ---------------- */ function normalizeEvent(e, client) { const dt = parseLegistarDate(e.EventDate, e.EventTime); if (!dt) return null; return { event_id: String(e.EventId), title: e.EventBodyName || "Meeting", datetime: dt, location: e.EventLocation || "", source_url: `https://${client}.legistar.com/MeetingDetail.aspx?ID=${e.EventId}` }; } function normalizeXML(xmlText, client) { const events = []; const matches = [...xmlText.matchAll(/([\s\S]*?)<\/GranicusEvent>/g)]; for (const m of matches) { const get = tag => (m[1].match(new RegExp(`<${tag}>(.*?)<\/${tag}>`)) || [])[1] || ""; const dt = parseLegistarDate(get("EventDate"), get("EventTime")); if (!dt) continue; events.push({ event_id: get("EventId"), title: get("EventBodyName") || "Meeting", datetime: dt, location: get("EventLocation") || "", source_url: `https://${client}.legistar.com/MeetingDetail.aspx?ID=${get("EventId")}` }); } return events; } /* ---------------- Date helpers ---------------- */ function parseLegistarDate(eventDate, eventTime) { if (!eventDate) return null; const datePart = eventDate.split("T")[0]; if (!eventTime) return `${datePart}T12:00:00Z`; const m = eventTime.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); if (!m) return `${datePart}T12:00:00Z`; let hour = parseInt(m[1], 10); const minute = m[2]; const meridian = m[3].toUpperCase(); if (meridian === "PM" && hour !== 12) hour += 12; if (meridian === "AM" && hour === 12) hour = 0; return `${datePart}T${String(hour).padStart(2, "0")}:${minute}:00Z`; } function isWithinLast30DaysOrFuture(iso) { const dt = new Date(iso); if (isNaN(dt.getTime())) return false; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 30); return dt >= cutoff; }