feat(rcal): map shows routes with booking status, calendar keeps semantic zoom

Map always shows individual event markers (no clustering). Transit
lines now colored by booking status: green solid = booked, red dashed
= not yet booked. New bookingStatus field on CalendarEvent as
placeholder for the forthcoming booking pipeline. Calendar views
retain semantic zoom (country/city chips at year/season/month levels).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-08 13:28:08 -04:00
parent 988b10fd65
commit 65b72ed7ac
5 changed files with 42 additions and 111 deletions

View File

@ -106,7 +106,7 @@ import { calendarSchema, calendarDocId, type CalendarDoc } from "../schemas";
import type { DocumentId } from "../../../shared/local-first/document";
import { TourEngine } from "../../../shared/tour-engine";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { generateBreadcrumb, CONTINENT_CENTROIDS } from "../geo-hierarchy";
import { generateBreadcrumb } from "../geo-hierarchy";
// ── Component ──
@ -262,6 +262,7 @@ class FolkCalendarView extends HTMLElement {
source_name: e.sourceName, source_color: e.sourceColor,
location_name: e.locationName,
location_breadcrumb: e.locationBreadcrumb ?? null,
booking_status: e.bookingStatus ?? null,
location_lat: e.locationLat, location_lng: e.locationLng,
latitude: e.locationLat ?? null,
longitude: e.locationLng ?? null,
@ -479,7 +480,7 @@ class FolkCalendarView extends HTMLElement {
const demoEvents: {
start: Date; durationMin: number; title: string; source: number;
desc: string; location: string | null; virtual: boolean;
lat?: number; lng?: number; breadcrumb?: string; likelihood?: number;
lat?: number; lng?: number; breadcrumb?: string; likelihood?: number; booked?: boolean;
}[] = [
// ── LAST MONTH ──
{ start: rel(-1, 18, 10, 0), durationMin: 90, title: "Sprint 22 Review", source: 0, desc: "Sprint review with Berlin engineering team", location: "Factory Berlin", virtual: false, lat: 52.5030, lng: 13.3345, breadcrumb: "Earth > Europe > Germany > Berlin" },
@ -514,11 +515,11 @@ class FolkCalendarView extends HTMLElement {
{ start: rel(0, 10, 15, 30), durationMin: 60, title: "Deploy Prep", source: 0, desc: "Pre-release checklist and staging verification", location: null, virtual: true },
{ start: rel(0, 11, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
{ start: rel(0, 11, 18, 0), durationMin: 120, title: "Workshop: Intro to CRDTs (1/4)", source: 4, desc: "Community workshop series on conflict-free replicated data types", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(0, 12, 7, 15), durationMin: 390, title: "Train Berlin \u2192 Amsterdam", source: 1, desc: "ICE 643 Berlin Hbf \u2192 Amsterdam Centraal", location: "Berlin Hauptbahnhof", virtual: false, lat: 52.5251, lng: 13.3694, breadcrumb: "Earth > Europe > Germany > Berlin" },
{ start: rel(0, 12, 14, 30), durationMin: 30, title: "Hotel Check-in", source: 1, desc: "Hotel V Nesplein, Amsterdam", location: "Hotel V Nesplein", virtual: false, lat: 52.3667, lng: 4.8945, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" },
{ start: rel(0, 13, 10, 0), durationMin: 180, title: "Partner Meeting", source: 0, desc: "On-site with Amsterdam design team", location: "WeWork Weteringschans", virtual: false, lat: 52.3603, lng: 4.8880, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" },
{ start: rel(0, 12, 7, 15), durationMin: 390, title: "Train Berlin \u2192 Amsterdam", source: 1, desc: "ICE 643 Berlin Hbf \u2192 Amsterdam Centraal", location: "Berlin Hauptbahnhof", virtual: false, lat: 52.5251, lng: 13.3694, breadcrumb: "Earth > Europe > Germany > Berlin", booked: true },
{ start: rel(0, 12, 14, 30), durationMin: 30, title: "Hotel Check-in", source: 1, desc: "Hotel V Nesplein, Amsterdam", location: "Hotel V Nesplein", virtual: false, lat: 52.3667, lng: 4.8945, breadcrumb: "Earth > Europe > Netherlands > Amsterdam", booked: true },
{ start: rel(0, 13, 10, 0), durationMin: 180, title: "Partner Meeting", source: 0, desc: "On-site with Amsterdam design team", location: "WeWork Weteringschans", virtual: false, lat: 52.3603, lng: 4.8880, breadcrumb: "Earth > Europe > Netherlands > Amsterdam", booked: true },
{ start: rel(0, 13, 15, 0), durationMin: 120, title: "Canal District Walk", source: 2, desc: "Afternoon along Prinsengracht and Jordaan", location: "Prinsengracht, Amsterdam", virtual: false, lat: 52.3738, lng: 4.8820, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" },
{ start: rel(0, 14, 9, 30), durationMin: 390, title: "Return Train Amsterdam \u2192 Berlin", source: 1, desc: "ICE 148 Amsterdam \u2192 Berlin", location: "Amsterdam Centraal", virtual: false, lat: 52.3791, lng: 4.9003, breadcrumb: "Earth > Europe > Netherlands > Amsterdam" },
{ start: rel(0, 14, 9, 30), durationMin: 390, title: "Return Train Amsterdam \u2192 Berlin", source: 1, desc: "ICE 148 Amsterdam \u2192 Berlin", location: "Amsterdam Centraal", virtual: false, lat: 52.3791, lng: 4.9003, breadcrumb: "Earth > Europe > Netherlands > Amsterdam", booked: true },
{ start: rel(0, 15, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Post-travel sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" },
{ start: rel(0, 15, 19, 30), durationMin: 120, title: "Dinner with Friends", source: 2, desc: "Birthday dinner for Mia", location: "Il Casolare, Kreuzberg", virtual: false, lat: 52.4900, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
{ start: rel(0, 16, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
@ -531,11 +532,11 @@ class FolkCalendarView extends HTMLElement {
{ start: rel(0, 19, 9, 0), durationMin: 45, title: "Dentist", source: 5, desc: "Regular checkup, Dr. Weber", location: "Torstr. 140, Berlin", virtual: false, lat: 52.5308, lng: 13.3970, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(0, 20, 19, 0), durationMin: 90, title: "Book Club", source: 4, desc: '"The Mushroom at the End of the World"', location: "Shakespeare & Sons, Berlin", virtual: false, lat: 52.4925, lng: 13.4310, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
{ start: rel(0, 21, 14, 0), durationMin: 60, title: "c-base Open Tuesday", source: 4, desc: "Weekly open hackerspace session", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(0, 22, 6, 45), durationMin: 195, title: "Flight \u2192 Lisbon", source: 1, desc: "TAP TP 571 BER \u2192 LIS", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin" },
{ start: rel(0, 23, 9, 0), durationMin: 540, title: "Web Summit Day 1", source: 3, desc: "Opening keynotes, startup pavilion", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon" },
{ start: rel(0, 24, 9, 0), durationMin: 540, title: "Web Summit Day 2", source: 3, desc: "Panel: Local-First Software", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon" },
{ start: rel(0, 22, 6, 45), durationMin: 195, title: "Flight \u2192 Lisbon", source: 1, desc: "TAP TP 571 BER \u2192 LIS", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin", booked: true },
{ start: rel(0, 23, 9, 0), durationMin: 540, title: "Web Summit Day 1", source: 3, desc: "Opening keynotes, startup pavilion", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon", booked: true },
{ start: rel(0, 24, 9, 0), durationMin: 540, title: "Web Summit Day 2", source: 3, desc: "Panel: Local-First Software", location: "Altice Arena, Lisbon", virtual: false, lat: 38.7685, lng: -9.0943, breadcrumb: "Earth > Europe > Portugal > Lisbon", booked: true },
{ start: rel(0, 25, 10, 0), durationMin: 360, title: "Lisbon City Tour", source: 2, desc: "Alfama, Tram 28, Past\u00e9is de Bel\u00e9m", location: "Alfama, Lisbon", virtual: false, lat: 38.7118, lng: -9.1300, breadcrumb: "Earth > Europe > Portugal > Lisbon" },
{ start: rel(0, 25, 19, 30), durationMin: 195, title: "Flight \u2192 Berlin", source: 1, desc: "TAP TP 572 LIS \u2192 BER", location: "Lisbon Airport", virtual: false, lat: 38.7756, lng: -9.1354, breadcrumb: "Earth > Europe > Portugal > Lisbon" },
{ start: rel(0, 25, 19, 30), durationMin: 195, title: "Flight \u2192 Berlin", source: 1, desc: "TAP TP 572 LIS \u2192 BER", location: "Lisbon Airport", virtual: false, lat: 38.7756, lng: -9.1354, breadcrumb: "Earth > Europe > Portugal > Lisbon", booked: true },
{ start: rel(0, 25, 18, 0), durationMin: 120, title: "Workshop: CRDTs (3/4)", source: 4, desc: "Merging trees and documents", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(0, 26, 18, 0), durationMin: 180, title: "Hackathon \u2014 c-base", source: 4, desc: "Local-first data sync hackathon", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(0, 27, 9, 0), durationMin: 30, title: "Team Standup", source: 0, desc: "Post-travel sync", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" },
@ -545,10 +546,10 @@ class FolkCalendarView extends HTMLElement {
// ── NEXT MONTH (+1) ──
{ start: rel(1, 1, 18, 0), durationMin: 120, title: "Workshop: CRDTs (4/4)", source: 4, desc: "Final session: production patterns", location: "c-base, Berlin", virtual: false, lat: 52.5130, lng: 13.4200, breadcrumb: "Earth > Europe > Germany > Berlin > Mitte" },
{ start: rel(1, 2, 10, 0), durationMin: 120, title: "Sprint 25 Planning", source: 0, desc: "Plan next sprint", location: "Factory Berlin", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin" },
{ start: rel(1, 3, 6, 0), durationMin: 180, title: "Flight \u2192 Barcelona", source: 1, desc: "VY 1862 BER \u2192 BCN", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin" },
{ start: rel(1, 3, 14, 0), durationMin: 240, title: "Team Retreat Day 1", source: 0, desc: "Strategy offsite at Barcel\u00f3 Sants", location: "Barcel\u00f3 Sants, Barcelona", virtual: false, lat: 41.3795, lng: 2.1405, breadcrumb: "Earth > Europe > Spain > Barcelona" },
{ start: rel(1, 4, 9, 0), durationMin: 480, title: "Team Retreat Day 2", source: 0, desc: "Workshops + Sagrada Familia visit", location: "Barcelona", virtual: false, lat: 41.4036, lng: 2.1744, breadcrumb: "Earth > Europe > Spain > Barcelona" },
{ start: rel(1, 5, 15, 0), durationMin: 180, title: "Flight \u2192 Berlin", source: 1, desc: "VY 1863 BCN \u2192 BER", location: "BCN Airport", virtual: false, lat: 41.2974, lng: 2.0833, breadcrumb: "Earth > Europe > Spain > Barcelona" },
{ start: rel(1, 3, 6, 0), durationMin: 180, title: "Flight \u2192 Barcelona", source: 1, desc: "VY 1862 BER \u2192 BCN", location: "BER Airport", virtual: false, lat: 52.3667, lng: 13.5033, breadcrumb: "Earth > Europe > Germany > Berlin", booked: true },
{ start: rel(1, 3, 14, 0), durationMin: 240, title: "Team Retreat Day 1", source: 0, desc: "Strategy offsite at Barcel\u00f3 Sants", location: "Barcel\u00f3 Sants, Barcelona", virtual: false, lat: 41.3795, lng: 2.1405, breadcrumb: "Earth > Europe > Spain > Barcelona", booked: true },
{ start: rel(1, 4, 9, 0), durationMin: 480, title: "Team Retreat Day 2", source: 0, desc: "Workshops + Sagrada Familia visit", location: "Barcelona", virtual: false, lat: 41.4036, lng: 2.1744, breadcrumb: "Earth > Europe > Spain > Barcelona", booked: true },
{ start: rel(1, 5, 15, 0), durationMin: 180, title: "Flight \u2192 Berlin", source: 1, desc: "VY 1863 BCN \u2192 BER", location: "BCN Airport", virtual: false, lat: 41.2974, lng: 2.0833, breadcrumb: "Earth > Europe > Spain > Barcelona", booked: true },
{ start: rel(1, 6, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
{ start: rel(1, 8, 7, 30), durationMin: 75, title: "Morning Yoga", source: 5, desc: "Vinyasa flow", location: "Yoga Studio Kreuzberg", virtual: false, breadcrumb: "Earth > Europe > Germany > Berlin > Kreuzberg" },
{ start: rel(1, 8, 19, 0), durationMin: 120, title: "Berlin Philharmonic", source: 2, desc: "Brahms Symphony No. 4", location: "Berliner Philharmonie", virtual: false, lat: 52.5103, lng: 13.3699, breadcrumb: "Earth > Europe > Germany > Berlin > Tiergarten" },
@ -668,6 +669,7 @@ class FolkCalendarView extends HTMLElement {
latitude: e.lat,
longitude: e.lng,
likelihood: e.likelihood ?? null,
booking_status: e.booked ? "booked" : null,
};
});
@ -2772,92 +2774,8 @@ class FolkCalendarView extends HTMLElement {
this.mapMarkerLayer.clearLayers();
const located = this.getVisibleLocatedEvents();
const spatialLevel = this.getEffectiveSpatialIndex();
if (spatialLevel <= 1) {
// Planet / Continent — group by continent
this.renderAggregateMarkers(located, 1);
} else if (spatialLevel <= 3) {
// Bioregion / Country — group by country
this.renderAggregateMarkers(located, 3);
} else if (spatialLevel <= 5) {
// Region / City — group by city
this.renderAggregateMarkers(located, 5);
} else {
// Neighborhood / Address / Coordinates — individual markers
this.renderIndividualMarkers(located);
}
// Auto-fit map to visible event bounds
if (this.zoomCoupled && located.length > 0) {
if (located.length === 1) {
this.leafletMap?.flyTo([located[0].latitude, located[0].longitude], this.getEffectiveLeafletZoom(), { duration: 0.8 });
} else {
const bounds = L.latLngBounds(located.map((e: any) => [e.latitude, e.longitude]));
this.leafletMap?.flyToBounds(bounds, { padding: [40, 40], maxZoom: 16, duration: 0.8 });
}
}
}
/** Render aggregate cluster markers grouped by spatial label at the given level. */
private renderAggregateMarkers(events: any[], level: number) {
const L = (window as any).L;
if (!L) return;
// Group events by spatial label
const groups = new Map<string, any[]>();
for (const ev of events) {
const label = this.getSpatialLabel(ev.location_breadcrumb, level) || "Other";
if (!groups.has(label)) groups.set(label, []);
groups.get(label)!.push(ev);
}
for (const [label, evs] of groups) {
// Compute centroid — use stable continent centroids at level ≤ 1
let lat: number, lng: number;
if (level <= 1 && CONTINENT_CENTROIDS[label]) {
[lat, lng] = CONTINENT_CENTROIDS[label];
} else {
let sumLat = 0, sumLng = 0;
for (const e of evs) { sumLat += e.latitude; sumLng += e.longitude; }
lat = sumLat / evs.length;
lng = sumLng / evs.length;
}
// Determine dominant source color for border
const colorCounts: Record<string, number> = {};
for (const e of evs) {
const c = e.source_color || "#6366f1";
colorCounts[c] = (colorCounts[c] || 0) + 1;
}
const dominantColor = Object.entries(colorCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || "#6366f1";
const icon = L.divIcon({
className: "",
html: `<div class="map-cluster-label" style="border-color:${dominantColor}"><span>${this.esc(label)}</span><span class="map-cluster-count">${evs.length}</span></div>`,
iconSize: [0, 0],
iconAnchor: [0, 0],
});
const marker = L.marker([lat, lng], { icon });
// Popup listing events in this cluster (first 10)
const popupList = evs.slice(0, 10).map((e: any) =>
`<div style="font-size:11px;padding:2px 0;border-left:2px solid ${e.source_color || "#6366f1"};padding-left:6px">${this.esc(e.title)}<br><span style="color:#888;font-size:10px">${new Date(e.start_time).toLocaleDateString("default", { month: "short", day: "numeric" })}</span></div>`
).join("");
const overflow = evs.length > 10 ? `<div style="color:#888;font-size:10px;margin-top:4px">+${evs.length - 10} more</div>` : "";
marker.bindPopup(`<div style="font-size:13px;color:#1a1a2e"><div style="font-weight:600;margin-bottom:6px">${this.esc(label)} (${evs.length})</div>${popupList}${overflow}</div>`);
this.mapMarkerLayer.addLayer(marker);
}
}
/** Render individual event markers (original behavior). */
private renderIndividualMarkers(events: any[]) {
const L = (window as any).L;
if (!L) return;
for (const ev of events) {
for (const ev of located) {
const es = this.getEventStyles(ev);
const marker = L.circleMarker([ev.latitude, ev.longitude], {
radius: 6, color: ev.source_color || "#6366f1",
@ -2873,6 +2791,16 @@ class FolkCalendarView extends HTMLElement {
</div>`);
this.mapMarkerLayer.addLayer(marker);
}
// Auto-fit map to visible event bounds
if (this.zoomCoupled && located.length > 0) {
if (located.length === 1) {
this.leafletMap?.flyTo([located[0].latitude, located[0].longitude], this.getEffectiveLeafletZoom(), { duration: 0.8 });
} else {
const bounds = L.latLngBounds(located.map((e: any) => [e.latitude, e.longitude]));
this.leafletMap?.flyToBounds(bounds, { padding: [40, 40], maxZoom: 16, duration: 0.8 });
}
}
}
private updateTransitLines() {
@ -2880,9 +2808,6 @@ class FolkCalendarView extends HTMLElement {
if (!L || !this.transitLineLayer) return;
this.transitLineLayer.clearLayers();
// Skip transit lines at aggregate zoom levels (spatial < 6)
if (this.getEffectiveSpatialIndex() < 6) return;
const sorted = this.getVisibleLocatedEvents()
.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());
@ -2892,14 +2817,16 @@ class FolkCalendarView extends HTMLElement {
const curr = sorted[i], next = sorted[i + 1];
if (Math.abs(curr.latitude - next.latitude) < 0.01 && Math.abs(curr.longitude - next.longitude) < 0.01) continue;
const isTravel = curr.source_name === "Travel" || next.source_name === "Travel";
// Line color based on booking status: green = booked, red = unbooked/unknown
const isBooked = next.booking_status === "booked";
const color = isBooked ? "#22c55e" : "#ef4444";
const label = isBooked ? "Booked" : "Not booked";
const line = L.polyline(
[[curr.latitude, curr.longitude], [next.latitude, next.longitude]],
{ color: isTravel ? "#f97316" : "#94a3b8", weight: isTravel ? 3 : 2,
opacity: isTravel ? 0.8 : 0.4, dashArray: isTravel ? "8, 8" : "4, 8" }
{ color, weight: 3, opacity: 0.7, dashArray: isBooked ? undefined : "8, 6" }
);
line.bindTooltip(
`${this.esc(curr.location_name || curr.title)} \u2192 ${this.esc(next.location_name || next.title)}`,
`${this.esc(curr.location_name || curr.title)} \u2192 ${this.esc(next.location_name || next.title)} <span style="color:${color};font-weight:600">[${label}]</span>`,
{ sticky: true }
);
this.transitLineLayer.addLayer(line);
@ -3008,10 +2935,6 @@ class FolkCalendarView extends HTMLElement {
.map-fab { position: absolute; bottom: 16px; right: 16px; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; z-index: 100; box-shadow: var(--rs-shadow-md); transition: all 0.15s; }
.map-fab:hover { border-color: var(--rs-primary-hover); background: var(--rs-bg-surface-raised); transform: scale(1.1); }
/* ── Map Cluster Labels (semantic zoom) ── */
.map-cluster-label { background: var(--rs-bg-surface, #1e1e2e); border: 2px solid var(--rs-primary, #6366f1); border-radius: 16px; padding: 4px 10px; font-size: 12px; font-weight: 600; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,0.2); color: var(--rs-text-primary, #e0e0e0); display: inline-flex; align-items: center; gap: 4px; transform: translate(-50%, -50%); pointer-events: auto; cursor: pointer; }
.map-cluster-count { font-size: 10px; opacity: 0.7; }
/* ── Synodic (reused in overlay) ── */
.synodic-section { margin: 0 0 8px; }
.synodic-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--rs-text-muted); margin-bottom: 6px; }

View File

@ -70,7 +70,7 @@ export class CalLocalFirstClient {
const docId = calendarDocId(this.#space) as DocumentId;
this.#sync.change<CalendarDoc>(docId, `Update event ${eventId}`, (d) => {
if (!d.events[eventId]) {
d.events[eventId] = { id: eventId, title: '', description: '', startTime: 0, endTime: 0, allDay: false, timezone: null, rrule: null, status: null, visibility: null, sourceId: null, sourceName: null, sourceType: null, sourceColor: null, locationId: null, locationName: null, coordinates: null, locationGranularity: null, locationLat: null, locationLng: null, locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, rToolSource: null, rToolEntityId: null, attendees: [], attendeeCount: 0, metadata: null, createdAt: Date.now(), updatedAt: Date.now(), ...changes } as CalendarEvent;
d.events[eventId] = { id: eventId, title: '', description: '', startTime: 0, endTime: 0, allDay: false, timezone: null, rrule: null, status: null, visibility: null, sourceId: null, sourceName: null, sourceType: null, sourceColor: null, locationId: null, locationName: null, coordinates: null, locationGranularity: null, locationLat: null, locationLng: null, locationBreadcrumb: null, bookingStatus: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, rToolSource: null, rToolEntityId: null, attendees: [], attendeeCount: 0, metadata: null, createdAt: Date.now(), updatedAt: Date.now(), ...changes } as CalendarEvent;
} else {
Object.assign(d.events[eventId], changes);
d.events[eventId].updatedAt = Date.now();

View File

@ -88,6 +88,7 @@ function eventToRow(ev: CalendarEvent, sources: Record<string, CalendarSource>)
location_lng: ev.locationLng,
location_granularity: ev.locationGranularity,
location_breadcrumb: ev.locationBreadcrumb,
booking_status: ev.bookingStatus,
is_virtual: ev.isVirtual,
virtual_url: ev.virtualUrl,
virtual_platform: ev.virtualPlatform,
@ -256,6 +257,7 @@ function seedDemoIfEmpty(space: string) {
locationLat: e.locationLat ?? null,
locationLng: e.locationLng ?? null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: e.isVirtual || false,
virtualUrl: e.virtualUrl || null,
virtualPlatform: e.virtualPlatform || null,
@ -413,6 +415,7 @@ routes.post("/api/events", async (c) => {
locationLat: null,
locationLng: null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: is_virtual || false,
virtualUrl: virtual_url || null,
virtualPlatform: virtual_platform || null,
@ -544,6 +547,7 @@ routes.post("/api/import-ics", async (c) => {
locationLat: null,
locationLng: null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: false,
virtualUrl: null,
virtualPlatform: null,

View File

@ -51,6 +51,7 @@ export interface CalendarEvent {
rToolSource: string | null;
rToolEntityId: string | null;
locationBreadcrumb: string | null; // "Earth > Europe > Germany > Berlin"
bookingStatus: string | null; // "booked" | "unbooked" | null (placeholder for booking pipeline)
attendees: unknown[];
attendeeCount: number;
tags: string[] | null;

View File

@ -260,6 +260,7 @@ async function executeCalendarEvent(
locationLat: null,
locationLng: null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: false,
virtualUrl: null,
virtualPlatform: null,
@ -1087,6 +1088,7 @@ function syncReminderToCalendar(reminder: Reminder, space: string): string | nul
locationLat: null,
locationLng: null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: false,
virtualUrl: null,
virtualPlatform: null,
@ -1698,6 +1700,7 @@ async function executeWorkflowNode(
locationLat: null,
locationLng: null,
locationBreadcrumb: null,
bookingStatus: null,
isVirtual: false,
virtualUrl: null,
virtualPlatform: null,