@rubytech/create-maxy-code 0.1.346 → 0.1.348
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/docs/references/calendar-booking.md +1 -1
- package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
- package/payload/server/public/assets/{AdminLoginScreens-CpE_Nf5z.js → AdminLoginScreens-B_5wYcR-.js} +1 -1
- package/payload/server/public/assets/{AdminShell-D3N3Aq04.js → AdminShell-CMV3XY-s.js} +1 -1
- package/payload/server/public/assets/{Checkbox-DQGuYIRH.js → Checkbox-LplFUKpb.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-Bp-g80PM.js +9 -0
- package/payload/server/public/assets/{OperatorConversations-DLfQEw1r.css → OperatorConversations-C4Cx814-.css} +1 -1
- package/payload/server/public/assets/{admin-BU0dY3_T.js → admin-3QQtd7j2.js} +1 -1
- package/payload/server/public/assets/{browser-B5ytGJAE.js → browser-D5q26eZL.js} +1 -1
- package/payload/server/public/assets/calendar-Csbdj7rp.js +1 -0
- package/payload/server/public/assets/chat-Ct86qtMI.js +1 -0
- package/payload/server/public/assets/{data-BVPOzSZ7.js → data-HwT5wPmj.js} +1 -1
- package/payload/server/public/assets/{graph-D0fPx1nX.js → graph-DFQbqmi9.js} +1 -1
- package/payload/server/public/assets/{graph-labels-C4dkheQ8.js → graph-labels-kCti2CaC.js} +1 -1
- package/payload/server/public/assets/{operator-DyrF6PQi.js → operator-BlCyq4q1.js} +1 -1
- package/payload/server/public/assets/page-drg1V-yA.js +30 -0
- package/payload/server/public/assets/{public-BrIaS6m3.js → public-BVjeYfsD.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +278 -29
- package/payload/server/public/assets/OperatorConversations-g7aAQCDn.js +0 -9
- package/payload/server/public/assets/calendar-CgVgCBhg.js +0 -1
- package/payload/server/public/assets/chat-cxTK--65.js +0 -1
- package/payload/server/public/assets/page-BPIaNNW1.js +0 -30
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Chat — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/chat-
|
|
8
|
+
<script type="module" crossorigin src="/assets/chat-Ct86qtMI.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CMV3XY-s.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-Dg11L4MQ.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-uTix4PVD.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/page-drg1V-yA.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Data — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/data-
|
|
8
|
+
<script type="module" crossorigin src="/assets/data-HwT5wPmj.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/graph-labels-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/graph-labels-kCti2CaC.js">
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Graph — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/graph-
|
|
8
|
+
<script type="module" crossorigin src="/assets/graph-DFQbqmi9.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/graph-labels-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/graph-labels-kCti2CaC.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CMV3XY-s.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-LplFUKpb.js">
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/admin-
|
|
8
|
+
<script type="module" crossorigin src="/assets/admin-3QQtd7j2.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CMV3XY-s.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-Dg11L4MQ.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-LplFUKpb.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-B_5wYcR-.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
|
|
17
17
|
<link rel="stylesheet" href="/brand-defaults.css">
|
|
18
18
|
</head>
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Operator — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/operator-
|
|
8
|
+
<script type="module" crossorigin src="/assets/operator-BlCyq4q1.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CMV3XY-s.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-Dg11L4MQ.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-LplFUKpb.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-B_5wYcR-.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-uTix4PVD.js">
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/page-drg1V-yA.js">
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/public-
|
|
8
|
+
<script type="module" crossorigin src="/assets/public-BVjeYfsD.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CAM3fms7.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-Bp-g80PM.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CMV3XY-s.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-Dg11L4MQ.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-uTix4PVD.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/page-drg1V-yA.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C4Cx814-.css">
|
|
16
16
|
<link rel="stylesheet" href="/brand-defaults.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body class="public-surface">
|
package/payload/server/server.js
CHANGED
|
@@ -17580,6 +17580,102 @@ app42.post("/launch", requireAdminSession, async (c) => {
|
|
|
17580
17580
|
});
|
|
17581
17581
|
var browser_default = app42;
|
|
17582
17582
|
|
|
17583
|
+
// server/lib/calendar-ics.ts
|
|
17584
|
+
var CRLF = "\r\n";
|
|
17585
|
+
var PRODID = "-//Maxy//Calendar//EN";
|
|
17586
|
+
var MAX_LINE_OCTETS = 75;
|
|
17587
|
+
function escapeText(value) {
|
|
17588
|
+
return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
|
|
17589
|
+
}
|
|
17590
|
+
function foldLine(line) {
|
|
17591
|
+
const buf = Buffer.from(line, "utf-8");
|
|
17592
|
+
if (buf.length <= MAX_LINE_OCTETS) return line;
|
|
17593
|
+
const parts = [];
|
|
17594
|
+
let offset = 0;
|
|
17595
|
+
let first = true;
|
|
17596
|
+
while (offset < buf.length) {
|
|
17597
|
+
const maxChunk = first ? MAX_LINE_OCTETS : MAX_LINE_OCTETS - 1;
|
|
17598
|
+
let end = Math.min(offset + maxChunk, buf.length);
|
|
17599
|
+
while (end < buf.length && end > offset && (buf[end] & 192) === 128) end--;
|
|
17600
|
+
const chunk = buf.subarray(offset, end).toString("utf-8");
|
|
17601
|
+
parts.push(first ? chunk : " " + chunk);
|
|
17602
|
+
offset = end;
|
|
17603
|
+
first = false;
|
|
17604
|
+
}
|
|
17605
|
+
return parts.join(CRLF);
|
|
17606
|
+
}
|
|
17607
|
+
function toICSDateTime(iso) {
|
|
17608
|
+
const d = new Date(iso);
|
|
17609
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
17610
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
17611
|
+
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
|
|
17612
|
+
}
|
|
17613
|
+
function toICSDate(iso) {
|
|
17614
|
+
const d = new Date(iso);
|
|
17615
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
17616
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
17617
|
+
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
|
|
17618
|
+
}
|
|
17619
|
+
function attendeeLine(a) {
|
|
17620
|
+
if (!a.email) return null;
|
|
17621
|
+
const cn = a.name ? `;CN=${escapeText(a.name)}` : "";
|
|
17622
|
+
return `ATTENDEE${cn}:mailto:${a.email}`;
|
|
17623
|
+
}
|
|
17624
|
+
function serializeMeetingICS(m, opts) {
|
|
17625
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
17626
|
+
const lines = [];
|
|
17627
|
+
lines.push("BEGIN:VCALENDAR");
|
|
17628
|
+
lines.push("VERSION:2.0");
|
|
17629
|
+
lines.push(`PRODID:${PRODID}`);
|
|
17630
|
+
lines.push("CALSCALE:GREGORIAN");
|
|
17631
|
+
lines.push("METHOD:PUBLISH");
|
|
17632
|
+
lines.push("BEGIN:VEVENT");
|
|
17633
|
+
lines.push(`UID:${m.uid ?? m.id}`);
|
|
17634
|
+
lines.push(`DTSTAMP:${toICSDateTime(now.toISOString())}`);
|
|
17635
|
+
if (m.isAllDay) {
|
|
17636
|
+
lines.push(`DTSTART;VALUE=DATE:${toICSDate(m.startsAt)}`);
|
|
17637
|
+
if (m.endsAt) lines.push(`DTEND;VALUE=DATE:${toICSDate(m.endsAt)}`);
|
|
17638
|
+
} else {
|
|
17639
|
+
lines.push(`DTSTART:${toICSDateTime(m.startsAt)}`);
|
|
17640
|
+
if (m.endsAt) lines.push(`DTEND:${toICSDateTime(m.endsAt)}`);
|
|
17641
|
+
}
|
|
17642
|
+
lines.push(`SUMMARY:${escapeText(m.title ?? "")}`);
|
|
17643
|
+
if (m.location) lines.push(`LOCATION:${escapeText(m.location)}`);
|
|
17644
|
+
if (opts.includeAttendees) {
|
|
17645
|
+
for (const a of m.attendees) {
|
|
17646
|
+
const line = attendeeLine(a);
|
|
17647
|
+
if (line) lines.push(line);
|
|
17648
|
+
}
|
|
17649
|
+
}
|
|
17650
|
+
lines.push("END:VEVENT");
|
|
17651
|
+
lines.push("END:VCALENDAR");
|
|
17652
|
+
return lines.map(foldLine).join(CRLF) + CRLF;
|
|
17653
|
+
}
|
|
17654
|
+
function countVEvents(ics) {
|
|
17655
|
+
const unfolded = ics.replace(/\r?\n[ \t]/g, "");
|
|
17656
|
+
const lines = unfolded.split(/\r?\n/);
|
|
17657
|
+
let count = 0;
|
|
17658
|
+
let inEvent = false;
|
|
17659
|
+
let nested = 0;
|
|
17660
|
+
for (const raw of lines) {
|
|
17661
|
+
const line = raw.trim();
|
|
17662
|
+
if (line === "BEGIN:VEVENT") {
|
|
17663
|
+
inEvent = true;
|
|
17664
|
+
nested = 0;
|
|
17665
|
+
continue;
|
|
17666
|
+
}
|
|
17667
|
+
if (!inEvent) continue;
|
|
17668
|
+
if (line === "END:VEVENT") {
|
|
17669
|
+
inEvent = false;
|
|
17670
|
+
count++;
|
|
17671
|
+
continue;
|
|
17672
|
+
}
|
|
17673
|
+
if (line.startsWith("BEGIN:")) nested++;
|
|
17674
|
+
else if (line.startsWith("END:") && nested > 0) nested--;
|
|
17675
|
+
}
|
|
17676
|
+
return count;
|
|
17677
|
+
}
|
|
17678
|
+
|
|
17583
17679
|
// server/routes/admin/calendar.ts
|
|
17584
17680
|
var app43 = new Hono();
|
|
17585
17681
|
function normalizeAttendees(raw) {
|
|
@@ -17592,12 +17688,26 @@ function normalizeAttendees(raw) {
|
|
|
17592
17688
|
};
|
|
17593
17689
|
});
|
|
17594
17690
|
}
|
|
17595
|
-
var EDITABLE_FIELDS = ["title", "startsAt", "endsAt", "location"];
|
|
17596
|
-
var
|
|
17691
|
+
var EDITABLE_FIELDS = ["title", "startsAt", "endsAt", "location", "privateNote"];
|
|
17692
|
+
var MEETING_COLS = `elementId(m) AS id, m.title AS title,
|
|
17597
17693
|
toString(m.startsAt) AS startsAt, toString(m.endsAt) AS endsAt,
|
|
17598
17694
|
m.location AS location, coalesce(m.isAllDay, false) AS isAllDay,
|
|
17599
|
-
'meeting' AS kind, m.uid AS uid,
|
|
17600
|
-
[(
|
|
17695
|
+
'meeting' AS kind, m.uid AS uid, m.privateNote AS privateNote,
|
|
17696
|
+
[(att:Person)-[:ATTENDED]->(m) | {name: coalesce(att.displayName, att.email), email: att.email}] AS attendees`;
|
|
17697
|
+
var MEETING_RETURN = `RETURN ${MEETING_COLS}`;
|
|
17698
|
+
function mapMeeting(r) {
|
|
17699
|
+
return {
|
|
17700
|
+
meetingId: r.get("id"),
|
|
17701
|
+
title: r.get("title"),
|
|
17702
|
+
startsAt: r.get("startsAt"),
|
|
17703
|
+
endsAt: r.get("endsAt"),
|
|
17704
|
+
location: r.get("location"),
|
|
17705
|
+
isAllDay: r.get("isAllDay"),
|
|
17706
|
+
kind: r.get("kind"),
|
|
17707
|
+
attendees: normalizeAttendees(r.get("attendees")),
|
|
17708
|
+
privateNote: r.get("privateNote")
|
|
17709
|
+
};
|
|
17710
|
+
}
|
|
17601
17711
|
app43.get("/meetings", requireAdminSession, async (c) => {
|
|
17602
17712
|
const cacheKey = c.var.cacheKey;
|
|
17603
17713
|
const accountId = getAccountIdForSession(cacheKey);
|
|
@@ -17617,7 +17727,7 @@ app43.get("/meetings", requireAdminSession, async (c) => {
|
|
|
17617
17727
|
RETURN elementId(m) AS id, m.title AS title,
|
|
17618
17728
|
toString(m.startsAt) AS startsAt, toString(m.endsAt) AS endsAt,
|
|
17619
17729
|
m.location AS location, coalesce(m.isAllDay, false) AS isAllDay,
|
|
17620
|
-
'meeting' AS kind,
|
|
17730
|
+
'meeting' AS kind, m.privateNote AS privateNote,
|
|
17621
17731
|
[(p:Person)-[:ATTENDED]->(m) | {name: coalesce(p.displayName, p.email), email: p.email}] AS attendees
|
|
17622
17732
|
UNION
|
|
17623
17733
|
MATCH (e:Event {accountId: $accountId})
|
|
@@ -17628,7 +17738,7 @@ app43.get("/meetings", requireAdminSession, async (c) => {
|
|
|
17628
17738
|
toString(datetime(e.startDate)) AS startsAt,
|
|
17629
17739
|
CASE WHEN e.endDate IS NULL THEN null ELSE toString(datetime(e.endDate)) END AS endsAt,
|
|
17630
17740
|
e.location AS location, false AS isAllDay,
|
|
17631
|
-
'event' AS kind,
|
|
17741
|
+
'event' AS kind, null AS privateNote,
|
|
17632
17742
|
[] AS attendees
|
|
17633
17743
|
UNION
|
|
17634
17744
|
MATCH (t:Task {accountId: $accountId})
|
|
@@ -17638,20 +17748,11 @@ app43.get("/meetings", requireAdminSession, async (c) => {
|
|
|
17638
17748
|
RETURN elementId(t) AS id, t.name AS title,
|
|
17639
17749
|
toString(datetime(t.dueDate)) AS startsAt, null AS endsAt,
|
|
17640
17750
|
null AS location, true AS isAllDay,
|
|
17641
|
-
'task' AS kind,
|
|
17751
|
+
'task' AS kind, null AS privateNote,
|
|
17642
17752
|
[] AS attendees`,
|
|
17643
17753
|
{ accountId, from, to }
|
|
17644
17754
|
);
|
|
17645
|
-
const meetings = res.records.map((
|
|
17646
|
-
meetingId: r.get("id"),
|
|
17647
|
-
title: r.get("title"),
|
|
17648
|
-
startsAt: r.get("startsAt"),
|
|
17649
|
-
endsAt: r.get("endsAt"),
|
|
17650
|
-
location: r.get("location"),
|
|
17651
|
-
isAllDay: r.get("isAllDay"),
|
|
17652
|
-
kind: r.get("kind"),
|
|
17653
|
-
attendees: normalizeAttendees(r.get("attendees"))
|
|
17654
|
-
})).sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
|
|
17755
|
+
const meetings = res.records.map(mapMeeting).sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
|
|
17655
17756
|
const counts = { meeting: 0, event: 0, task: 0 };
|
|
17656
17757
|
for (const m of meetings) counts[m.kind]++;
|
|
17657
17758
|
console.log(
|
|
@@ -17666,6 +17767,68 @@ app43.get("/meetings", requireAdminSession, async (c) => {
|
|
|
17666
17767
|
await session.close();
|
|
17667
17768
|
}
|
|
17668
17769
|
});
|
|
17770
|
+
function icsFilename(title) {
|
|
17771
|
+
const slug = (title ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
17772
|
+
return `${slug || "meeting"}.ics`;
|
|
17773
|
+
}
|
|
17774
|
+
app43.get("/meetings/:id/ics", requireAdminSession, async (c) => {
|
|
17775
|
+
const cacheKey = c.var.cacheKey;
|
|
17776
|
+
const accountId = getAccountIdForSession(cacheKey);
|
|
17777
|
+
if (!accountId) {
|
|
17778
|
+
console.error('[calendar] op=ics auth-rejected reason="no account for session"');
|
|
17779
|
+
return c.json({ error: "Account not found for session" }, 401);
|
|
17780
|
+
}
|
|
17781
|
+
const id = c.req.param("id");
|
|
17782
|
+
const includeAttendees = c.req.query("attendees") === "1";
|
|
17783
|
+
console.log(`[calendar] op=ics-request id=${id} attendees=${includeAttendees}`);
|
|
17784
|
+
const session = getSession();
|
|
17785
|
+
try {
|
|
17786
|
+
const res = await session.run(
|
|
17787
|
+
`MATCH (m:Meeting {accountId: $accountId})
|
|
17788
|
+
WHERE elementId(m) = $id
|
|
17789
|
+
${MEETING_RETURN}`,
|
|
17790
|
+
{ accountId, id }
|
|
17791
|
+
);
|
|
17792
|
+
if (res.records.length === 0) {
|
|
17793
|
+
console.warn(`[calendar] op=ics-404 id=${id} reason="no meeting for account"`);
|
|
17794
|
+
return c.json({ error: "Meeting not found for this account" }, 404);
|
|
17795
|
+
}
|
|
17796
|
+
const r = res.records[0];
|
|
17797
|
+
const mapped = mapMeeting(r);
|
|
17798
|
+
const uid = r.get("uid") ?? null;
|
|
17799
|
+
const corr = uid ?? mapped.meetingId;
|
|
17800
|
+
const input = {
|
|
17801
|
+
uid,
|
|
17802
|
+
id: mapped.meetingId,
|
|
17803
|
+
title: mapped.title,
|
|
17804
|
+
startsAt: mapped.startsAt,
|
|
17805
|
+
endsAt: mapped.endsAt,
|
|
17806
|
+
location: mapped.location,
|
|
17807
|
+
isAllDay: mapped.isAllDay,
|
|
17808
|
+
attendees: mapped.attendees
|
|
17809
|
+
};
|
|
17810
|
+
console.log(`[calendar] op=ics-resolved id=${corr} allDay=${input.isAllDay} attendees=${input.attendees.length}`);
|
|
17811
|
+
const ics = serializeMeetingICS(input, { includeAttendees });
|
|
17812
|
+
const bytes = Buffer.byteLength(ics, "utf-8");
|
|
17813
|
+
const vevents = countVEvents(ics);
|
|
17814
|
+
console.log(`[calendar] op=ics-serialized id=${corr} bytes=${bytes} vevents=${vevents}`);
|
|
17815
|
+
if (vevents !== 1 || bytes === 0) {
|
|
17816
|
+
console.error(`[calendar] op=ics-malformed id=${corr} vevents=${vevents} bytes=${bytes}`);
|
|
17817
|
+
return c.json({ error: "Failed to produce a valid calendar file" }, 500);
|
|
17818
|
+
}
|
|
17819
|
+
console.log(`[calendar] op=ics-export id=${corr} bytes=${bytes} attendees=${includeAttendees}`);
|
|
17820
|
+
return c.body(ics, 200, {
|
|
17821
|
+
"content-type": "text/calendar; charset=utf-8",
|
|
17822
|
+
"content-disposition": `attachment; filename="${icsFilename(mapped.title)}"`
|
|
17823
|
+
});
|
|
17824
|
+
} catch (err) {
|
|
17825
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17826
|
+
console.error(`[calendar] op=ics-fail id=${id} err="${message}"`);
|
|
17827
|
+
return c.json({ error: `ICS export failed: ${message}` }, 503);
|
|
17828
|
+
} finally {
|
|
17829
|
+
await session.close();
|
|
17830
|
+
}
|
|
17831
|
+
});
|
|
17669
17832
|
app43.patch("/meetings/:id", requireAdminSession, async (c) => {
|
|
17670
17833
|
const cacheKey = c.var.cacheKey;
|
|
17671
17834
|
const accountId = getAccountIdForSession(cacheKey);
|
|
@@ -17677,7 +17840,7 @@ app43.patch("/meetings/:id", requireAdminSession, async (c) => {
|
|
|
17677
17840
|
const body = await safeJson(c) ?? {};
|
|
17678
17841
|
const fields = EDITABLE_FIELDS.filter((f) => f in body);
|
|
17679
17842
|
if (fields.length === 0) {
|
|
17680
|
-
return c.json({ error: "No editable field supplied (title, startsAt, endsAt, location)" }, 400);
|
|
17843
|
+
return c.json({ error: "No editable field supplied (title, startsAt, endsAt, location, privateNote)" }, 400);
|
|
17681
17844
|
}
|
|
17682
17845
|
const setClauses = fields.map(
|
|
17683
17846
|
(f) => f === "startsAt" || f === "endsAt" ? `m.${f} = datetime($${f})` : `m.${f} = $${f}`
|
|
@@ -17701,17 +17864,7 @@ app43.patch("/meetings/:id", requireAdminSession, async (c) => {
|
|
|
17701
17864
|
const r = res.records[0];
|
|
17702
17865
|
const uid = r.get("uid");
|
|
17703
17866
|
console.log(`[calendar-edit] account=${accountId.slice(0, 8)} uid=${uid ?? "absent"} fields=${fields.join(",")}`);
|
|
17704
|
-
|
|
17705
|
-
meetingId: r.get("id"),
|
|
17706
|
-
title: r.get("title"),
|
|
17707
|
-
startsAt: r.get("startsAt"),
|
|
17708
|
-
endsAt: r.get("endsAt"),
|
|
17709
|
-
location: r.get("location"),
|
|
17710
|
-
isAllDay: r.get("isAllDay"),
|
|
17711
|
-
kind: "meeting",
|
|
17712
|
-
attendees: normalizeAttendees(r.get("attendees"))
|
|
17713
|
-
};
|
|
17714
|
-
return c.json({ meeting });
|
|
17867
|
+
return c.json({ meeting: mapMeeting(r) });
|
|
17715
17868
|
} catch (err) {
|
|
17716
17869
|
const message = err instanceof Error ? err.message : String(err);
|
|
17717
17870
|
console.error(`[calendar-edit] op=fail err="${message}"`);
|
|
@@ -17720,6 +17873,102 @@ app43.patch("/meetings/:id", requireAdminSession, async (c) => {
|
|
|
17720
17873
|
await session.close();
|
|
17721
17874
|
}
|
|
17722
17875
|
});
|
|
17876
|
+
app43.post("/meetings/:id/attendees", requireAdminSession, async (c) => {
|
|
17877
|
+
const cacheKey = c.var.cacheKey;
|
|
17878
|
+
const accountId = getAccountIdForSession(cacheKey);
|
|
17879
|
+
if (!accountId) {
|
|
17880
|
+
console.error('[calendar] op=attendee-add auth-rejected reason="no account for session"');
|
|
17881
|
+
return c.json({ error: "Account not found for session" }, 401);
|
|
17882
|
+
}
|
|
17883
|
+
const id = c.req.param("id");
|
|
17884
|
+
const body = await safeJson(c) ?? {};
|
|
17885
|
+
const email = typeof body.email === "string" && body.email.trim() ? body.email.trim() : null;
|
|
17886
|
+
const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : null;
|
|
17887
|
+
if (!email && !name) {
|
|
17888
|
+
return c.json({ error: "email or name required" }, 400);
|
|
17889
|
+
}
|
|
17890
|
+
const cypher = email ? `MATCH (m:Meeting {accountId: $accountId})
|
|
17891
|
+
WHERE elementId(m) = $id
|
|
17892
|
+
OPTIONAL MATCH (existing:Person {accountId: $accountId, email: $email})
|
|
17893
|
+
WITH m, existing IS NULL AS personCreated
|
|
17894
|
+
MERGE (p:Person {accountId: $accountId, email: $email})
|
|
17895
|
+
ON CREATE SET p.displayName = $name
|
|
17896
|
+
MERGE (p)-[:ATTENDED]->(m)
|
|
17897
|
+
WITH m, personCreated
|
|
17898
|
+
RETURN ${MEETING_COLS}, personCreated AS personCreated` : `MATCH (m:Meeting {accountId: $accountId})
|
|
17899
|
+
WHERE elementId(m) = $id
|
|
17900
|
+
CREATE (p:Person {accountId: $accountId, displayName: $name})
|
|
17901
|
+
MERGE (p)-[:ATTENDED]->(m)
|
|
17902
|
+
WITH m, true AS personCreated
|
|
17903
|
+
RETURN ${MEETING_COLS}, personCreated AS personCreated`;
|
|
17904
|
+
const session = getSession();
|
|
17905
|
+
try {
|
|
17906
|
+
const res = await session.run(cypher, { accountId, id, email, name });
|
|
17907
|
+
if (res.records.length === 0) {
|
|
17908
|
+
console.warn(`[calendar-attendee] op=add account=${accountId.slice(0, 8)} uid=absent email=${email ?? "none"} found=0`);
|
|
17909
|
+
return c.json({ error: "Meeting not found for this account" }, 404);
|
|
17910
|
+
}
|
|
17911
|
+
const r = res.records[0];
|
|
17912
|
+
const uid = r.get("uid");
|
|
17913
|
+
const personCreated = r.get("personCreated");
|
|
17914
|
+
console.log(
|
|
17915
|
+
`[calendar-attendee] op=add account=${accountId.slice(0, 8)} uid=${uid ?? "absent"} email=${email ?? "none"} personCreated=${personCreated}`
|
|
17916
|
+
);
|
|
17917
|
+
return c.json({ meeting: mapMeeting(r) });
|
|
17918
|
+
} catch (err) {
|
|
17919
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17920
|
+
console.error(`[calendar-attendee] op=add-fail err="${message}"`);
|
|
17921
|
+
return c.json({ error: `Attendee add failed: ${message}` }, 503);
|
|
17922
|
+
} finally {
|
|
17923
|
+
await session.close();
|
|
17924
|
+
}
|
|
17925
|
+
});
|
|
17926
|
+
app43.delete("/meetings/:id/attendees", requireAdminSession, async (c) => {
|
|
17927
|
+
const cacheKey = c.var.cacheKey;
|
|
17928
|
+
const accountId = getAccountIdForSession(cacheKey);
|
|
17929
|
+
if (!accountId) {
|
|
17930
|
+
console.error('[calendar] op=attendee-remove auth-rejected reason="no account for session"');
|
|
17931
|
+
return c.json({ error: "Account not found for session" }, 401);
|
|
17932
|
+
}
|
|
17933
|
+
const id = c.req.param("id");
|
|
17934
|
+
const body = await safeJson(c) ?? {};
|
|
17935
|
+
const email = typeof body.email === "string" && body.email.trim() ? body.email.trim() : null;
|
|
17936
|
+
const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : null;
|
|
17937
|
+
if (!email && !name) {
|
|
17938
|
+
return c.json({ error: "email or name required" }, 400);
|
|
17939
|
+
}
|
|
17940
|
+
const keyPredicate = email ? "att.email = $email" : "att.displayName = $name";
|
|
17941
|
+
const cypher = `MATCH (m:Meeting {accountId: $accountId})
|
|
17942
|
+
WHERE elementId(m) = $id
|
|
17943
|
+
OPTIONAL MATCH (att:Person {accountId: $accountId})-[r:ATTENDED]->(m)
|
|
17944
|
+
WHERE ${keyPredicate}
|
|
17945
|
+
DELETE r
|
|
17946
|
+
WITH m, count(att) AS removed, collect(att) AS survivors
|
|
17947
|
+
RETURN ${MEETING_COLS}, removed AS removed,
|
|
17948
|
+
all(s IN survivors WHERE s IS NOT NULL) AS personSurvived`;
|
|
17949
|
+
const session = getSession();
|
|
17950
|
+
try {
|
|
17951
|
+
const res = await session.run(cypher, { accountId, id, email, name });
|
|
17952
|
+
if (res.records.length === 0) {
|
|
17953
|
+
console.warn(`[calendar-attendee] op=remove account=${accountId.slice(0, 8)} uid=absent key=${email ?? name} found=0`);
|
|
17954
|
+
return c.json({ error: "Meeting not found for this account" }, 404);
|
|
17955
|
+
}
|
|
17956
|
+
const r = res.records[0];
|
|
17957
|
+
const uid = r.get("uid");
|
|
17958
|
+
const removed = Number(r.get("removed"));
|
|
17959
|
+
const personSurvived = r.get("personSurvived");
|
|
17960
|
+
console.log(
|
|
17961
|
+
`[calendar-attendee] op=remove account=${accountId.slice(0, 8)} uid=${uid ?? "absent"} key=${email ?? name} removed=${removed} personSurvived=${personSurvived}`
|
|
17962
|
+
);
|
|
17963
|
+
return c.json({ meeting: mapMeeting(r), removed });
|
|
17964
|
+
} catch (err) {
|
|
17965
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17966
|
+
console.error(`[calendar-attendee] op=remove-fail err="${message}"`);
|
|
17967
|
+
return c.json({ error: `Attendee remove failed: ${message}` }, 503);
|
|
17968
|
+
} finally {
|
|
17969
|
+
await session.close();
|
|
17970
|
+
}
|
|
17971
|
+
});
|
|
17723
17972
|
app43.delete("/meetings/:id", requireAdminSession, async (c) => {
|
|
17724
17973
|
const cacheKey = c.var.cacheKey;
|
|
17725
17974
|
const accountId = getAccountIdForSession(cacheKey);
|