@bonginkan/maria-lite 6.3.0 → 6.3.1
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/README.md +18 -2
- package/dist/cli.cjs +2170 -90
- package/dist/desktop-client.js +924 -11
- package/dist/ext.cjs +1 -1
- package/dist/ext.d.cts +1 -1
- package/origin/index.meta.json +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -45614,7 +45614,7 @@ var require_src8 = __commonJS({
|
|
|
45614
45614
|
*/
|
|
45615
45615
|
}, {
|
|
45616
45616
|
key: "getToken",
|
|
45617
|
-
value: function
|
|
45617
|
+
value: function getToken3(callback) {
|
|
45618
45618
|
var opts = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
|
|
45619
45619
|
if (_typeof(callback) === "object") {
|
|
45620
45620
|
opts = callback;
|
|
@@ -97578,7 +97578,7 @@ function resolvePackageJsonNearEntrypoint() {
|
|
|
97578
97578
|
}
|
|
97579
97579
|
function resolveMariaLiteVersionInfo() {
|
|
97580
97580
|
const fallbackName = EXPECTED_PKG_NAME;
|
|
97581
|
-
const fallbackVersion = String(process.env.MARIA_LITE_VERSION || "").trim() || "6.3.
|
|
97581
|
+
const fallbackVersion = String(process.env.MARIA_LITE_VERSION || "").trim() || "6.3.1";
|
|
97582
97582
|
const near = resolvePackageJsonNearEntrypoint();
|
|
97583
97583
|
if (near) {
|
|
97584
97584
|
const name = fallbackName;
|
|
@@ -122583,7 +122583,7 @@ async function handleToolCalls(params) {
|
|
|
122583
122583
|
...written.map((w) => `- ${w.rel}`)
|
|
122584
122584
|
].join("\n");
|
|
122585
122585
|
await params.ctxStore.appendCommon({ actorId: params.actorId, text: header });
|
|
122586
|
-
const
|
|
122586
|
+
const clamp2 = (s2, max) => {
|
|
122587
122587
|
const t2 = String(s2 || "");
|
|
122588
122588
|
if (t2.length <= max) return t2;
|
|
122589
122589
|
return `${t2.slice(0, max)}
|
|
@@ -122599,15 +122599,15 @@ async function handleToolCalls(params) {
|
|
|
122599
122599
|
if (fileRel) lines.push(`file=${fileRel}`);
|
|
122600
122600
|
lines.push("");
|
|
122601
122601
|
lines.push("description:");
|
|
122602
|
-
lines.push(
|
|
122602
|
+
lines.push(clamp2(p.description || "", 2e3));
|
|
122603
122603
|
lines.push("");
|
|
122604
122604
|
lines.push("entries:");
|
|
122605
122605
|
for (const e2 of p.entries.slice(0, 20)) {
|
|
122606
122606
|
lines.push(`- id=${e2.id} kind=${e2.kind} importance=${e2.importance} title=${e2.title}`);
|
|
122607
122607
|
if (e2.tags && e2.tags.length) lines.push(` tags=${e2.tags.slice().sort((a, b) => a.localeCompare(b)).join(",")}`);
|
|
122608
|
-
if (e2.summary) lines.push(` summary=${
|
|
122608
|
+
if (e2.summary) lines.push(` summary=${clamp2(e2.summary, 800).replaceAll("\n", " ")}`);
|
|
122609
122609
|
if (e2.body) lines.push(` body=
|
|
122610
|
-
${
|
|
122610
|
+
${clamp2(e2.body, 2200)}`);
|
|
122611
122611
|
}
|
|
122612
122612
|
await params.ctxStore.appendCommon({ actorId: params.actorId, text: lines.join("\n") });
|
|
122613
122613
|
}
|
|
@@ -127245,7 +127245,7 @@ function categorizeCommand(id) {
|
|
|
127245
127245
|
const dataSet = /* @__PURE__ */ new Set(["kp-sync", "kp-generate", "kp-update", "kp-synthesis", "import-origin", "reorganize", "origin-fetch"]);
|
|
127246
127246
|
const creativeSet = /* @__PURE__ */ new Set(["blog", "slides", "docs", "proposal", "pdf", "manga", "novel", "film", "spreadsheet"]);
|
|
127247
127247
|
const academicSet = /* @__PURE__ */ new Set(["paper", "exam"]);
|
|
127248
|
-
const googleSet = /* @__PURE__ */ new Set(["gmail", "gcal", "gdrive", "gmeet"]);
|
|
127248
|
+
const googleSet = /* @__PURE__ */ new Set(["gmail", "gcal", "gdrive", "gmeet", "resource-manager"]);
|
|
127249
127249
|
const phoneSet = /* @__PURE__ */ new Set(["phone-tenant", "phone-dept", "phone-prompt", "phone-dict", "phone-hp-import", "phone-deploy", "phone-init"]);
|
|
127250
127250
|
const systemSet = /* @__PURE__ */ new Set(["help", "version", "gui", "desktop", "login", "logout", "account", "vup", "schedule", "task-manager", "log-viewer", "google-connect"]);
|
|
127251
127251
|
if (dailySet.has(id)) return "daily";
|
|
@@ -127368,7 +127368,8 @@ function commandIcon(id) {
|
|
|
127368
127368
|
"competitors": "\u{1F3C6}",
|
|
127369
127369
|
"task-distribution": "\u{1F4CB}",
|
|
127370
127370
|
"billing-pl": "\u{1F4B0}",
|
|
127371
|
-
"dev-adviser": "\u{1F468}\u200D\u{1F4BB}"
|
|
127371
|
+
"dev-adviser": "\u{1F468}\u200D\u{1F4BB}",
|
|
127372
|
+
"resource-manager": "\u{1F3E2}"
|
|
127372
127373
|
};
|
|
127373
127374
|
return icons[id] || "\u{1F4CE}";
|
|
127374
127375
|
}
|
|
@@ -192002,7 +192003,7 @@ function normalizeImageCli(ctx) {
|
|
|
192002
192003
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
192003
192004
|
const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
|
|
192004
192005
|
const apply = planOnly ? false : applyExplicit || true;
|
|
192005
|
-
const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "
|
|
192006
|
+
const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/images";
|
|
192006
192007
|
const outPathRel = normalizeRelOutPath(ctx.parsed.options.path, `.${format2}`) || void 0;
|
|
192007
192008
|
const concurrency = ctx.parsed.options.concurrency ? Math.max(1, Math.min(16, Math.floor(Number(ctx.parsed.options.concurrency)))) : void 0;
|
|
192008
192009
|
const retry = ctx.parsed.options.retry ? Math.max(0, Math.min(10, Math.floor(Number(ctx.parsed.options.retry)))) : void 0;
|
|
@@ -192335,7 +192336,7 @@ function normalizeVideoCli(ctx) {
|
|
|
192335
192336
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
192336
192337
|
const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
|
|
192337
192338
|
const apply = planOnly ? false : applyExplicit || true;
|
|
192338
|
-
const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "
|
|
192339
|
+
const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/videos";
|
|
192339
192340
|
const outPathRel = normalizeRelOutPath2(ctx.parsed.options.path, `.${format2}`) || void 0;
|
|
192340
192341
|
const concurrency = ctx.parsed.options.concurrency ? Math.max(1, Math.min(16, Math.floor(Number(ctx.parsed.options.concurrency)))) : void 0;
|
|
192341
192342
|
const retry = ctx.parsed.options.retry ? Math.max(0, Math.min(10, Math.floor(Number(ctx.parsed.options.retry)))) : void 0;
|
|
@@ -193314,8 +193315,8 @@ function parseSlidesTextShiftOpt(raw) {
|
|
|
193314
193315
|
if (parts.length !== 4) return null;
|
|
193315
193316
|
const nums = parts.map((p) => Number(p));
|
|
193316
193317
|
if (!nums.every((n) => Number.isFinite(n))) return null;
|
|
193317
|
-
const
|
|
193318
|
-
const [up, right, down, left] = nums.map((n) =>
|
|
193318
|
+
const clamp2 = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
|
|
193319
|
+
const [up, right, down, left] = nums.map((n) => clamp2(Number(n)));
|
|
193319
193320
|
return { up, right, down, left };
|
|
193320
193321
|
}
|
|
193321
193322
|
function slidesTextShiftToDxDy(shift) {
|
|
@@ -193404,7 +193405,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
|
|
|
193404
193405
|
if (!s1Bullets) return { ok: false, error: "DECK_TITLE_SLIDE_BULLETS_INVALID" };
|
|
193405
193406
|
if (s1Bullets.length !== 0) return { ok: false, error: "DECK_TITLE_SLIDE_BULLETS_NOT_EMPTY" };
|
|
193406
193407
|
}
|
|
193407
|
-
const
|
|
193408
|
+
const clamp2 = (s2, maxLen) => {
|
|
193408
193409
|
const t2 = String(s2 || "").trim();
|
|
193409
193410
|
if (t2.length <= maxLen) return t2;
|
|
193410
193411
|
return t2.slice(0, Math.max(0, maxLen - 1)).trimEnd() + "\u2026";
|
|
@@ -193418,12 +193419,12 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
|
|
|
193418
193419
|
for (const s2 of slides) {
|
|
193419
193420
|
if (!isRecord(s2)) return { ok: false, error: "DECK_SLIDE_NOT_OBJECT" };
|
|
193420
193421
|
const st0 = typeof s2.title === "string" ? s2.title.trim() : "";
|
|
193421
|
-
const st =
|
|
193422
|
+
const st = clamp2(st0, 48);
|
|
193422
193423
|
if (!st) return { ok: false, error: "DECK_SLIDE_TITLE_EMPTY" };
|
|
193423
193424
|
if (hasTerminalPunct(st)) return { ok: false, error: "DECK_SLIDE_TITLE_SENTENCE_LIKE_FORBIDDEN" };
|
|
193424
193425
|
s2.title = st;
|
|
193425
193426
|
const sub0 = typeof s2.subtitle === "string" ? String(s2.subtitle).trim() : "";
|
|
193426
|
-
const sub = sub0 ?
|
|
193427
|
+
const sub = sub0 ? clamp2(sub0, 80) : "";
|
|
193427
193428
|
const kind0 = typeof s2.kind === "string" ? String(s2.kind).trim().toLowerCase() : "";
|
|
193428
193429
|
const kind = kind0 === "title" || kind0 === "agenda" || kind0 === "section" || kind0 === "content" || kind0 === "appendix" ? kind0 : "";
|
|
193429
193430
|
if (kind) s2.kind = kind;
|
|
@@ -193459,7 +193460,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
|
|
|
193459
193460
|
const name = srec && typeof srec.name === "string" ? String(srec.name).trim().slice(0, 40) : "";
|
|
193460
193461
|
const values = srec && Array.isArray(srec.values) ? srec.values.map((v) => typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" ? Number(v) : NaN).filter((v) => Number.isFinite(v)).slice(0, 6) : [];
|
|
193461
193462
|
const caption0 = typeof c.caption === "string" ? String(c.caption).trim() : "";
|
|
193462
|
-
const caption = caption0 ?
|
|
193463
|
+
const caption = caption0 ? clamp2(caption0, 140) : "";
|
|
193463
193464
|
const okChart = type2 === "bar" && categories.length >= 2 && values.length === categories.length;
|
|
193464
193465
|
if (!okChart) {
|
|
193465
193466
|
s2.chart = void 0;
|
|
@@ -193483,7 +193484,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
|
|
|
193483
193484
|
const headers = Array.isArray(headersRaw) ? headersRaw.map((x2) => typeof x2 === "string" ? x2.trim() : "").filter(Boolean) : [];
|
|
193484
193485
|
const rows = Array.isArray(rowsRaw) ? rowsRaw.filter((r2) => Array.isArray(r2)).map((r2) => r2.map((c) => typeof c === "string" ? c.trim() : "").map((c) => c.slice(0, 80))) : [];
|
|
193485
193486
|
const caption0 = typeof t0.caption === "string" ? String(t0.caption).trim() : "";
|
|
193486
|
-
const caption = caption0 ?
|
|
193487
|
+
const caption = caption0 ? clamp2(caption0, 140) : "";
|
|
193487
193488
|
const isEmptyTable = headers.length === 0 && rows.length === 0 && !caption;
|
|
193488
193489
|
if (isEmptyTable) {
|
|
193489
193490
|
s2.table = void 0;
|
|
@@ -193623,10 +193624,10 @@ function pickFigureCanvas(params) {
|
|
|
193623
193624
|
const hint = typeof params.hint === "string" ? params.hint.trim() : "";
|
|
193624
193625
|
const baseW = params.slideCanvas.width;
|
|
193625
193626
|
const baseH = params.slideCanvas.height;
|
|
193626
|
-
const
|
|
193627
|
-
if (hint === "tall") return { width:
|
|
193628
|
-
if (hint === "square") return { width:
|
|
193629
|
-
return { width:
|
|
193627
|
+
const clamp2 = (x2) => Math.max(512, Math.min(2048, Math.floor(x2)));
|
|
193628
|
+
if (hint === "tall") return { width: clamp2(baseW * 0.38), height: clamp2(baseH * 0.72) };
|
|
193629
|
+
if (hint === "square") return { width: clamp2(baseW * 0.5), height: clamp2(baseH * 0.5) };
|
|
193630
|
+
return { width: clamp2(baseW * 0.62), height: clamp2(baseH * 0.42) };
|
|
193630
193631
|
}
|
|
193631
193632
|
async function saveImageFromApiResult(params) {
|
|
193632
193633
|
return await saveImageFromApiResultShared({
|
|
@@ -194534,7 +194535,7 @@ ${prompt}` : prompt,
|
|
|
194534
194535
|
const replace = has2("replace") || has2("overwrite");
|
|
194535
194536
|
const newSeries = has2("new-series") || has2("newSeries") || Boolean(envInputs.newSeries);
|
|
194536
194537
|
const seriesRef = String(envInputs.series ?? pickOpt4("series") ?? "").trim();
|
|
194537
|
-
const outDirRel = String(envInputs.outDir ?? pickOpt4("out") ?? "
|
|
194538
|
+
const outDirRel = String(envInputs.outDir ?? pickOpt4("out") ?? ".maria/desktop/novel").trim() || ".maria/desktop/novel";
|
|
194538
194539
|
const lang = normalizeLang(String(envInputs.lang ?? pickOpt4("lang") ?? "en"));
|
|
194539
194540
|
const format2 = normalizeFormat4(String(envInputs.format ?? pickOpt4("format") ?? "md"));
|
|
194540
194541
|
const genre = String(envInputs.genre ?? pickOpt4("genre") ?? "").trim() || void 0;
|
|
@@ -227615,8 +227616,8 @@ ${this.help.usage}` };
|
|
|
227615
227616
|
const format2 = normalizeFormat5(ctx.parsed.options.format);
|
|
227616
227617
|
const model = ctx.parsed.options.model ? String(ctx.parsed.options.model).trim() : void 0;
|
|
227617
227618
|
const seed = ctx.parsed.options.seed ? parseSeed4(ctx.parsed.options.seed) : void 0;
|
|
227618
|
-
const outDirRel = normalizeMangaOutDir(ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "manga");
|
|
227619
|
-
const outRootAbs = path87__namespace.resolve(ctx.cwd, outDirRel || "manga");
|
|
227619
|
+
const outDirRel = normalizeMangaOutDir(ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/manga");
|
|
227620
|
+
const outRootAbs = path87__namespace.resolve(ctx.cwd, outDirRel || ".maria/desktop/manga");
|
|
227620
227621
|
const stamp = nowStamp5();
|
|
227621
227622
|
const inBase = inParts.length === 1 ? path87__namespace.basename(inParts[0]) : inParts.length > 1 ? "sources" : "";
|
|
227622
227623
|
const folder = `${stamp}_${safeSlug3((theme || inBase || "source").slice(0, 64))}`;
|
|
@@ -228543,7 +228544,7 @@ function createSlidesField() {
|
|
|
228543
228544
|
const format2 = normalizeFormat3(ctx.parsed.options.format);
|
|
228544
228545
|
const model = typeof ctx.parsed.options.model === "string" ? String(ctx.parsed.options.model).trim() : void 0;
|
|
228545
228546
|
const seed = typeof ctx.parsed.options.seed === "string" ? parseSeed3(ctx.parsed.options.seed) : void 0;
|
|
228546
|
-
const outBaseRel = normalizeSlidesOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "slides");
|
|
228547
|
+
const outBaseRel = normalizeSlidesOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/slides");
|
|
228547
228548
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf");
|
|
228548
228549
|
const wantsPptx = hasLiteFlag(ctx.parsed, "pptx");
|
|
228549
228550
|
const titleTitleShiftRaw = typeof ctx.parsed.options["title-title-shift"] === "string" ? String(ctx.parsed.options["title-title-shift"]).trim() : "";
|
|
@@ -230075,7 +230076,7 @@ function createFilmField() {
|
|
|
230075
230076
|
const model = modelRaw || "sora-2";
|
|
230076
230077
|
const provider = typeof ctx.parsed.options.provider === "string" ? String(ctx.parsed.options.provider).trim() : void 0;
|
|
230077
230078
|
const seed = ctx.parsed.options.seed ? parseSeed5(ctx.parsed.options.seed) : void 0;
|
|
230078
|
-
const outBaseRel = normalizeFilmOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "film");
|
|
230079
|
+
const outBaseRel = normalizeFilmOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/film");
|
|
230079
230080
|
const res = typeof ctx.parsed.options.res === "string" && String(ctx.parsed.options.res).trim() ? String(ctx.parsed.options.res).trim() : pickDefaultResForAspect({ aspect });
|
|
230080
230081
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
230081
230082
|
const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
|
|
@@ -231001,7 +231002,7 @@ function createDocsField() {
|
|
|
231001
231002
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
231002
231003
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
231003
231004
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
231004
|
-
const outBaseRel = (outBase || "docs").replace(/\\/g, "/");
|
|
231005
|
+
const outBaseRel = (outBase || ".maria/desktop/docs").replace(/\\/g, "/");
|
|
231005
231006
|
const src = await resolveInputSourceText(ctx);
|
|
231006
231007
|
if (!src.sourceText.trim()) return { text: `Usage:
|
|
231007
231008
|
${this.help.usage}` };
|
|
@@ -231308,8 +231309,8 @@ async function loadCommonSettings(cwd) {
|
|
|
231308
231309
|
if (!r2) return null;
|
|
231309
231310
|
const nums = ["up", "right", "down", "left"].map((k) => Number(r2[k]));
|
|
231310
231311
|
if (!nums.every((n) => Number.isFinite(n))) return null;
|
|
231311
|
-
const
|
|
231312
|
-
return { up:
|
|
231312
|
+
const clamp2 = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
|
|
231313
|
+
return { up: clamp2(nums[0]), right: clamp2(nums[1]), down: clamp2(nums[2]), left: clamp2(nums[3]) };
|
|
231313
231314
|
};
|
|
231314
231315
|
const titleTitleShift = normShift(v.titleTitleShift);
|
|
231315
231316
|
const titleSubtitleShift = normShift(v.titleSubtitleShift);
|
|
@@ -256282,6 +256283,1492 @@ var init_phone_routes = __esm({
|
|
|
256282
256283
|
init_desktop_server_helpers();
|
|
256283
256284
|
}
|
|
256284
256285
|
});
|
|
256286
|
+
|
|
256287
|
+
// services/desktop/routes/calendar-api-routes.ts
|
|
256288
|
+
async function getToken2() {
|
|
256289
|
+
const mgr = new GoogleOAuthManager();
|
|
256290
|
+
return mgr.getValidToken();
|
|
256291
|
+
}
|
|
256292
|
+
function notConnected2(res) {
|
|
256293
|
+
respondJson2(res, 200, { ok: false, error: "not_connected" });
|
|
256294
|
+
return true;
|
|
256295
|
+
}
|
|
256296
|
+
async function gFetch2(token, url, init) {
|
|
256297
|
+
const headers = {
|
|
256298
|
+
Authorization: `Bearer ${token}`,
|
|
256299
|
+
...init?.headers || {}
|
|
256300
|
+
};
|
|
256301
|
+
return fetch(url, { ...init, headers });
|
|
256302
|
+
}
|
|
256303
|
+
function resolveProjectId2() {
|
|
256304
|
+
return process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || process.env.FIREBASE_PROJECT_ID || process.env.GCP_PROJECT_ID || "";
|
|
256305
|
+
}
|
|
256306
|
+
async function getFirestore2() {
|
|
256307
|
+
if (_firestoreInstance2) return _firestoreInstance2;
|
|
256308
|
+
if (_firestoreInitPromise2) return _firestoreInitPromise2;
|
|
256309
|
+
_firestoreInitPromise2 = (async () => {
|
|
256310
|
+
const projectId = resolveProjectId2();
|
|
256311
|
+
if (!projectId) return null;
|
|
256312
|
+
try {
|
|
256313
|
+
const { Firestore: Firestore3 } = await Promise.resolve().then(() => __toESM(require_src21(), 1));
|
|
256314
|
+
_firestoreInstance2 = new Firestore3({ projectId });
|
|
256315
|
+
return _firestoreInstance2;
|
|
256316
|
+
} catch {
|
|
256317
|
+
return null;
|
|
256318
|
+
}
|
|
256319
|
+
})();
|
|
256320
|
+
return _firestoreInitPromise2;
|
|
256321
|
+
}
|
|
256322
|
+
function firestoreUnavailable(res) {
|
|
256323
|
+
respondJson2(res, 503, {
|
|
256324
|
+
ok: false,
|
|
256325
|
+
error: "firestore_unavailable",
|
|
256326
|
+
message: "Firestore is not configured. Set GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT env var."
|
|
256327
|
+
});
|
|
256328
|
+
return true;
|
|
256329
|
+
}
|
|
256330
|
+
function generateReservationId() {
|
|
256331
|
+
const now = /* @__PURE__ */ new Date();
|
|
256332
|
+
const datePart = [
|
|
256333
|
+
now.getFullYear(),
|
|
256334
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
256335
|
+
String(now.getDate()).padStart(2, "0")
|
|
256336
|
+
].join("");
|
|
256337
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
256338
|
+
let random = "";
|
|
256339
|
+
for (let i2 = 0; i2 < 5; i2++) {
|
|
256340
|
+
random += chars[Math.floor(Math.random() * chars.length)];
|
|
256341
|
+
}
|
|
256342
|
+
return `R-${datePart}-${random}`;
|
|
256343
|
+
}
|
|
256344
|
+
function buildRrule(type2, count, until) {
|
|
256345
|
+
const freqMap = {
|
|
256346
|
+
daily: "DAILY",
|
|
256347
|
+
weekly: "WEEKLY",
|
|
256348
|
+
biweekly: "WEEKLY",
|
|
256349
|
+
monthly: "MONTHLY"
|
|
256350
|
+
};
|
|
256351
|
+
const freq = freqMap[type2];
|
|
256352
|
+
if (!freq) return null;
|
|
256353
|
+
let rule = `RRULE:FREQ=${freq}`;
|
|
256354
|
+
if (type2 === "biweekly") rule += ";INTERVAL=2";
|
|
256355
|
+
if (count) rule += `;COUNT=${count}`;
|
|
256356
|
+
else if (until) rule += `;UNTIL=${until.replace(/-/g, "")}T235959Z`;
|
|
256357
|
+
return rule;
|
|
256358
|
+
}
|
|
256359
|
+
async function fetchResources(token, buildingId, minCapacity) {
|
|
256360
|
+
let url = `${ADMIN_RESOURCES_BASE}/calendars?maxResults=500`;
|
|
256361
|
+
if (buildingId) {
|
|
256362
|
+
url += `&query=buildingId="${encodeURIComponent(buildingId)}"`;
|
|
256363
|
+
}
|
|
256364
|
+
const apiRes = await gFetch2(token, url);
|
|
256365
|
+
if (!apiRes.ok) return [];
|
|
256366
|
+
const data = await apiRes.json();
|
|
256367
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
256368
|
+
const resources = items.map((item) => ({
|
|
256369
|
+
resourceId: String(item.resourceId || ""),
|
|
256370
|
+
resourceEmail: String(item.resourceEmail || ""),
|
|
256371
|
+
resourceName: String(item.resourceName || ""),
|
|
256372
|
+
resourceType: String(item.resourceType || ""),
|
|
256373
|
+
capacity: Number(item.capacity || 0),
|
|
256374
|
+
buildingId: String(item.buildingId || ""),
|
|
256375
|
+
floorName: String(item.floorName || ""),
|
|
256376
|
+
userVisibleDescription: String(item.userVisibleDescription || "")
|
|
256377
|
+
}));
|
|
256378
|
+
if (minCapacity != null && minCapacity > 0) {
|
|
256379
|
+
return resources.filter((r2) => r2.capacity >= minCapacity);
|
|
256380
|
+
}
|
|
256381
|
+
return resources;
|
|
256382
|
+
}
|
|
256383
|
+
function findSlots(params) {
|
|
256384
|
+
const {
|
|
256385
|
+
busyMap,
|
|
256386
|
+
required,
|
|
256387
|
+
optional,
|
|
256388
|
+
resourceEmails,
|
|
256389
|
+
durationMs,
|
|
256390
|
+
dateFrom,
|
|
256391
|
+
dateTo,
|
|
256392
|
+
workStartHour,
|
|
256393
|
+
workEndHour,
|
|
256394
|
+
offset,
|
|
256395
|
+
bufferMinutes = DEFAULT_BUFFER_MINUTES,
|
|
256396
|
+
timeZone = DEFAULT_TIMEZONE
|
|
256397
|
+
} = params;
|
|
256398
|
+
const minBufferMs = bufferMinutes * 60 * 1e3;
|
|
256399
|
+
const allTargets = [...resourceEmails, ...required, ...optional];
|
|
256400
|
+
const candidates = [];
|
|
256401
|
+
const [fromY, fromM, fromD] = dateFrom.split("-").map(Number);
|
|
256402
|
+
const [toY, toM, toD] = dateTo.split("-").map(Number);
|
|
256403
|
+
const dStart = new Date(Date.UTC(fromY, fromM - 1, fromD));
|
|
256404
|
+
const dEnd = new Date(Date.UTC(toY, toM - 1, toD));
|
|
256405
|
+
for (let d = new Date(dStart); d <= dEnd; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
256406
|
+
const yy = d.getUTCFullYear();
|
|
256407
|
+
const mm = d.getUTCMonth() + 1;
|
|
256408
|
+
const dd = d.getUTCDate();
|
|
256409
|
+
const dayStr = `${yy}-${String(mm).padStart(2, "0")}-${String(dd).padStart(2, "0")}`;
|
|
256410
|
+
const weekdayRef = new Date(Date.UTC(yy, mm - 1, dd, 12, 0, 0));
|
|
256411
|
+
if (weekdayRef.getUTCDay() === 0 || weekdayRef.getUTCDay() === 6) continue;
|
|
256412
|
+
const slotDayStart = makeDateInTz(dayStr, workStartHour, 0, timeZone);
|
|
256413
|
+
const slotDayEnd = makeDateInTz(dayStr, workEndHour, 0, timeZone);
|
|
256414
|
+
for (let t2 = slotDayStart.getTime(); t2 + durationMs <= slotDayEnd.getTime(); t2 += SLOT_STEP_MS) {
|
|
256415
|
+
const slotS = t2;
|
|
256416
|
+
const slotE = t2 + durationMs;
|
|
256417
|
+
let requiredOk = true;
|
|
256418
|
+
let resourcesOk = true;
|
|
256419
|
+
let availCount = 0;
|
|
256420
|
+
const missingOpt = [];
|
|
256421
|
+
const availableResourceEmails = [];
|
|
256422
|
+
for (const email of resourceEmails) {
|
|
256423
|
+
const busy = busyMap[email] || [];
|
|
256424
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256425
|
+
if (isBusy) {
|
|
256426
|
+
resourcesOk = false;
|
|
256427
|
+
} else {
|
|
256428
|
+
availableResourceEmails.push(email);
|
|
256429
|
+
availCount++;
|
|
256430
|
+
}
|
|
256431
|
+
}
|
|
256432
|
+
if (resourceEmails.length > 0 && !resourcesOk && availableResourceEmails.length === 0) continue;
|
|
256433
|
+
for (const email of required) {
|
|
256434
|
+
const busy = busyMap[email] || [];
|
|
256435
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256436
|
+
if (isBusy) {
|
|
256437
|
+
requiredOk = false;
|
|
256438
|
+
} else {
|
|
256439
|
+
availCount++;
|
|
256440
|
+
}
|
|
256441
|
+
}
|
|
256442
|
+
if (!requiredOk) continue;
|
|
256443
|
+
for (const email of optional) {
|
|
256444
|
+
const busy = busyMap[email] || [];
|
|
256445
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256446
|
+
if (isBusy) {
|
|
256447
|
+
missingOpt.push(email);
|
|
256448
|
+
} else {
|
|
256449
|
+
availCount++;
|
|
256450
|
+
}
|
|
256451
|
+
}
|
|
256452
|
+
const mustBeAvailable = [...resourceEmails.filter((e2) => availableResourceEmails.includes(e2)), ...required];
|
|
256453
|
+
let bufBefore = Infinity;
|
|
256454
|
+
let bufAfter = Infinity;
|
|
256455
|
+
for (const email of mustBeAvailable) {
|
|
256456
|
+
const busy = busyMap[email] || [];
|
|
256457
|
+
let gapBefore = slotS - slotDayStart.getTime();
|
|
256458
|
+
let gapAfter = slotDayEnd.getTime() - slotE;
|
|
256459
|
+
for (const b of busy) {
|
|
256460
|
+
if (b.e <= slotS) gapBefore = Math.min(gapBefore, slotS - b.e);
|
|
256461
|
+
if (b.s >= slotE) gapAfter = Math.min(gapAfter, b.s - slotE);
|
|
256462
|
+
}
|
|
256463
|
+
bufBefore = Math.min(bufBefore, gapBefore);
|
|
256464
|
+
bufAfter = Math.min(bufAfter, gapAfter);
|
|
256465
|
+
}
|
|
256466
|
+
if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
|
|
256467
|
+
if (!isFinite(bufAfter)) bufAfter = 12 * 60 * 60 * 1e3;
|
|
256468
|
+
candidates.push({
|
|
256469
|
+
start: slotS,
|
|
256470
|
+
end: slotE,
|
|
256471
|
+
allAvailable: availCount === allTargets.length,
|
|
256472
|
+
availableCount: availCount,
|
|
256473
|
+
totalParticipants: allTargets.length,
|
|
256474
|
+
missingOptional: missingOpt,
|
|
256475
|
+
bufferBefore: bufBefore,
|
|
256476
|
+
bufferAfter: bufAfter,
|
|
256477
|
+
resourceEmails: availableResourceEmails
|
|
256478
|
+
});
|
|
256479
|
+
}
|
|
256480
|
+
}
|
|
256481
|
+
const hasSufficientBuffer = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
|
|
256482
|
+
const bufferImbalance = (c) => Math.abs(c.bufferBefore - c.bufferAfter);
|
|
256483
|
+
candidates.sort((a, b) => {
|
|
256484
|
+
if (a.allAvailable !== b.allAvailable) return a.allAvailable ? -1 : 1;
|
|
256485
|
+
if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
|
|
256486
|
+
if (a.missingOptional.length !== b.missingOptional.length) return a.missingOptional.length - b.missingOptional.length;
|
|
256487
|
+
const optIdx = (m2) => {
|
|
256488
|
+
let worst = -1;
|
|
256489
|
+
for (const e2 of m2) {
|
|
256490
|
+
const i2 = optional.indexOf(e2);
|
|
256491
|
+
if (i2 > worst) worst = i2;
|
|
256492
|
+
}
|
|
256493
|
+
return worst;
|
|
256494
|
+
};
|
|
256495
|
+
const ai = optIdx(a.missingOptional);
|
|
256496
|
+
const bi = optIdx(b.missingOptional);
|
|
256497
|
+
if (ai !== bi) return ai - bi;
|
|
256498
|
+
const aSuf = hasSufficientBuffer(a);
|
|
256499
|
+
const bSuf = hasSufficientBuffer(b);
|
|
256500
|
+
if (aSuf && bSuf) {
|
|
256501
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
256502
|
+
return bufferImbalance(a) - bufferImbalance(b);
|
|
256503
|
+
}
|
|
256504
|
+
if (aSuf !== bSuf) return aSuf ? -1 : 1;
|
|
256505
|
+
const aTot = a.bufferBefore + a.bufferAfter;
|
|
256506
|
+
const bTot = b.bufferBefore + b.bufferAfter;
|
|
256507
|
+
if (aTot !== bTot) return bTot - aTot;
|
|
256508
|
+
return bufferImbalance(a) - bufferImbalance(b);
|
|
256509
|
+
});
|
|
256510
|
+
const nonOverlapping = [];
|
|
256511
|
+
const needed = (offset + 1) * TOP_SLOT_COUNT + 1;
|
|
256512
|
+
for (const c of candidates) {
|
|
256513
|
+
const overlaps = nonOverlapping.some((t2) => t2.start < c.end && t2.end > c.start);
|
|
256514
|
+
if (!overlaps) nonOverlapping.push(c);
|
|
256515
|
+
if (nonOverlapping.length >= needed) break;
|
|
256516
|
+
}
|
|
256517
|
+
const start = offset * TOP_SLOT_COUNT;
|
|
256518
|
+
const page = nonOverlapping.slice(start, start + TOP_SLOT_COUNT);
|
|
256519
|
+
return { slots: page, hasMore: nonOverlapping.length > start + TOP_SLOT_COUNT };
|
|
256520
|
+
}
|
|
256521
|
+
async function handleCalendarApiRoute(method, pathname, req, res) {
|
|
256522
|
+
if (!pathname.startsWith("/api/calendar/")) return false;
|
|
256523
|
+
if (method === "GET" && pathname === "/api/calendar/resources") {
|
|
256524
|
+
const token = await getToken2();
|
|
256525
|
+
if (!token) return notConnected2(res);
|
|
256526
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
256527
|
+
const buildingId = url.searchParams.get("buildingId") || void 0;
|
|
256528
|
+
const capacityRaw = url.searchParams.get("capacity");
|
|
256529
|
+
const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
|
|
256530
|
+
try {
|
|
256531
|
+
const resources = await fetchResources(token, buildingId, capacity);
|
|
256532
|
+
return respondJson2(res, 200, { ok: true, resources }), true;
|
|
256533
|
+
} catch (err) {
|
|
256534
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256535
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${msg}` }), true;
|
|
256536
|
+
}
|
|
256537
|
+
}
|
|
256538
|
+
if (method === "GET" && pathname === "/api/calendar/buildings") {
|
|
256539
|
+
const token = await getToken2();
|
|
256540
|
+
if (!token) return notConnected2(res);
|
|
256541
|
+
const apiRes = await gFetch2(token, `${ADMIN_RESOURCES_BASE}/buildings?maxResults=500`);
|
|
256542
|
+
if (!apiRes.ok) {
|
|
256543
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${apiRes.status}` }), true;
|
|
256544
|
+
}
|
|
256545
|
+
const data = await apiRes.json();
|
|
256546
|
+
const items = Array.isArray(data.buildings) ? data.buildings : [];
|
|
256547
|
+
const buildings = items.map((b) => ({
|
|
256548
|
+
buildingId: String(b.buildingId || ""),
|
|
256549
|
+
buildingName: String(b.buildingName || ""),
|
|
256550
|
+
description: String(b.description || ""),
|
|
256551
|
+
floorNames: Array.isArray(b.floorNames) ? b.floorNames : []
|
|
256552
|
+
}));
|
|
256553
|
+
return respondJson2(res, 200, { ok: true, buildings }), true;
|
|
256554
|
+
}
|
|
256555
|
+
if (method === "POST" && pathname === "/api/calendar/buildings") {
|
|
256556
|
+
const token = await getToken2();
|
|
256557
|
+
if (!token) return notConnected2(res);
|
|
256558
|
+
const reqBody = await parseJsonBody2(req);
|
|
256559
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256560
|
+
const buildingName = String(reqBody.buildingName || "").trim();
|
|
256561
|
+
if (!buildingName) return respondJson2(res, 400, { ok: false, error: "buildingName is required" }), true;
|
|
256562
|
+
const buildingId = String(reqBody.buildingId || "").trim() || buildingName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `bld-${Date.now()}`;
|
|
256563
|
+
const body = { buildingId, buildingName };
|
|
256564
|
+
if (reqBody.description) body.description = String(reqBody.description).trim();
|
|
256565
|
+
if (reqBody.floorNames) {
|
|
256566
|
+
if (Array.isArray(reqBody.floorNames)) {
|
|
256567
|
+
body.floorNames = reqBody.floorNames.map(String);
|
|
256568
|
+
} else {
|
|
256569
|
+
const raw = String(reqBody.floorNames).trim();
|
|
256570
|
+
const n = Number(raw);
|
|
256571
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
256572
|
+
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
256573
|
+
} else {
|
|
256574
|
+
body.floorNames = raw.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
256575
|
+
}
|
|
256576
|
+
}
|
|
256577
|
+
}
|
|
256578
|
+
const apiRes = await gFetch2(token, `${ADMIN_RESOURCES_BASE}/buildings`, {
|
|
256579
|
+
method: "POST",
|
|
256580
|
+
headers: { "Content-Type": "application/json" },
|
|
256581
|
+
body: JSON.stringify(body)
|
|
256582
|
+
});
|
|
256583
|
+
if (!apiRes.ok) {
|
|
256584
|
+
const errText = await apiRes.text().catch(() => "");
|
|
256585
|
+
if (apiRes.status === 403) {
|
|
256586
|
+
return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256587
|
+
}
|
|
256588
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${apiRes.status} ${errText}` }), true;
|
|
256589
|
+
}
|
|
256590
|
+
const created = await apiRes.json();
|
|
256591
|
+
return respondJson2(res, 200, {
|
|
256592
|
+
ok: true,
|
|
256593
|
+
building: {
|
|
256594
|
+
buildingId: String(created.buildingId || ""),
|
|
256595
|
+
buildingName: String(created.buildingName || ""),
|
|
256596
|
+
description: String(created.description || ""),
|
|
256597
|
+
floorNames: Array.isArray(created.floorNames) ? created.floorNames : []
|
|
256598
|
+
}
|
|
256599
|
+
}), true;
|
|
256600
|
+
}
|
|
256601
|
+
if (method === "POST" && pathname === "/api/calendar/resources") {
|
|
256602
|
+
const token = await getToken2();
|
|
256603
|
+
if (!token) return notConnected2(res);
|
|
256604
|
+
const reqBody = await parseJsonBody2(req);
|
|
256605
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256606
|
+
const resourceName = String(reqBody.resourceName || "").trim();
|
|
256607
|
+
if (!resourceName) return respondJson2(res, 400, { ok: false, error: "resourceName is required" }), true;
|
|
256608
|
+
const resourceId = String(reqBody.resourceId || "").trim() || resourceName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `res-${Date.now()}`;
|
|
256609
|
+
const body = {
|
|
256610
|
+
resourceId,
|
|
256611
|
+
resourceName,
|
|
256612
|
+
resourceType: String(reqBody.resourceType || "CONFERENCE_ROOM").trim()
|
|
256613
|
+
};
|
|
256614
|
+
if (reqBody.buildingId) body.buildingId = String(reqBody.buildingId).trim();
|
|
256615
|
+
if (reqBody.capacity) body.capacity = Math.max(0, Number(reqBody.capacity) || 0);
|
|
256616
|
+
if (reqBody.floorName) body.floorName = String(reqBody.floorName).trim();
|
|
256617
|
+
if (reqBody.resourceDescription) body.resourceDescription = String(reqBody.resourceDescription).trim();
|
|
256618
|
+
const apiRes = await gFetch2(token, `${ADMIN_RESOURCES_BASE}/calendars`, {
|
|
256619
|
+
method: "POST",
|
|
256620
|
+
headers: { "Content-Type": "application/json" },
|
|
256621
|
+
body: JSON.stringify(body)
|
|
256622
|
+
});
|
|
256623
|
+
if (!apiRes.ok) {
|
|
256624
|
+
const errText = await apiRes.text().catch(() => "");
|
|
256625
|
+
if (apiRes.status === 403) {
|
|
256626
|
+
return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256627
|
+
}
|
|
256628
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${apiRes.status} ${errText}` }), true;
|
|
256629
|
+
}
|
|
256630
|
+
const created = await apiRes.json();
|
|
256631
|
+
return respondJson2(res, 200, {
|
|
256632
|
+
ok: true,
|
|
256633
|
+
resource: {
|
|
256634
|
+
resourceId: String(created.resourceId || ""),
|
|
256635
|
+
resourceEmail: String(created.resourceEmail || ""),
|
|
256636
|
+
resourceName: String(created.resourceName || ""),
|
|
256637
|
+
capacity: Number(created.capacity || 0),
|
|
256638
|
+
buildingId: String(created.buildingId || ""),
|
|
256639
|
+
resourceType: String(created.resourceType || ""),
|
|
256640
|
+
floorName: String(created.floorName || "")
|
|
256641
|
+
}
|
|
256642
|
+
}), true;
|
|
256643
|
+
}
|
|
256644
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/buildings/")) {
|
|
256645
|
+
const buildingId = decodeURIComponent(pathname.slice("/api/calendar/buildings/".length));
|
|
256646
|
+
if (!buildingId) return respondJson2(res, 400, { ok: false, error: "buildingId is required" }), true;
|
|
256647
|
+
const token = await getToken2();
|
|
256648
|
+
if (!token) return notConnected2(res);
|
|
256649
|
+
const reqBody = await parseJsonBody2(req);
|
|
256650
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256651
|
+
const body = {};
|
|
256652
|
+
if (reqBody.buildingName) body.buildingName = String(reqBody.buildingName).trim();
|
|
256653
|
+
if (reqBody.description !== void 0) body.description = String(reqBody.description).trim();
|
|
256654
|
+
if (reqBody.floorNames) {
|
|
256655
|
+
if (Array.isArray(reqBody.floorNames)) {
|
|
256656
|
+
body.floorNames = reqBody.floorNames.map(String);
|
|
256657
|
+
} else {
|
|
256658
|
+
const raw = String(reqBody.floorNames).trim();
|
|
256659
|
+
const n = Number(raw);
|
|
256660
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
256661
|
+
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
256662
|
+
} else {
|
|
256663
|
+
body.floorNames = raw.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
256664
|
+
}
|
|
256665
|
+
}
|
|
256666
|
+
}
|
|
256667
|
+
if (Object.keys(body).length === 0) {
|
|
256668
|
+
return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
|
|
256669
|
+
}
|
|
256670
|
+
const apiRes = await gFetch2(token, `${ADMIN_RESOURCES_BASE}/buildings/${encodeURIComponent(buildingId)}`, {
|
|
256671
|
+
method: "PATCH",
|
|
256672
|
+
headers: { "Content-Type": "application/json" },
|
|
256673
|
+
body: JSON.stringify(body)
|
|
256674
|
+
});
|
|
256675
|
+
if (!apiRes.ok) {
|
|
256676
|
+
const errText = await apiRes.text().catch(() => "");
|
|
256677
|
+
if (apiRes.status === 403) {
|
|
256678
|
+
return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256679
|
+
}
|
|
256680
|
+
if (apiRes.status === 404) {
|
|
256681
|
+
return respondJson2(res, 404, { ok: false, error: "Building not found" }), true;
|
|
256682
|
+
}
|
|
256683
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${apiRes.status} ${errText}` }), true;
|
|
256684
|
+
}
|
|
256685
|
+
const updated = await apiRes.json();
|
|
256686
|
+
return respondJson2(res, 200, {
|
|
256687
|
+
ok: true,
|
|
256688
|
+
building: {
|
|
256689
|
+
buildingId: String(updated.buildingId || ""),
|
|
256690
|
+
buildingName: String(updated.buildingName || ""),
|
|
256691
|
+
description: String(updated.description || ""),
|
|
256692
|
+
floorNames: Array.isArray(updated.floorNames) ? updated.floorNames : []
|
|
256693
|
+
}
|
|
256694
|
+
}), true;
|
|
256695
|
+
}
|
|
256696
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/resources/")) {
|
|
256697
|
+
const resourceId = decodeURIComponent(pathname.slice("/api/calendar/resources/".length));
|
|
256698
|
+
if (!resourceId) return respondJson2(res, 400, { ok: false, error: "resourceId is required" }), true;
|
|
256699
|
+
const token = await getToken2();
|
|
256700
|
+
if (!token) return notConnected2(res);
|
|
256701
|
+
const reqBody = await parseJsonBody2(req);
|
|
256702
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256703
|
+
const body = {};
|
|
256704
|
+
if (reqBody.resourceName) body.resourceName = String(reqBody.resourceName).trim();
|
|
256705
|
+
if (reqBody.buildingId !== void 0) body.buildingId = String(reqBody.buildingId).trim();
|
|
256706
|
+
if (reqBody.capacity !== void 0) body.capacity = Math.max(0, Number(reqBody.capacity) || 0);
|
|
256707
|
+
if (reqBody.resourceType) body.resourceType = String(reqBody.resourceType).trim();
|
|
256708
|
+
if (reqBody.floorName !== void 0) body.floorName = String(reqBody.floorName).trim();
|
|
256709
|
+
if (reqBody.resourceDescription !== void 0) body.resourceDescription = String(reqBody.resourceDescription).trim();
|
|
256710
|
+
if (Object.keys(body).length === 0) {
|
|
256711
|
+
return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
|
|
256712
|
+
}
|
|
256713
|
+
const apiRes = await gFetch2(token, `${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`, {
|
|
256714
|
+
method: "PATCH",
|
|
256715
|
+
headers: { "Content-Type": "application/json" },
|
|
256716
|
+
body: JSON.stringify(body)
|
|
256717
|
+
});
|
|
256718
|
+
if (!apiRes.ok) {
|
|
256719
|
+
const errText = await apiRes.text().catch(() => "");
|
|
256720
|
+
if (apiRes.status === 403) {
|
|
256721
|
+
return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256722
|
+
}
|
|
256723
|
+
if (apiRes.status === 404) {
|
|
256724
|
+
return respondJson2(res, 404, { ok: false, error: "Resource not found" }), true;
|
|
256725
|
+
}
|
|
256726
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${apiRes.status} ${errText}` }), true;
|
|
256727
|
+
}
|
|
256728
|
+
const updated = await apiRes.json();
|
|
256729
|
+
return respondJson2(res, 200, {
|
|
256730
|
+
ok: true,
|
|
256731
|
+
resource: {
|
|
256732
|
+
resourceId: String(updated.resourceId || ""),
|
|
256733
|
+
resourceEmail: String(updated.resourceEmail || ""),
|
|
256734
|
+
resourceName: String(updated.resourceName || ""),
|
|
256735
|
+
capacity: Number(updated.capacity || 0),
|
|
256736
|
+
buildingId: String(updated.buildingId || ""),
|
|
256737
|
+
resourceType: String(updated.resourceType || ""),
|
|
256738
|
+
floorName: String(updated.floorName || "")
|
|
256739
|
+
}
|
|
256740
|
+
}), true;
|
|
256741
|
+
}
|
|
256742
|
+
if (method === "POST" && pathname === "/api/calendar/slots") {
|
|
256743
|
+
const token = await getToken2();
|
|
256744
|
+
if (!token) return notConnected2(res);
|
|
256745
|
+
const reqBody = await parseJsonBody2(req);
|
|
256746
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256747
|
+
const resourceId = String(reqBody.resourceId || "").trim();
|
|
256748
|
+
const buildingId = String(reqBody.buildingId || "").trim();
|
|
256749
|
+
const capacity = Number(reqBody.capacity || 0) || 0;
|
|
256750
|
+
const dateSingle = String(reqBody.date || "").trim();
|
|
256751
|
+
const dateFrom = dateSingle || String(reqBody.dateFrom || "").trim();
|
|
256752
|
+
const dateTo = dateSingle || String(reqBody.dateTo || "").trim();
|
|
256753
|
+
const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
|
|
256754
|
+
const workStartHour = Number(reqBody.workStartHour ?? DEFAULT_WORK_START_HOUR);
|
|
256755
|
+
const workEndHour = Number(reqBody.workEndHour ?? DEFAULT_WORK_END_HOUR);
|
|
256756
|
+
const offset = Math.max(0, Number(reqBody.offset || 0) || 0);
|
|
256757
|
+
const bufferMinutes = Math.max(0, Math.min(60, Number(reqBody.bufferMinutes ?? DEFAULT_BUFFER_MINUTES) || 0));
|
|
256758
|
+
const required = Array.isArray(reqBody.required) ? reqBody.required.map(String).filter(Boolean) : [];
|
|
256759
|
+
const optional = Array.isArray(reqBody.optional) ? reqBody.optional.map(String).filter(Boolean) : [];
|
|
256760
|
+
if (!dateFrom || !dateTo) {
|
|
256761
|
+
return respondJson2(res, 400, { ok: false, error: "date or dateFrom/dateTo is required (YYYY-MM-DD)" }), true;
|
|
256762
|
+
}
|
|
256763
|
+
let resourceEmails = [];
|
|
256764
|
+
let resolvedResources = [];
|
|
256765
|
+
if (resourceId) {
|
|
256766
|
+
resourceEmails = [resourceId];
|
|
256767
|
+
} else if (buildingId || capacity > 0) {
|
|
256768
|
+
resolvedResources = await fetchResources(
|
|
256769
|
+
token,
|
|
256770
|
+
buildingId || void 0,
|
|
256771
|
+
capacity > 0 ? capacity : void 0
|
|
256772
|
+
);
|
|
256773
|
+
resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
|
|
256774
|
+
} else if (required.length === 0 && optional.length === 0) {
|
|
256775
|
+
resolvedResources = await fetchResources(token);
|
|
256776
|
+
resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
|
|
256777
|
+
}
|
|
256778
|
+
const allTargets = [...resourceEmails, ...required, ...optional];
|
|
256779
|
+
if (allTargets.length === 0) {
|
|
256780
|
+
return respondJson2(res, 400, { ok: false, error: "At least one target is required (resourceId, buildingId, required, or optional)" }), true;
|
|
256781
|
+
}
|
|
256782
|
+
const tz = String(reqBody.timeZone || "").trim() || DEFAULT_TIMEZONE;
|
|
256783
|
+
const timeMin = makeDateInTz(dateFrom, 0, 0, tz).toISOString();
|
|
256784
|
+
const timeMax = makeDateInTz(dateTo, 23, 59, tz).toISOString();
|
|
256785
|
+
const fbBody = {
|
|
256786
|
+
timeMin,
|
|
256787
|
+
timeMax,
|
|
256788
|
+
timeZone: tz,
|
|
256789
|
+
items: allTargets.map((e2) => ({ id: e2 }))
|
|
256790
|
+
};
|
|
256791
|
+
const fbRes = await gFetch2(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256792
|
+
method: "POST",
|
|
256793
|
+
headers: { "Content-Type": "application/json" },
|
|
256794
|
+
body: JSON.stringify(fbBody)
|
|
256795
|
+
});
|
|
256796
|
+
if (!fbRes.ok) {
|
|
256797
|
+
const err = await fbRes.text().catch(() => "");
|
|
256798
|
+
return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
|
|
256799
|
+
}
|
|
256800
|
+
const fbData = await fbRes.json();
|
|
256801
|
+
const calendars = fbData.calendars || {};
|
|
256802
|
+
const busyMap = {};
|
|
256803
|
+
for (const email of allTargets) {
|
|
256804
|
+
const cal = calendars[email];
|
|
256805
|
+
busyMap[email] = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ s: new Date(b.start).getTime(), e: new Date(b.end).getTime() })) : [];
|
|
256806
|
+
}
|
|
256807
|
+
const durationMs = durationMinutes * 60 * 1e3;
|
|
256808
|
+
const { slots: top, hasMore } = findSlots({
|
|
256809
|
+
busyMap,
|
|
256810
|
+
required,
|
|
256811
|
+
optional,
|
|
256812
|
+
resourceEmails,
|
|
256813
|
+
durationMs,
|
|
256814
|
+
dateFrom,
|
|
256815
|
+
dateTo,
|
|
256816
|
+
workStartHour,
|
|
256817
|
+
workEndHour,
|
|
256818
|
+
offset,
|
|
256819
|
+
bufferMinutes,
|
|
256820
|
+
timeZone: tz
|
|
256821
|
+
});
|
|
256822
|
+
const slots = top.map((c, i2) => ({
|
|
256823
|
+
rank: i2 + 1,
|
|
256824
|
+
start: new Date(c.start).toISOString(),
|
|
256825
|
+
end: new Date(c.end).toISOString(),
|
|
256826
|
+
durationMinutes,
|
|
256827
|
+
allAvailable: c.allAvailable,
|
|
256828
|
+
availableCount: c.availableCount,
|
|
256829
|
+
totalParticipants: c.totalParticipants,
|
|
256830
|
+
missingOptional: c.missingOptional,
|
|
256831
|
+
bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
|
|
256832
|
+
bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
|
|
256833
|
+
resourceEmails: c.resourceEmails
|
|
256834
|
+
}));
|
|
256835
|
+
const resourceInfo = resolvedResources.length > 0 ? resolvedResources.map((r2) => ({
|
|
256836
|
+
resourceEmail: r2.resourceEmail,
|
|
256837
|
+
resourceName: r2.resourceName,
|
|
256838
|
+
capacity: r2.capacity
|
|
256839
|
+
})) : void 0;
|
|
256840
|
+
return respondJson2(res, 200, {
|
|
256841
|
+
ok: true,
|
|
256842
|
+
slots,
|
|
256843
|
+
hasMore,
|
|
256844
|
+
...resourceInfo ? { resources: resourceInfo } : {},
|
|
256845
|
+
durationMinutes
|
|
256846
|
+
}), true;
|
|
256847
|
+
}
|
|
256848
|
+
if (method === "POST" && pathname === "/api/calendar/check") {
|
|
256849
|
+
const token = await getToken2();
|
|
256850
|
+
if (!token) return notConnected2(res);
|
|
256851
|
+
const reqBody = await parseJsonBody2(req);
|
|
256852
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256853
|
+
const targets = Array.isArray(reqBody.targets) ? reqBody.targets.map(String).filter(Boolean) : [];
|
|
256854
|
+
const datetime = String(reqBody.datetime || "").trim();
|
|
256855
|
+
const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
|
|
256856
|
+
if (!targets.length) return respondJson2(res, 400, { ok: false, error: "targets array is required" }), true;
|
|
256857
|
+
if (!datetime) return respondJson2(res, 400, { ok: false, error: "datetime is required (ISO 8601)" }), true;
|
|
256858
|
+
const startDate = new Date(datetime);
|
|
256859
|
+
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1e3);
|
|
256860
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
256861
|
+
const fbBody = {
|
|
256862
|
+
timeMin: startDate.toISOString(),
|
|
256863
|
+
timeMax: endDate.toISOString(),
|
|
256864
|
+
timeZone: tz,
|
|
256865
|
+
items: targets.map((e2) => ({ id: e2 }))
|
|
256866
|
+
};
|
|
256867
|
+
const fbRes = await gFetch2(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256868
|
+
method: "POST",
|
|
256869
|
+
headers: { "Content-Type": "application/json" },
|
|
256870
|
+
body: JSON.stringify(fbBody)
|
|
256871
|
+
});
|
|
256872
|
+
if (!fbRes.ok) {
|
|
256873
|
+
const err = await fbRes.text().catch(() => "");
|
|
256874
|
+
return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
|
|
256875
|
+
}
|
|
256876
|
+
const fbData = await fbRes.json();
|
|
256877
|
+
const calendars = fbData.calendars || {};
|
|
256878
|
+
let allAvailable = true;
|
|
256879
|
+
const statuses = targets.map((id) => {
|
|
256880
|
+
const cal = calendars[id];
|
|
256881
|
+
const conflicts = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ start: b.start, end: b.end })) : [];
|
|
256882
|
+
const available = conflicts.length === 0;
|
|
256883
|
+
if (!available) allAvailable = false;
|
|
256884
|
+
return { id, available, conflicts };
|
|
256885
|
+
});
|
|
256886
|
+
return respondJson2(res, 200, {
|
|
256887
|
+
ok: true,
|
|
256888
|
+
allAvailable,
|
|
256889
|
+
datetime: startDate.toISOString(),
|
|
256890
|
+
durationMinutes,
|
|
256891
|
+
statuses
|
|
256892
|
+
}), true;
|
|
256893
|
+
}
|
|
256894
|
+
if (method === "POST" && pathname === "/api/calendar/reserve") {
|
|
256895
|
+
const token = await getToken2();
|
|
256896
|
+
if (!token) return notConnected2(res);
|
|
256897
|
+
const db = await getFirestore2();
|
|
256898
|
+
if (!db) return firestoreUnavailable(res);
|
|
256899
|
+
const reqBody = await parseJsonBody2(req);
|
|
256900
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256901
|
+
const resourceEmail = String(reqBody.resourceEmail || "").trim();
|
|
256902
|
+
const start = String(reqBody.start || "").trim();
|
|
256903
|
+
const end = String(reqBody.end || "").trim();
|
|
256904
|
+
const title = String(reqBody.title || "").trim();
|
|
256905
|
+
const description = String(reqBody.description || "").trim();
|
|
256906
|
+
const bookerName = String(reqBody.bookerName || "").trim();
|
|
256907
|
+
const bookerPhone = String(reqBody.bookerPhone || "").trim();
|
|
256908
|
+
const bookerEmail = String(reqBody.bookerEmail || "").trim();
|
|
256909
|
+
const callSid = String(reqBody.callSid || "").trim();
|
|
256910
|
+
const bookedVia = String(reqBody.bookedVia || "phone").trim();
|
|
256911
|
+
const isAllDay = reqBody.isAllDay === true;
|
|
256912
|
+
const recurrenceTypeRaw = String(reqBody.recurrenceType || "").trim();
|
|
256913
|
+
const recurrenceType = VALID_RECURRENCE_TYPES.has(recurrenceTypeRaw) ? recurrenceTypeRaw : null;
|
|
256914
|
+
const recurrenceCount = recurrenceType && reqBody.recurrenceCount ? Math.max(1, Math.min(365, Number(reqBody.recurrenceCount) || 0)) : void 0;
|
|
256915
|
+
const recurrenceUntil = recurrenceType && typeof reqBody.recurrenceUntil === "string" ? reqBody.recurrenceUntil.trim() : void 0;
|
|
256916
|
+
if (!start) return respondJson2(res, 400, { ok: false, error: "start is required (ISO 8601)" }), true;
|
|
256917
|
+
if (!end) return respondJson2(res, 400, { ok: false, error: "end is required (ISO 8601)" }), true;
|
|
256918
|
+
if (!title) return respondJson2(res, 400, { ok: false, error: "title is required" }), true;
|
|
256919
|
+
const rrule = recurrenceType ? buildRrule(recurrenceType, recurrenceCount, recurrenceUntil) : null;
|
|
256920
|
+
const calendarId = resourceEmail || "primary";
|
|
256921
|
+
if (resourceEmail) {
|
|
256922
|
+
try {
|
|
256923
|
+
const fbBody = {
|
|
256924
|
+
timeMin: start,
|
|
256925
|
+
timeMax: end,
|
|
256926
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
256927
|
+
items: [{ id: calendarId }]
|
|
256928
|
+
};
|
|
256929
|
+
const fbRes = await gFetch2(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256930
|
+
method: "POST",
|
|
256931
|
+
headers: { "Content-Type": "application/json" },
|
|
256932
|
+
body: JSON.stringify(fbBody)
|
|
256933
|
+
});
|
|
256934
|
+
if (fbRes.ok) {
|
|
256935
|
+
const fbData = await fbRes.json();
|
|
256936
|
+
const fbCalendars = fbData.calendars || {};
|
|
256937
|
+
const busyPeriods = fbCalendars[calendarId]?.busy || [];
|
|
256938
|
+
if (busyPeriods.length > 0) {
|
|
256939
|
+
return respondJson2(res, 409, {
|
|
256940
|
+
ok: false,
|
|
256941
|
+
error: "slot_conflict",
|
|
256942
|
+
message: "The requested time slot is already booked. Please choose a different time.",
|
|
256943
|
+
conflictingPeriods: busyPeriods
|
|
256944
|
+
}), true;
|
|
256945
|
+
}
|
|
256946
|
+
} else {
|
|
256947
|
+
const fbErr = await fbRes.text().catch(() => "");
|
|
256948
|
+
console.warn(
|
|
256949
|
+
`FreeBusy pre-check failed (${fbRes.status}), proceeding without check: ${fbErr}`
|
|
256950
|
+
);
|
|
256951
|
+
}
|
|
256952
|
+
} catch (fbError) {
|
|
256953
|
+
console.warn(
|
|
256954
|
+
"FreeBusy pre-check threw an error, proceeding without check:",
|
|
256955
|
+
fbError instanceof Error ? fbError.message : String(fbError)
|
|
256956
|
+
);
|
|
256957
|
+
}
|
|
256958
|
+
}
|
|
256959
|
+
const eventBody = {
|
|
256960
|
+
summary: title,
|
|
256961
|
+
start: isAllDay ? { date: start } : { dateTime: start },
|
|
256962
|
+
end: isAllDay ? { date: end } : { dateTime: end }
|
|
256963
|
+
};
|
|
256964
|
+
if (description) eventBody.description = description;
|
|
256965
|
+
if (resourceEmail) {
|
|
256966
|
+
eventBody.attendees = [{ email: resourceEmail, resource: true }];
|
|
256967
|
+
}
|
|
256968
|
+
if (rrule) {
|
|
256969
|
+
eventBody.recurrence = [rrule];
|
|
256970
|
+
}
|
|
256971
|
+
const createRes = await gFetch2(
|
|
256972
|
+
token,
|
|
256973
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events`,
|
|
256974
|
+
{
|
|
256975
|
+
method: "POST",
|
|
256976
|
+
headers: { "Content-Type": "application/json" },
|
|
256977
|
+
body: JSON.stringify(eventBody)
|
|
256978
|
+
}
|
|
256979
|
+
);
|
|
256980
|
+
if (!createRes.ok) {
|
|
256981
|
+
const err = await createRes.text().catch(() => "");
|
|
256982
|
+
return respondJson2(res, 200, { ok: false, error: `Calendar create failed: ${createRes.status} ${err}` }), true;
|
|
256983
|
+
}
|
|
256984
|
+
const calEvent = await createRes.json();
|
|
256985
|
+
const calendarEventId = String(calEvent.id || "");
|
|
256986
|
+
let resourceName = "";
|
|
256987
|
+
if (resourceEmail) {
|
|
256988
|
+
try {
|
|
256989
|
+
const resources = await fetchResources(token);
|
|
256990
|
+
const match = resources.find((r2) => r2.resourceEmail === resourceEmail);
|
|
256991
|
+
if (match) resourceName = match.resourceName;
|
|
256992
|
+
} catch {
|
|
256993
|
+
}
|
|
256994
|
+
}
|
|
256995
|
+
const reservationId = generateReservationId();
|
|
256996
|
+
const nowIso40 = (/* @__PURE__ */ new Date()).toISOString();
|
|
256997
|
+
const reservation = {
|
|
256998
|
+
reservationId,
|
|
256999
|
+
calendarEventId,
|
|
257000
|
+
resourceCalendarId: resourceEmail,
|
|
257001
|
+
resourceName,
|
|
257002
|
+
startTime: start,
|
|
257003
|
+
endTime: end,
|
|
257004
|
+
title,
|
|
257005
|
+
isAllDay,
|
|
257006
|
+
status: "confirmed",
|
|
257007
|
+
bookerName,
|
|
257008
|
+
bookerPhone,
|
|
257009
|
+
bookerEmail,
|
|
257010
|
+
callSid,
|
|
257011
|
+
bookedVia,
|
|
257012
|
+
createdAt: nowIso40,
|
|
257013
|
+
updatedAt: nowIso40
|
|
257014
|
+
};
|
|
257015
|
+
if (rrule && recurrenceType) {
|
|
257016
|
+
reservation.isRecurring = true;
|
|
257017
|
+
reservation.recurrenceRule = rrule;
|
|
257018
|
+
reservation.recurrenceType = recurrenceType;
|
|
257019
|
+
reservation.recurrenceCount = recurrenceCount || null;
|
|
257020
|
+
reservation.recurrenceUntil = recurrenceUntil || null;
|
|
257021
|
+
}
|
|
257022
|
+
try {
|
|
257023
|
+
if (resourceEmail) {
|
|
257024
|
+
await db.runTransaction(async (transaction) => {
|
|
257025
|
+
const conflictQuery = db.collection("reservations").where("resourceCalendarId", "==", resourceEmail).where("status", "==", "confirmed").where("startTime", "<", end);
|
|
257026
|
+
const conflictSnap = await transaction.get(conflictQuery);
|
|
257027
|
+
const hasConflict = conflictSnap.docs.some((doc) => {
|
|
257028
|
+
const data = doc.data();
|
|
257029
|
+
return data.endTime > start;
|
|
257030
|
+
});
|
|
257031
|
+
if (hasConflict) {
|
|
257032
|
+
throw new Error("RESERVATION_CONFLICT");
|
|
257033
|
+
}
|
|
257034
|
+
transaction.set(
|
|
257035
|
+
db.collection("reservations").doc(reservationId),
|
|
257036
|
+
reservation
|
|
257037
|
+
);
|
|
257038
|
+
});
|
|
257039
|
+
} else {
|
|
257040
|
+
await db.collection("reservations").doc(reservationId).set(reservation);
|
|
257041
|
+
}
|
|
257042
|
+
} catch (err) {
|
|
257043
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257044
|
+
if (calendarEventId) {
|
|
257045
|
+
try {
|
|
257046
|
+
await gFetch2(
|
|
257047
|
+
token,
|
|
257048
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257049
|
+
{ method: "DELETE" }
|
|
257050
|
+
);
|
|
257051
|
+
} catch (rollbackErr) {
|
|
257052
|
+
console.warn(
|
|
257053
|
+
"Failed to rollback Calendar event after Firestore error:",
|
|
257054
|
+
rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
|
|
257055
|
+
);
|
|
257056
|
+
}
|
|
257057
|
+
}
|
|
257058
|
+
if (msg === "RESERVATION_CONFLICT") {
|
|
257059
|
+
return respondJson2(res, 409, {
|
|
257060
|
+
ok: false,
|
|
257061
|
+
error: "slot_conflict",
|
|
257062
|
+
message: "The requested time slot is already booked. Please choose a different time."
|
|
257063
|
+
}), true;
|
|
257064
|
+
}
|
|
257065
|
+
return respondJson2(res, 200, {
|
|
257066
|
+
ok: false,
|
|
257067
|
+
error: `Firestore write failed: ${msg}`
|
|
257068
|
+
}), true;
|
|
257069
|
+
}
|
|
257070
|
+
return respondJson2(res, 200, {
|
|
257071
|
+
ok: true,
|
|
257072
|
+
reservationId,
|
|
257073
|
+
calendarEventId: calEvent.id,
|
|
257074
|
+
resourceName,
|
|
257075
|
+
start,
|
|
257076
|
+
end,
|
|
257077
|
+
title,
|
|
257078
|
+
status: "confirmed",
|
|
257079
|
+
...rrule ? {
|
|
257080
|
+
isRecurring: true,
|
|
257081
|
+
recurrenceType,
|
|
257082
|
+
recurrenceRule: rrule
|
|
257083
|
+
} : {}
|
|
257084
|
+
}), true;
|
|
257085
|
+
}
|
|
257086
|
+
if (method === "GET" && /^\/api\/calendar\/reserve\/[^/]+\/instances$/.test(pathname)) {
|
|
257087
|
+
const parts = pathname.split("/");
|
|
257088
|
+
const reservationId = decodeURIComponent(parts[4]);
|
|
257089
|
+
const db = await getFirestore2();
|
|
257090
|
+
if (!db) return firestoreUnavailable(res);
|
|
257091
|
+
const docSnap = await db.collection("reservations").doc(reservationId).get();
|
|
257092
|
+
if (!docSnap.exists) {
|
|
257093
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257094
|
+
}
|
|
257095
|
+
const data = docSnap.data();
|
|
257096
|
+
if (!data.isRecurring || !data.calendarEventId) {
|
|
257097
|
+
return respondJson2(res, 400, { ok: false, error: "Not a recurring reservation" }), true;
|
|
257098
|
+
}
|
|
257099
|
+
const token = await getToken2();
|
|
257100
|
+
if (!token) return notConnected2(res);
|
|
257101
|
+
const calendarId = String(data.resourceCalendarId || "") || "primary";
|
|
257102
|
+
const eventId = String(data.calendarEventId);
|
|
257103
|
+
const instancesRes = await gFetch2(
|
|
257104
|
+
token,
|
|
257105
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}/instances?maxResults=50`
|
|
257106
|
+
);
|
|
257107
|
+
if (!instancesRes.ok) {
|
|
257108
|
+
const errText = await instancesRes.text().catch(() => "");
|
|
257109
|
+
return respondJson2(res, 502, {
|
|
257110
|
+
ok: false,
|
|
257111
|
+
error: `Failed to fetch instances: ${instancesRes.status}`,
|
|
257112
|
+
detail: errText
|
|
257113
|
+
}), true;
|
|
257114
|
+
}
|
|
257115
|
+
const instancesData = await instancesRes.json();
|
|
257116
|
+
const items = Array.isArray(instancesData.items) ? instancesData.items : [];
|
|
257117
|
+
const instances = items.map((item) => {
|
|
257118
|
+
const startObj = item.start;
|
|
257119
|
+
const endObj = item.end;
|
|
257120
|
+
return {
|
|
257121
|
+
eventId: String(item.id || ""),
|
|
257122
|
+
start: startObj?.dateTime || startObj?.date || "",
|
|
257123
|
+
end: endObj?.dateTime || endObj?.date || "",
|
|
257124
|
+
status: String(item.status || "confirmed")
|
|
257125
|
+
};
|
|
257126
|
+
});
|
|
257127
|
+
return respondJson2(res, 200, { ok: true, reservationId, instances }), true;
|
|
257128
|
+
}
|
|
257129
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257130
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257131
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257132
|
+
const reqBody = await parseJsonBody2(req);
|
|
257133
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
257134
|
+
const start = String(reqBody.start || "").trim();
|
|
257135
|
+
const end = String(reqBody.end || "").trim();
|
|
257136
|
+
const resourceEmail = reqBody.resourceEmail ? String(reqBody.resourceEmail).trim() : "";
|
|
257137
|
+
const isAllDay = reqBody.isAllDay !== void 0 ? reqBody.isAllDay === true : void 0;
|
|
257138
|
+
if (!start || !end) {
|
|
257139
|
+
return respondJson2(res, 400, { ok: false, error: "start and end are required" }), true;
|
|
257140
|
+
}
|
|
257141
|
+
const db = await getFirestore2();
|
|
257142
|
+
if (!db) return firestoreUnavailable(res);
|
|
257143
|
+
const docRef = db.collection("reservations").doc(reservationId);
|
|
257144
|
+
let docSnap = await docRef.get();
|
|
257145
|
+
if (!docSnap.exists && reservationId.startsWith("gcal-")) {
|
|
257146
|
+
const gcalEventId = reservationId.slice(5);
|
|
257147
|
+
const calendarIdForUpsert = resourceEmail || String(reqBody.calendarId || "").trim();
|
|
257148
|
+
if (!calendarIdForUpsert) {
|
|
257149
|
+
return respondJson2(res, 400, { ok: false, error: "calendarId is required for direct calendar bookings" }), true;
|
|
257150
|
+
}
|
|
257151
|
+
const upsertToken = await getToken2();
|
|
257152
|
+
if (!upsertToken) return notConnected2(res);
|
|
257153
|
+
const eventRes = await gFetch2(
|
|
257154
|
+
upsertToken,
|
|
257155
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarIdForUpsert)}/events/${encodeURIComponent(gcalEventId)}`
|
|
257156
|
+
);
|
|
257157
|
+
if (!eventRes.ok) {
|
|
257158
|
+
return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
|
|
257159
|
+
}
|
|
257160
|
+
const gcalEvent = await eventRes.json();
|
|
257161
|
+
const gcalStart = gcalEvent.start;
|
|
257162
|
+
const gcalEnd = gcalEvent.end;
|
|
257163
|
+
const creator = gcalEvent.creator;
|
|
257164
|
+
const isAllDay2 = Boolean(gcalStart?.date && !gcalStart?.dateTime);
|
|
257165
|
+
const upsertDoc = {
|
|
257166
|
+
reservationId,
|
|
257167
|
+
calendarEventId: gcalEventId,
|
|
257168
|
+
resourceCalendarId: calendarIdForUpsert,
|
|
257169
|
+
resourceName: String(gcalEvent.location || ""),
|
|
257170
|
+
startTime: gcalStart?.dateTime || gcalStart?.date || "",
|
|
257171
|
+
endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
|
|
257172
|
+
title: String(gcalEvent.summary || "Direct Calendar Booking"),
|
|
257173
|
+
isAllDay: isAllDay2,
|
|
257174
|
+
status: "confirmed",
|
|
257175
|
+
bookerName: creator?.displayName || creator?.email || "",
|
|
257176
|
+
bookerPhone: "",
|
|
257177
|
+
bookerEmail: creator?.email || "",
|
|
257178
|
+
bookedVia: "direct_calendar",
|
|
257179
|
+
callSid: "",
|
|
257180
|
+
createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
|
|
257181
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257182
|
+
};
|
|
257183
|
+
await docRef.set(upsertDoc);
|
|
257184
|
+
docSnap = await docRef.get();
|
|
257185
|
+
}
|
|
257186
|
+
if (!docSnap.exists) {
|
|
257187
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257188
|
+
}
|
|
257189
|
+
const data = docSnap.data();
|
|
257190
|
+
if (data.status !== "confirmed") {
|
|
257191
|
+
return respondJson2(res, 400, { ok: false, error: "Only confirmed reservations can be modified" }), true;
|
|
257192
|
+
}
|
|
257193
|
+
const token = await getToken2();
|
|
257194
|
+
if (!token) return notConnected2(res);
|
|
257195
|
+
const effectiveResource = resourceEmail || String(data.resourceCalendarId || "");
|
|
257196
|
+
const calendarId = effectiveResource || "primary";
|
|
257197
|
+
const calendarEventId = String(data.calendarEventId || "");
|
|
257198
|
+
if (effectiveResource) {
|
|
257199
|
+
try {
|
|
257200
|
+
const fbBody = {
|
|
257201
|
+
timeMin: start,
|
|
257202
|
+
timeMax: end,
|
|
257203
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
257204
|
+
items: [{ id: calendarId }]
|
|
257205
|
+
};
|
|
257206
|
+
const fbRes = await gFetch2(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
257207
|
+
method: "POST",
|
|
257208
|
+
headers: { "Content-Type": "application/json" },
|
|
257209
|
+
body: JSON.stringify(fbBody)
|
|
257210
|
+
});
|
|
257211
|
+
if (fbRes.ok) {
|
|
257212
|
+
const fbData = await fbRes.json();
|
|
257213
|
+
const fbCalendars = fbData.calendars || {};
|
|
257214
|
+
const busyPeriods = fbCalendars[calendarId]?.busy || [];
|
|
257215
|
+
const currentStart = String(data.startTime || "");
|
|
257216
|
+
const currentEnd = String(data.endTime || "");
|
|
257217
|
+
const externalConflicts = busyPeriods.filter((b) => {
|
|
257218
|
+
const bStart = new Date(b.start).getTime();
|
|
257219
|
+
const bEnd = new Date(b.end).getTime();
|
|
257220
|
+
const curStart = currentStart ? new Date(currentStart).getTime() : 0;
|
|
257221
|
+
const curEnd = currentEnd ? new Date(currentEnd).getTime() : 0;
|
|
257222
|
+
return !(bStart === curStart && bEnd === curEnd);
|
|
257223
|
+
});
|
|
257224
|
+
if (externalConflicts.length > 0) {
|
|
257225
|
+
return respondJson2(res, 409, {
|
|
257226
|
+
ok: false,
|
|
257227
|
+
error: "slot_conflict",
|
|
257228
|
+
message: "The requested time slot is already booked by another reservation. Please choose a different time.",
|
|
257229
|
+
conflictingPeriods: externalConflicts
|
|
257230
|
+
}), true;
|
|
257231
|
+
}
|
|
257232
|
+
} else {
|
|
257233
|
+
const fbErr = await fbRes.text().catch(() => "");
|
|
257234
|
+
console.warn(
|
|
257235
|
+
`FreeBusy pre-check for modify failed (${fbRes.status}), proceeding: ${fbErr}`
|
|
257236
|
+
);
|
|
257237
|
+
}
|
|
257238
|
+
} catch (fbError) {
|
|
257239
|
+
console.warn(
|
|
257240
|
+
"FreeBusy pre-check for modify threw an error, proceeding:",
|
|
257241
|
+
fbError instanceof Error ? fbError.message : String(fbError)
|
|
257242
|
+
);
|
|
257243
|
+
}
|
|
257244
|
+
}
|
|
257245
|
+
if (calendarEventId) {
|
|
257246
|
+
const effectiveIsAllDay = isAllDay !== void 0 ? isAllDay : Boolean(data.isAllDay);
|
|
257247
|
+
const patchBody = {};
|
|
257248
|
+
if (start) patchBody.start = effectiveIsAllDay ? { date: start, dateTime: null } : { dateTime: start, date: null };
|
|
257249
|
+
if (end) patchBody.end = effectiveIsAllDay ? { date: end, dateTime: null } : { dateTime: end, date: null };
|
|
257250
|
+
if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
|
|
257251
|
+
patchBody.attendees = [{ email: resourceEmail, resource: true }];
|
|
257252
|
+
}
|
|
257253
|
+
const patchRes = await gFetch2(
|
|
257254
|
+
token,
|
|
257255
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257256
|
+
{ method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patchBody) }
|
|
257257
|
+
);
|
|
257258
|
+
if (!patchRes.ok) {
|
|
257259
|
+
const errText = await patchRes.text().catch(() => "");
|
|
257260
|
+
return respondJson2(res, 502, { ok: false, error: `Calendar update failed: ${patchRes.status}`, detail: errText }), true;
|
|
257261
|
+
}
|
|
257262
|
+
}
|
|
257263
|
+
const updateFields = {
|
|
257264
|
+
startTime: start,
|
|
257265
|
+
endTime: end,
|
|
257266
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257267
|
+
};
|
|
257268
|
+
if (isAllDay !== void 0) {
|
|
257269
|
+
updateFields.isAllDay = isAllDay;
|
|
257270
|
+
}
|
|
257271
|
+
if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
|
|
257272
|
+
updateFields.resourceCalendarId = resourceEmail;
|
|
257273
|
+
try {
|
|
257274
|
+
const resources = await fetchResources(token);
|
|
257275
|
+
const matched = resources.find((r2) => r2.resourceEmail === resourceEmail);
|
|
257276
|
+
if (matched) updateFields.resourceName = matched.resourceName || resourceEmail;
|
|
257277
|
+
} catch {
|
|
257278
|
+
}
|
|
257279
|
+
}
|
|
257280
|
+
await docRef.update(updateFields);
|
|
257281
|
+
return respondJson2(res, 200, {
|
|
257282
|
+
ok: true,
|
|
257283
|
+
reservationId,
|
|
257284
|
+
start,
|
|
257285
|
+
end,
|
|
257286
|
+
resourceName: String(updateFields.resourceName || data.resourceName || ""),
|
|
257287
|
+
status: "confirmed"
|
|
257288
|
+
}), true;
|
|
257289
|
+
}
|
|
257290
|
+
if (method === "DELETE" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257291
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257292
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257293
|
+
const db = await getFirestore2();
|
|
257294
|
+
if (!db) return firestoreUnavailable(res);
|
|
257295
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
257296
|
+
const docRef = db.collection("reservations").doc(reservationId);
|
|
257297
|
+
let docSnap = await docRef.get();
|
|
257298
|
+
if (!docSnap.exists && reservationId.startsWith("gcal-")) {
|
|
257299
|
+
const gcalEventId = reservationId.slice(5);
|
|
257300
|
+
const gcalCalendarId = url.searchParams.get("calendarId") || "";
|
|
257301
|
+
if (!gcalCalendarId) {
|
|
257302
|
+
return respondJson2(res, 400, { ok: false, error: "calendarId query param is required for direct calendar bookings" }), true;
|
|
257303
|
+
}
|
|
257304
|
+
const upsertToken = await getToken2();
|
|
257305
|
+
if (!upsertToken) return notConnected2(res);
|
|
257306
|
+
const eventRes = await gFetch2(
|
|
257307
|
+
upsertToken,
|
|
257308
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(gcalCalendarId)}/events/${encodeURIComponent(gcalEventId)}`
|
|
257309
|
+
);
|
|
257310
|
+
if (!eventRes.ok) {
|
|
257311
|
+
return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
|
|
257312
|
+
}
|
|
257313
|
+
const gcalEvent = await eventRes.json();
|
|
257314
|
+
const gcalStart = gcalEvent.start;
|
|
257315
|
+
const gcalEnd = gcalEvent.end;
|
|
257316
|
+
const creator = gcalEvent.creator;
|
|
257317
|
+
const isAllDay = Boolean(gcalStart?.date && !gcalStart?.dateTime);
|
|
257318
|
+
const upsertDoc = {
|
|
257319
|
+
reservationId,
|
|
257320
|
+
calendarEventId: gcalEventId,
|
|
257321
|
+
resourceCalendarId: gcalCalendarId,
|
|
257322
|
+
resourceName: String(gcalEvent.location || ""),
|
|
257323
|
+
startTime: gcalStart?.dateTime || gcalStart?.date || "",
|
|
257324
|
+
endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
|
|
257325
|
+
title: String(gcalEvent.summary || "Direct Calendar Booking"),
|
|
257326
|
+
isAllDay,
|
|
257327
|
+
status: "confirmed",
|
|
257328
|
+
bookerName: creator?.displayName || creator?.email || "",
|
|
257329
|
+
bookerPhone: "",
|
|
257330
|
+
bookerEmail: creator?.email || "",
|
|
257331
|
+
bookedVia: "direct_calendar",
|
|
257332
|
+
callSid: "",
|
|
257333
|
+
createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
|
|
257334
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257335
|
+
};
|
|
257336
|
+
await docRef.set(upsertDoc);
|
|
257337
|
+
docSnap = await docRef.get();
|
|
257338
|
+
}
|
|
257339
|
+
if (!docSnap.exists) {
|
|
257340
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257341
|
+
}
|
|
257342
|
+
const data = docSnap.data();
|
|
257343
|
+
const scope = url.searchParams.get("scope") || "all";
|
|
257344
|
+
const instanceEventId = url.searchParams.get("instanceEventId") || "";
|
|
257345
|
+
const calendarEventId = String(data.calendarEventId || "");
|
|
257346
|
+
const calendarId = String(data.resourceCalendarId || "") || "primary";
|
|
257347
|
+
const isRecurring = Boolean(data.isRecurring);
|
|
257348
|
+
const token = await getToken2();
|
|
257349
|
+
if (isRecurring && scope === "single") {
|
|
257350
|
+
if (!instanceEventId) {
|
|
257351
|
+
return respondJson2(res, 400, {
|
|
257352
|
+
ok: false,
|
|
257353
|
+
error: "instanceEventId is required when scope=single"
|
|
257354
|
+
}), true;
|
|
257355
|
+
}
|
|
257356
|
+
if (token) {
|
|
257357
|
+
try {
|
|
257358
|
+
await gFetch2(
|
|
257359
|
+
token,
|
|
257360
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`,
|
|
257361
|
+
{ method: "DELETE" }
|
|
257362
|
+
);
|
|
257363
|
+
} catch {
|
|
257364
|
+
}
|
|
257365
|
+
}
|
|
257366
|
+
return respondJson2(res, 200, {
|
|
257367
|
+
ok: true,
|
|
257368
|
+
reservationId,
|
|
257369
|
+
cancelledInstanceEventId: instanceEventId,
|
|
257370
|
+
scope: "single",
|
|
257371
|
+
status: "instance_cancelled"
|
|
257372
|
+
}), true;
|
|
257373
|
+
} else if (isRecurring && scope === "this_and_following") {
|
|
257374
|
+
if (!instanceEventId) {
|
|
257375
|
+
return respondJson2(res, 400, {
|
|
257376
|
+
ok: false,
|
|
257377
|
+
error: "instanceEventId is required when scope=this_and_following to determine the cut-off date"
|
|
257378
|
+
}), true;
|
|
257379
|
+
}
|
|
257380
|
+
if (token && calendarEventId) {
|
|
257381
|
+
try {
|
|
257382
|
+
const instanceRes = await gFetch2(
|
|
257383
|
+
token,
|
|
257384
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`
|
|
257385
|
+
);
|
|
257386
|
+
if (instanceRes.ok) {
|
|
257387
|
+
const instanceData = await instanceRes.json();
|
|
257388
|
+
const instanceStart = instanceData.start?.dateTime || "";
|
|
257389
|
+
if (instanceStart) {
|
|
257390
|
+
const cutoffDate = new Date(instanceStart);
|
|
257391
|
+
cutoffDate.setDate(cutoffDate.getDate() - 1);
|
|
257392
|
+
const untilStr = cutoffDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
|
257393
|
+
const parentRes = await gFetch2(
|
|
257394
|
+
token,
|
|
257395
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`
|
|
257396
|
+
);
|
|
257397
|
+
if (parentRes.ok) {
|
|
257398
|
+
const parentData = await parentRes.json();
|
|
257399
|
+
const existingRecurrence = Array.isArray(parentData.recurrence) ? parentData.recurrence : [];
|
|
257400
|
+
const updatedRecurrence = existingRecurrence.map((rule) => {
|
|
257401
|
+
if (!rule.startsWith("RRULE:")) return rule;
|
|
257402
|
+
const parts = rule.split(";").filter(
|
|
257403
|
+
(p) => !p.startsWith("COUNT=") && !p.startsWith("UNTIL=")
|
|
257404
|
+
);
|
|
257405
|
+
return [...parts, `UNTIL=${untilStr}`].join(";");
|
|
257406
|
+
});
|
|
257407
|
+
await gFetch2(
|
|
257408
|
+
token,
|
|
257409
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257410
|
+
{
|
|
257411
|
+
method: "PATCH",
|
|
257412
|
+
headers: { "Content-Type": "application/json" },
|
|
257413
|
+
body: JSON.stringify({ recurrence: updatedRecurrence })
|
|
257414
|
+
}
|
|
257415
|
+
);
|
|
257416
|
+
}
|
|
257417
|
+
}
|
|
257418
|
+
}
|
|
257419
|
+
} catch (err) {
|
|
257420
|
+
console.warn(
|
|
257421
|
+
"Failed to update RRULE for this_and_following cancel:",
|
|
257422
|
+
err instanceof Error ? err.message : String(err)
|
|
257423
|
+
);
|
|
257424
|
+
}
|
|
257425
|
+
}
|
|
257426
|
+
await docRef.update({
|
|
257427
|
+
recurrenceUntil: instanceEventId,
|
|
257428
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257429
|
+
});
|
|
257430
|
+
return respondJson2(res, 200, {
|
|
257431
|
+
ok: true,
|
|
257432
|
+
reservationId,
|
|
257433
|
+
scope: "this_and_following",
|
|
257434
|
+
status: "truncated"
|
|
257435
|
+
}), true;
|
|
257436
|
+
} else {
|
|
257437
|
+
if (calendarEventId && token) {
|
|
257438
|
+
try {
|
|
257439
|
+
await gFetch2(
|
|
257440
|
+
token,
|
|
257441
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257442
|
+
{ method: "DELETE" }
|
|
257443
|
+
);
|
|
257444
|
+
} catch {
|
|
257445
|
+
}
|
|
257446
|
+
}
|
|
257447
|
+
await docRef.update({
|
|
257448
|
+
status: "cancelled",
|
|
257449
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257450
|
+
});
|
|
257451
|
+
return respondJson2(res, 200, {
|
|
257452
|
+
ok: true,
|
|
257453
|
+
reservationId,
|
|
257454
|
+
scope: "all",
|
|
257455
|
+
status: "cancelled"
|
|
257456
|
+
}), true;
|
|
257457
|
+
}
|
|
257458
|
+
}
|
|
257459
|
+
if (method === "GET" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257460
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257461
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257462
|
+
const db = await getFirestore2();
|
|
257463
|
+
if (!db) return firestoreUnavailable(res);
|
|
257464
|
+
const docSnap = await db.collection("reservations").doc(reservationId).get();
|
|
257465
|
+
if (!docSnap.exists) {
|
|
257466
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257467
|
+
}
|
|
257468
|
+
return respondJson2(res, 200, { ok: true, reservation: docSnap.data() }), true;
|
|
257469
|
+
}
|
|
257470
|
+
if (method === "GET" && pathname === "/api/calendar/reservations") {
|
|
257471
|
+
const db = await getFirestore2();
|
|
257472
|
+
if (!db) return firestoreUnavailable(res);
|
|
257473
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
257474
|
+
const status = url.searchParams.get("status") || "";
|
|
257475
|
+
const date = url.searchParams.get("date") || "";
|
|
257476
|
+
const resourceEmail = url.searchParams.get("resourceEmail") || "";
|
|
257477
|
+
const buildingId = url.searchParams.get("buildingId") || "";
|
|
257478
|
+
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") || DEFAULT_RESERVATION_LIMIT) || DEFAULT_RESERVATION_LIMIT));
|
|
257479
|
+
let query = db.collection("reservations");
|
|
257480
|
+
if (status) {
|
|
257481
|
+
query = query.where("status", "==", status);
|
|
257482
|
+
}
|
|
257483
|
+
if (resourceEmail) {
|
|
257484
|
+
query = query.where("resourceCalendarId", "==", resourceEmail);
|
|
257485
|
+
}
|
|
257486
|
+
if (date) {
|
|
257487
|
+
const dayStart = `${date}T00:00:00`;
|
|
257488
|
+
const dayEnd = `${date}T23:59:59`;
|
|
257489
|
+
query = query.where("startTime", ">=", dayStart).where("startTime", "<=", dayEnd + "Z");
|
|
257490
|
+
}
|
|
257491
|
+
query = query.orderBy("createdAt", "desc").limit(limit);
|
|
257492
|
+
let firestoreDocs;
|
|
257493
|
+
try {
|
|
257494
|
+
const snap = await query.get();
|
|
257495
|
+
firestoreDocs = snap.docs;
|
|
257496
|
+
} catch (err) {
|
|
257497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257498
|
+
return respondJson2(res, 200, { ok: false, error: `Firestore query failed: ${msg}` }), true;
|
|
257499
|
+
}
|
|
257500
|
+
const token = await getToken2();
|
|
257501
|
+
if (!token) {
|
|
257502
|
+
if (firestoreDocs.length === 0) {
|
|
257503
|
+
return respondJson2(res, 200, { ok: true, reservations: [], count: 0, dataSource: "firestore_fallback" }), true;
|
|
257504
|
+
}
|
|
257505
|
+
const reservations = firestoreDocs.map((doc) => doc.data());
|
|
257506
|
+
return respondJson2(res, 200, {
|
|
257507
|
+
ok: true,
|
|
257508
|
+
reservations,
|
|
257509
|
+
count: reservations.length,
|
|
257510
|
+
dataSource: "firestore_fallback"
|
|
257511
|
+
}), true;
|
|
257512
|
+
}
|
|
257513
|
+
const docsWithEventId = [];
|
|
257514
|
+
const docsWithoutEventId = [];
|
|
257515
|
+
for (const doc of firestoreDocs) {
|
|
257516
|
+
const data = doc.data();
|
|
257517
|
+
if (data.calendarEventId) {
|
|
257518
|
+
docsWithEventId.push(data);
|
|
257519
|
+
} else {
|
|
257520
|
+
docsWithoutEventId.push(data);
|
|
257521
|
+
}
|
|
257522
|
+
}
|
|
257523
|
+
const byCalendar = /* @__PURE__ */ new Map();
|
|
257524
|
+
for (const doc of docsWithEventId) {
|
|
257525
|
+
const calId = doc.resourceCalendarId || "primary";
|
|
257526
|
+
const group = byCalendar.get(calId);
|
|
257527
|
+
if (group) {
|
|
257528
|
+
group.push(doc);
|
|
257529
|
+
} else {
|
|
257530
|
+
byCalendar.set(calId, [doc]);
|
|
257531
|
+
}
|
|
257532
|
+
}
|
|
257533
|
+
const additionalCalendarIds = [];
|
|
257534
|
+
if (resourceEmail && !byCalendar.has(resourceEmail)) {
|
|
257535
|
+
additionalCalendarIds.push(resourceEmail);
|
|
257536
|
+
}
|
|
257537
|
+
if (!resourceEmail) {
|
|
257538
|
+
try {
|
|
257539
|
+
const resources = await fetchResources(token, buildingId || void 0);
|
|
257540
|
+
for (const r2 of resources) {
|
|
257541
|
+
if (r2.resourceEmail && !byCalendar.has(r2.resourceEmail)) {
|
|
257542
|
+
additionalCalendarIds.push(r2.resourceEmail);
|
|
257543
|
+
}
|
|
257544
|
+
}
|
|
257545
|
+
} catch (err) {
|
|
257546
|
+
console.warn(
|
|
257547
|
+
"Failed to fetch resource calendars from Admin SDK:",
|
|
257548
|
+
err instanceof Error ? err.message : String(err)
|
|
257549
|
+
);
|
|
257550
|
+
}
|
|
257551
|
+
}
|
|
257552
|
+
for (const calId of additionalCalendarIds) {
|
|
257553
|
+
if (!byCalendar.has(calId)) {
|
|
257554
|
+
byCalendar.set(calId, []);
|
|
257555
|
+
}
|
|
257556
|
+
}
|
|
257557
|
+
let timeMin;
|
|
257558
|
+
let timeMax;
|
|
257559
|
+
if (date) {
|
|
257560
|
+
timeMin = `${date}T00:00:00+09:00`;
|
|
257561
|
+
timeMax = `${date}T23:59:59+09:00`;
|
|
257562
|
+
} else {
|
|
257563
|
+
const startTimes = docsWithEventId.map((d) => d.startTime).filter(Boolean).sort();
|
|
257564
|
+
if (startTimes.length > 0) {
|
|
257565
|
+
const earliest = new Date(startTimes[0]);
|
|
257566
|
+
earliest.setDate(earliest.getDate() - 1);
|
|
257567
|
+
const latest = new Date(startTimes[startTimes.length - 1]);
|
|
257568
|
+
latest.setDate(latest.getDate() + 1);
|
|
257569
|
+
timeMin = earliest.toISOString();
|
|
257570
|
+
timeMax = latest.toISOString();
|
|
257571
|
+
} else {
|
|
257572
|
+
const now = /* @__PURE__ */ new Date();
|
|
257573
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
257574
|
+
const thirtyDaysAhead = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1e3);
|
|
257575
|
+
timeMin = thirtyDaysAgo.toISOString();
|
|
257576
|
+
timeMax = thirtyDaysAhead.toISOString();
|
|
257577
|
+
}
|
|
257578
|
+
}
|
|
257579
|
+
const gcalEventMap = /* @__PURE__ */ new Map();
|
|
257580
|
+
let gcalFetchFailed = false;
|
|
257581
|
+
try {
|
|
257582
|
+
const calendarFetches = Array.from(byCalendar.keys()).map(async (calendarId) => {
|
|
257583
|
+
const eventsUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?timeMin=${encodeURIComponent(timeMin)}&timeMax=${encodeURIComponent(timeMax)}&singleEvents=true&orderBy=startTime&maxResults=250`;
|
|
257584
|
+
const eventsRes = await gFetch2(token, eventsUrl);
|
|
257585
|
+
if (!eventsRes.ok) {
|
|
257586
|
+
console.warn(
|
|
257587
|
+
`GCal events.list failed for calendar ${calendarId}: ${eventsRes.status}`
|
|
257588
|
+
);
|
|
257589
|
+
return;
|
|
257590
|
+
}
|
|
257591
|
+
const eventsData = await eventsRes.json();
|
|
257592
|
+
const items = Array.isArray(eventsData.items) ? eventsData.items : [];
|
|
257593
|
+
for (const item of items) {
|
|
257594
|
+
const eventId = String(item.id || "");
|
|
257595
|
+
if (!eventId) continue;
|
|
257596
|
+
const startObj = item.start;
|
|
257597
|
+
const endObj = item.end;
|
|
257598
|
+
const isAllDay = Boolean(startObj?.date && !startObj?.dateTime);
|
|
257599
|
+
const eventStart = startObj?.dateTime || startObj?.date || "";
|
|
257600
|
+
const eventEnd = endObj?.dateTime || endObj?.date || "";
|
|
257601
|
+
const creatorObj = item.creator;
|
|
257602
|
+
const organizerObj = item.organizer;
|
|
257603
|
+
gcalEventMap.set(eventId, {
|
|
257604
|
+
startTime: eventStart,
|
|
257605
|
+
endTime: eventEnd,
|
|
257606
|
+
title: String(item.summary || ""),
|
|
257607
|
+
isAllDay,
|
|
257608
|
+
gcalStatus: String(item.status || "confirmed"),
|
|
257609
|
+
creatorEmail: creatorObj?.email || "",
|
|
257610
|
+
creatorDisplayName: creatorObj?.displayName || "",
|
|
257611
|
+
organizerEmail: organizerObj?.email || "",
|
|
257612
|
+
organizerDisplayName: organizerObj?.displayName || "",
|
|
257613
|
+
location: String(item.location || ""),
|
|
257614
|
+
created: String(item.created || ""),
|
|
257615
|
+
updated: String(item.updated || ""),
|
|
257616
|
+
calendarId
|
|
257617
|
+
});
|
|
257618
|
+
}
|
|
257619
|
+
});
|
|
257620
|
+
await Promise.all(calendarFetches);
|
|
257621
|
+
} catch (err) {
|
|
257622
|
+
console.warn(
|
|
257623
|
+
"GCal fetch failed, falling back to Firestore:",
|
|
257624
|
+
err instanceof Error ? err.message : String(err)
|
|
257625
|
+
);
|
|
257626
|
+
gcalFetchFailed = true;
|
|
257627
|
+
}
|
|
257628
|
+
if (gcalFetchFailed) {
|
|
257629
|
+
const reservations = firestoreDocs.map((doc) => doc.data());
|
|
257630
|
+
return respondJson2(res, 200, {
|
|
257631
|
+
ok: true,
|
|
257632
|
+
reservations,
|
|
257633
|
+
count: reservations.length,
|
|
257634
|
+
dataSource: "firestore_fallback"
|
|
257635
|
+
}), true;
|
|
257636
|
+
}
|
|
257637
|
+
const mergedReservations = [];
|
|
257638
|
+
for (const fsDoc of docsWithEventId) {
|
|
257639
|
+
const gcalEvent = gcalEventMap.get(fsDoc.calendarEventId);
|
|
257640
|
+
if (gcalEvent) {
|
|
257641
|
+
const effectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : fsDoc.status;
|
|
257642
|
+
mergedReservations.push({
|
|
257643
|
+
reservationId: fsDoc.reservationId,
|
|
257644
|
+
calendarEventId: fsDoc.calendarEventId,
|
|
257645
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257646
|
+
resourceName: fsDoc.resourceName,
|
|
257647
|
+
startTime: gcalEvent.startTime,
|
|
257648
|
+
// from GCal
|
|
257649
|
+
endTime: gcalEvent.endTime,
|
|
257650
|
+
// from GCal
|
|
257651
|
+
title: gcalEvent.title,
|
|
257652
|
+
// from GCal
|
|
257653
|
+
isAllDay: gcalEvent.isAllDay,
|
|
257654
|
+
status: effectiveStatus,
|
|
257655
|
+
bookerName: fsDoc.bookerName,
|
|
257656
|
+
// from Firestore
|
|
257657
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257658
|
+
// from Firestore
|
|
257659
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257660
|
+
// from Firestore
|
|
257661
|
+
callSid: fsDoc.callSid,
|
|
257662
|
+
// from Firestore
|
|
257663
|
+
bookedVia: fsDoc.bookedVia,
|
|
257664
|
+
// from Firestore
|
|
257665
|
+
createdAt: fsDoc.createdAt,
|
|
257666
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257667
|
+
dataSource: "google_calendar"
|
|
257668
|
+
});
|
|
257669
|
+
} else {
|
|
257670
|
+
mergedReservations.push({
|
|
257671
|
+
reservationId: fsDoc.reservationId,
|
|
257672
|
+
calendarEventId: fsDoc.calendarEventId,
|
|
257673
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257674
|
+
resourceName: fsDoc.resourceName,
|
|
257675
|
+
startTime: fsDoc.startTime,
|
|
257676
|
+
// from Firestore (stale)
|
|
257677
|
+
endTime: fsDoc.endTime,
|
|
257678
|
+
// from Firestore (stale)
|
|
257679
|
+
title: fsDoc.title,
|
|
257680
|
+
// from Firestore (stale)
|
|
257681
|
+
isAllDay: false,
|
|
257682
|
+
status: fsDoc.status === "confirmed" ? "cancelled" : fsDoc.status,
|
|
257683
|
+
bookerName: fsDoc.bookerName,
|
|
257684
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257685
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257686
|
+
callSid: fsDoc.callSid,
|
|
257687
|
+
bookedVia: fsDoc.bookedVia,
|
|
257688
|
+
createdAt: fsDoc.createdAt,
|
|
257689
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257690
|
+
dataSource: "stale"
|
|
257691
|
+
});
|
|
257692
|
+
}
|
|
257693
|
+
}
|
|
257694
|
+
for (const fsDoc of docsWithoutEventId) {
|
|
257695
|
+
mergedReservations.push({
|
|
257696
|
+
reservationId: fsDoc.reservationId,
|
|
257697
|
+
calendarEventId: "",
|
|
257698
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257699
|
+
resourceName: fsDoc.resourceName,
|
|
257700
|
+
startTime: fsDoc.startTime,
|
|
257701
|
+
endTime: fsDoc.endTime,
|
|
257702
|
+
title: fsDoc.title,
|
|
257703
|
+
isAllDay: false,
|
|
257704
|
+
status: fsDoc.status,
|
|
257705
|
+
bookerName: fsDoc.bookerName,
|
|
257706
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257707
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257708
|
+
callSid: fsDoc.callSid,
|
|
257709
|
+
bookedVia: fsDoc.bookedVia,
|
|
257710
|
+
createdAt: fsDoc.createdAt,
|
|
257711
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257712
|
+
dataSource: "firestore_fallback"
|
|
257713
|
+
});
|
|
257714
|
+
}
|
|
257715
|
+
const matchedEventIds = new Set(docsWithEventId.map((d) => d.calendarEventId));
|
|
257716
|
+
for (const [eventId, gcalEvent] of gcalEventMap) {
|
|
257717
|
+
if (matchedEventIds.has(eventId)) continue;
|
|
257718
|
+
if (status) {
|
|
257719
|
+
const gcalEffectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed";
|
|
257720
|
+
if (gcalEffectiveStatus !== status) continue;
|
|
257721
|
+
}
|
|
257722
|
+
mergedReservations.push({
|
|
257723
|
+
reservationId: `gcal-${eventId}`,
|
|
257724
|
+
calendarEventId: eventId,
|
|
257725
|
+
resourceCalendarId: gcalEvent.calendarId,
|
|
257726
|
+
resourceName: gcalEvent.location || gcalEvent.organizerDisplayName || "",
|
|
257727
|
+
startTime: gcalEvent.startTime,
|
|
257728
|
+
endTime: gcalEvent.endTime,
|
|
257729
|
+
title: gcalEvent.title || "Direct Calendar Booking",
|
|
257730
|
+
isAllDay: gcalEvent.isAllDay,
|
|
257731
|
+
status: gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed",
|
|
257732
|
+
bookerName: gcalEvent.creatorDisplayName || gcalEvent.creatorEmail || "",
|
|
257733
|
+
bookerPhone: "",
|
|
257734
|
+
bookerEmail: gcalEvent.creatorEmail,
|
|
257735
|
+
callSid: "",
|
|
257736
|
+
bookedVia: "direct_calendar",
|
|
257737
|
+
createdAt: gcalEvent.created,
|
|
257738
|
+
updatedAt: gcalEvent.updated,
|
|
257739
|
+
dataSource: "google_calendar_only"
|
|
257740
|
+
});
|
|
257741
|
+
}
|
|
257742
|
+
return respondJson2(res, 200, {
|
|
257743
|
+
ok: true,
|
|
257744
|
+
reservations: mergedReservations,
|
|
257745
|
+
count: mergedReservations.length,
|
|
257746
|
+
dataSource: "google_calendar"
|
|
257747
|
+
}), true;
|
|
257748
|
+
}
|
|
257749
|
+
return false;
|
|
257750
|
+
}
|
|
257751
|
+
var ADMIN_RESOURCES_BASE, CALENDAR_API_BASE, DEFAULT_WORK_START_HOUR, DEFAULT_WORK_END_HOUR, DEFAULT_DURATION_MINUTES, DEFAULT_RESERVATION_LIMIT, SLOT_STEP_MS, DEFAULT_BUFFER_MINUTES, TOP_SLOT_COUNT, DEFAULT_TIMEZONE, _firestoreInstance2, _firestoreInitPromise2, VALID_RECURRENCE_TYPES;
|
|
257752
|
+
var init_calendar_api_routes = __esm({
|
|
257753
|
+
"services/desktop/routes/calendar-api-routes.ts"() {
|
|
257754
|
+
init_desktop_server_helpers();
|
|
257755
|
+
init_google_oauth();
|
|
257756
|
+
init_date_tz();
|
|
257757
|
+
ADMIN_RESOURCES_BASE = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
|
|
257758
|
+
CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
|
|
257759
|
+
DEFAULT_WORK_START_HOUR = 9;
|
|
257760
|
+
DEFAULT_WORK_END_HOUR = 18;
|
|
257761
|
+
DEFAULT_DURATION_MINUTES = 60;
|
|
257762
|
+
DEFAULT_RESERVATION_LIMIT = 50;
|
|
257763
|
+
SLOT_STEP_MS = 5 * 60 * 1e3;
|
|
257764
|
+
DEFAULT_BUFFER_MINUTES = 10;
|
|
257765
|
+
TOP_SLOT_COUNT = 3;
|
|
257766
|
+
DEFAULT_TIMEZONE = "Asia/Tokyo";
|
|
257767
|
+
_firestoreInstance2 = null;
|
|
257768
|
+
_firestoreInitPromise2 = null;
|
|
257769
|
+
VALID_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["daily", "weekly", "biweekly", "monthly"]);
|
|
257770
|
+
}
|
|
257771
|
+
});
|
|
256285
257772
|
function resolveMaxConcurrency() {
|
|
256286
257773
|
const envRaw = String(process.env.MARIA_DESKTOP_MAX_CONCURRENCY || "").trim();
|
|
256287
257774
|
if (envRaw) {
|
|
@@ -257435,6 +258922,10 @@ function createDesktopRequestHandler(s2) {
|
|
|
257435
258922
|
const handled = await handlePhoneRoute(method, pathname, req, res);
|
|
257436
258923
|
if (handled) return;
|
|
257437
258924
|
}
|
|
258925
|
+
if (pathname.startsWith("/api/calendar/")) {
|
|
258926
|
+
const handled = await handleCalendarApiRoute(method, pathname, req, res);
|
|
258927
|
+
if (handled) return;
|
|
258928
|
+
}
|
|
257438
258929
|
{
|
|
257439
258930
|
const handled = await handleGuiCompatRoute(method, pathname, url, req, res, cwd, s2);
|
|
257440
258931
|
if (handled) return;
|
|
@@ -257649,6 +259140,7 @@ var init_desktop_server = __esm({
|
|
|
257649
259140
|
init_google_oauth_routes();
|
|
257650
259141
|
init_google_api_routes();
|
|
257651
259142
|
init_phone_routes();
|
|
259143
|
+
init_calendar_api_routes();
|
|
257652
259144
|
init_desktop_server_helpers();
|
|
257653
259145
|
init_desktop_data_readers();
|
|
257654
259146
|
init_desktop_job_queue();
|
|
@@ -257991,7 +259483,7 @@ function createSpreadsheetField() {
|
|
|
257991
259483
|
const wantsJson = hasLiteFlag(ctx.parsed, "json");
|
|
257992
259484
|
const titleOpt = typeof ctx.parsed.options.title === "string" ? String(ctx.parsed.options.title).trim() : "";
|
|
257993
259485
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
257994
|
-
const outBaseRel = (outBase || "spreadsheet").replace(/\\/g, "/");
|
|
259486
|
+
const outBaseRel = (outBase || ".maria/desktop/spreadsheet").replace(/\\/g, "/");
|
|
257995
259487
|
const sheetsN = parseSheetsCount(ctx.parsed.options.sheets, 1);
|
|
257996
259488
|
const stamp = nowStamp3();
|
|
257997
259489
|
const outDirRel = path87__namespace.posix.join(
|
|
@@ -259658,7 +261150,7 @@ function createPaperField() {
|
|
|
259658
261150
|
const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : 6;
|
|
259659
261151
|
return Number.isFinite(n) ? Math.max(3, Math.min(20, Math.floor(n))) : 6;
|
|
259660
261152
|
})();
|
|
259661
|
-
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "paper").replace(/\\/g, "/") || "paper";
|
|
261153
|
+
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/paper").replace(/\\/g, "/") || ".maria/desktop/paper";
|
|
259662
261154
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
259663
261155
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
259664
261156
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
@@ -260163,7 +261655,7 @@ function createExamField() {
|
|
|
260163
261655
|
const inlineArgs = ctx.parsed.args.join(" ").trim();
|
|
260164
261656
|
const langRaw = typeof ctx.parsed.options.lang === "string" ? String(ctx.parsed.options.lang).trim().toLowerCase() : "";
|
|
260165
261657
|
const langExplicit = langRaw === "ja" || langRaw === "jp" ? "ja" : langRaw === "en" ? "en" : "";
|
|
260166
|
-
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "exam").replace(/\\/g, "/") || "exam";
|
|
261658
|
+
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/exam").replace(/\\/g, "/") || ".maria/desktop/exam";
|
|
260167
261659
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
260168
261660
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
260169
261661
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
@@ -262207,8 +263699,27 @@ async function queryFreeBusy(token, emails, from, to, signal) {
|
|
|
262207
263699
|
}
|
|
262208
263700
|
async function findMeetingSlots(token, required, optional, durationMin, dateFrom, dateTo, workStart, workEnd, signal, extra) {
|
|
262209
263701
|
let resourceCalendarIds = [];
|
|
263702
|
+
const resourceNameMap = {};
|
|
262210
263703
|
if (extra?.resourceId) {
|
|
262211
263704
|
resourceCalendarIds = [extra.resourceId];
|
|
263705
|
+
try {
|
|
263706
|
+
const adminBaseUrl = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources/calendars";
|
|
263707
|
+
const qParams = new URLSearchParams({ maxResults: "5", query: `resourceEmail="${extra.resourceId}"` });
|
|
263708
|
+
const rRes = await fetch(`${adminBaseUrl}?${qParams}`, {
|
|
263709
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
263710
|
+
...signal ? { signal } : {}
|
|
263711
|
+
});
|
|
263712
|
+
if (rRes.ok) {
|
|
263713
|
+
const rData = await rRes.json();
|
|
263714
|
+
const items = Array.isArray(rData.items) ? rData.items : [];
|
|
263715
|
+
for (const r2 of items) {
|
|
263716
|
+
const email = String(r2.resourceEmail || "");
|
|
263717
|
+
const name = String(r2.resourceName || r2.generatedResourceName || "");
|
|
263718
|
+
if (email && name) resourceNameMap[email] = name;
|
|
263719
|
+
}
|
|
263720
|
+
}
|
|
263721
|
+
} catch {
|
|
263722
|
+
}
|
|
262212
263723
|
} else if (extra?.buildingId || extra?.capacity) {
|
|
262213
263724
|
try {
|
|
262214
263725
|
const adminBaseUrl = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources/calendars";
|
|
@@ -262226,6 +263737,11 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262226
263737
|
return true;
|
|
262227
263738
|
});
|
|
262228
263739
|
resourceCalendarIds = filtered.map((r2) => String(r2.resourceEmail || "")).filter(Boolean);
|
|
263740
|
+
for (const r2 of items) {
|
|
263741
|
+
const email = String(r2.resourceEmail || "");
|
|
263742
|
+
const name = String(r2.resourceName || r2.generatedResourceName || "");
|
|
263743
|
+
if (email && name) resourceNameMap[email] = name;
|
|
263744
|
+
}
|
|
262229
263745
|
}
|
|
262230
263746
|
} catch {
|
|
262231
263747
|
}
|
|
@@ -262277,6 +263793,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262277
263793
|
let requiredOk = true;
|
|
262278
263794
|
let availCount = 0;
|
|
262279
263795
|
const missingOpt = [];
|
|
263796
|
+
const slotAvailResources = [];
|
|
262280
263797
|
for (const email of allEmails) {
|
|
262281
263798
|
const busy = busyMap[email] || [];
|
|
262282
263799
|
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
@@ -262288,6 +263805,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262288
263805
|
}
|
|
262289
263806
|
} else {
|
|
262290
263807
|
availCount++;
|
|
263808
|
+
if (resourceEmails.includes(email)) {
|
|
263809
|
+
slotAvailResources.push(email);
|
|
263810
|
+
}
|
|
262291
263811
|
}
|
|
262292
263812
|
}
|
|
262293
263813
|
if (!requiredOk) continue;
|
|
@@ -262306,7 +263826,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262306
263826
|
}
|
|
262307
263827
|
if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
|
|
262308
263828
|
if (!isFinite(bufAfter)) bufAfter = 12 * 60 * 60 * 1e3;
|
|
262309
|
-
candidates.push({ start: slotS, end: slotE, allAvailable: availCount === allEmails.length, availableCount: availCount, missingOptional: missingOpt, bufferBefore: bufBefore, bufferAfter: bufAfter });
|
|
263829
|
+
candidates.push({ start: slotS, end: slotE, allAvailable: availCount === allEmails.length, availableCount: availCount, missingOptional: missingOpt, bufferBefore: bufBefore, bufferAfter: bufAfter, availableResources: slotAvailResources });
|
|
262310
263830
|
}
|
|
262311
263831
|
}
|
|
262312
263832
|
const hasSuf = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
|
|
@@ -262347,6 +263867,10 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262347
263867
|
const label = c.allAvailable ? `All ${allEmails.length} available` : `${c.availableCount}/${allEmails.length} available`;
|
|
262348
263868
|
lines.push(` #${i2 + 1}: ${s2.toLocaleString()} \u2192 ${e2.toLocaleTimeString()} (${label})`);
|
|
262349
263869
|
if (c.missingOptional.length) lines.push(` Unavailable (optional): ${c.missingOptional.join(", ")}`);
|
|
263870
|
+
if (c.availableResources.length > 0) {
|
|
263871
|
+
const names = c.availableResources.map((e3) => resourceNameMap[e3] || e3);
|
|
263872
|
+
lines.push(` Available resources: ${names.join(", ")}`);
|
|
263873
|
+
}
|
|
262350
263874
|
lines.push(` Buffer: ${Math.round(c.bufferBefore / 6e4)} min before, ${Math.round(c.bufferAfter / 6e4)} min after`);
|
|
262351
263875
|
lines.push("");
|
|
262352
263876
|
}
|
|
@@ -262364,7 +263888,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262364
263888
|
missingOptional: c.missingOptional,
|
|
262365
263889
|
bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
|
|
262366
263890
|
bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
|
|
262367
|
-
bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4)
|
|
263891
|
+
bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4),
|
|
263892
|
+
resourceEmails: c.availableResources,
|
|
263893
|
+
resourceNames: c.availableResources.map((e2) => resourceNameMap[e2] || e2)
|
|
262368
263894
|
}))
|
|
262369
263895
|
}
|
|
262370
263896
|
};
|
|
@@ -262599,7 +264125,7 @@ function formatImportSummary(results, totalRecords, isDryRun) {
|
|
|
262599
264125
|
async function fetchExistingBuildingIds(token, signal) {
|
|
262600
264126
|
const ids = /* @__PURE__ */ new Set();
|
|
262601
264127
|
try {
|
|
262602
|
-
const res = await fetch(`${
|
|
264128
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/buildings?maxResults=200`, {
|
|
262603
264129
|
headers: { Authorization: `Bearer ${token}` },
|
|
262604
264130
|
...signal ? { signal } : {}
|
|
262605
264131
|
});
|
|
@@ -262619,7 +264145,7 @@ async function fetchExistingBuildingIds(token, signal) {
|
|
|
262619
264145
|
async function listResources(token, buildingId, signal) {
|
|
262620
264146
|
const params = new URLSearchParams({ maxResults: "200" });
|
|
262621
264147
|
if (buildingId) params.set("query", `buildingId="${buildingId}"`);
|
|
262622
|
-
const res = await fetch(`${
|
|
264148
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/calendars?${params}`, {
|
|
262623
264149
|
headers: { Authorization: `Bearer ${token}` },
|
|
262624
264150
|
...signal ? { signal } : {}
|
|
262625
264151
|
});
|
|
@@ -262647,7 +264173,7 @@ async function listResources(token, buildingId, signal) {
|
|
|
262647
264173
|
return { text: lines.join("\n"), json: { ok: true, resources: items } };
|
|
262648
264174
|
}
|
|
262649
264175
|
async function getResource(token, resourceId, signal) {
|
|
262650
|
-
const res = await fetch(`${
|
|
264176
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/calendars/${encodeURIComponent(resourceId)}`, {
|
|
262651
264177
|
headers: { Authorization: `Bearer ${token}` },
|
|
262652
264178
|
...signal ? { signal } : {}
|
|
262653
264179
|
});
|
|
@@ -262678,7 +264204,7 @@ async function createResource(token, params, signal) {
|
|
|
262678
264204
|
if (params.capacity) body.capacity = params.capacity;
|
|
262679
264205
|
if (params.floorName) body.floorName = params.floorName;
|
|
262680
264206
|
if (params.description) body.resourceDescription = params.description;
|
|
262681
|
-
const res = await fetch(`${
|
|
264207
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/calendars`, {
|
|
262682
264208
|
method: "POST",
|
|
262683
264209
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262684
264210
|
body: JSON.stringify(body),
|
|
@@ -262769,7 +264295,7 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
|
|
|
262769
264295
|
if (rec.floor || rec.floorName) body.floorName = rec.floor || rec.floorName;
|
|
262770
264296
|
if (rec.description || rec.resourceDescription) body.resourceDescription = rec.description || rec.resourceDescription;
|
|
262771
264297
|
try {
|
|
262772
|
-
const createRes = await fetch(`${
|
|
264298
|
+
const createRes = await fetch(`${ADMIN_RESOURCES_BASE2}/calendars`, {
|
|
262773
264299
|
method: "POST",
|
|
262774
264300
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262775
264301
|
body: JSON.stringify(body),
|
|
@@ -262800,7 +264326,7 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
|
|
|
262800
264326
|
};
|
|
262801
264327
|
}
|
|
262802
264328
|
async function listBuildings(token, signal) {
|
|
262803
|
-
const res = await fetch(`${
|
|
264329
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/buildings?maxResults=200`, {
|
|
262804
264330
|
headers: { Authorization: `Bearer ${token}` },
|
|
262805
264331
|
...signal ? { signal } : {}
|
|
262806
264332
|
});
|
|
@@ -262836,7 +264362,7 @@ async function createBuilding(token, params, signal) {
|
|
|
262836
264362
|
body.floorNames = params.floors.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
262837
264363
|
}
|
|
262838
264364
|
}
|
|
262839
|
-
const res = await fetch(`${
|
|
264365
|
+
const res = await fetch(`${ADMIN_RESOURCES_BASE2}/buildings`, {
|
|
262840
264366
|
method: "POST",
|
|
262841
264367
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262842
264368
|
body: JSON.stringify(body),
|
|
@@ -262924,7 +264450,7 @@ async function bulkImportBuildings(token, filePath4, signal, dryRun = false) {
|
|
|
262924
264450
|
body.floorNames = n > 0 ? Array.from({ length: n }, (_, i22) => String(i22 + 1)) : f3.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
262925
264451
|
}
|
|
262926
264452
|
try {
|
|
262927
|
-
const createRes = await fetch(`${
|
|
264453
|
+
const createRes = await fetch(`${ADMIN_RESOURCES_BASE2}/buildings`, {
|
|
262928
264454
|
method: "POST",
|
|
262929
264455
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262930
264456
|
body: JSON.stringify(body),
|
|
@@ -263003,7 +264529,7 @@ async function checkAvailability(token, targets, datetime, durationMinutes, sign
|
|
|
263003
264529
|
function createGcalField() {
|
|
263004
264530
|
return { commandId: "gcal", worker: new GcalWorker(), checker: new AlwaysPassChecker11() };
|
|
263005
264531
|
}
|
|
263006
|
-
var GcalWorker,
|
|
264532
|
+
var GcalWorker, ADMIN_RESOURCES_BASE2, EMAIL_REGEX, AlwaysPassChecker11;
|
|
263007
264533
|
var init_gcal_field = __esm({
|
|
263008
264534
|
"commands/google/gcal.field.ts"() {
|
|
263009
264535
|
init_base2();
|
|
@@ -263180,7 +264706,7 @@ var init_gcal_field = __esm({
|
|
|
263180
264706
|
}
|
|
263181
264707
|
];
|
|
263182
264708
|
};
|
|
263183
|
-
|
|
264709
|
+
ADMIN_RESOURCES_BASE2 = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
|
|
263184
264710
|
EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
263185
264711
|
AlwaysPassChecker11 = class extends LiteCheckerAgent {
|
|
263186
264712
|
commandId = "gcal";
|
|
@@ -286736,7 +288262,7 @@ var require_firestore = __commonJS({
|
|
|
286736
288262
|
"../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/firestore/index.js"(exports2) {
|
|
286737
288263
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
286738
288264
|
exports2.FirebaseFirestoreError = exports2.setLogFunction = exports2.v1 = exports2.WriteResult = exports2.WriteBatch = exports2.Transaction = exports2.Timestamp = exports2.QuerySnapshot = exports2.QueryPartition = exports2.QueryDocumentSnapshot = exports2.Query = exports2.GrpcStatus = exports2.GeoPoint = exports2.Firestore = exports2.Filter = exports2.FieldValue = exports2.FieldPath = exports2.DocumentSnapshot = exports2.DocumentReference = exports2.CollectionReference = exports2.CollectionGroup = exports2.BundleBuilder = exports2.BulkWriter = exports2.AggregateQuerySnapshot = exports2.AggregateQuery = exports2.AggregateField = void 0;
|
|
286739
|
-
exports2.getFirestore =
|
|
288265
|
+
exports2.getFirestore = getFirestore4;
|
|
286740
288266
|
exports2.initializeFirestore = initializeFirestore2;
|
|
286741
288267
|
var app_1 = require_app();
|
|
286742
288268
|
var firestore_internal_1 = require_firestore_internal();
|
|
@@ -286817,7 +288343,7 @@ var require_firestore = __commonJS({
|
|
|
286817
288343
|
Object.defineProperty(exports2, "setLogFunction", { enumerable: true, get: function() {
|
|
286818
288344
|
return firestore_1.setLogFunction;
|
|
286819
288345
|
} });
|
|
286820
|
-
function
|
|
288346
|
+
function getFirestore4(appOrDatabaseId, optionalDatabaseId) {
|
|
286821
288347
|
const app = typeof appOrDatabaseId === "object" ? appOrDatabaseId : (0, app_1.getApp)();
|
|
286822
288348
|
const databaseId = (typeof appOrDatabaseId === "string" ? appOrDatabaseId : optionalDatabaseId) || path_1.DEFAULT_DATABASE_ID;
|
|
286823
288349
|
const firebaseApp = app;
|
|
@@ -286839,7 +288365,7 @@ var require_firestore = __commonJS({
|
|
|
286839
288365
|
});
|
|
286840
288366
|
|
|
286841
288367
|
// ../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/esm/firestore/index.js
|
|
286842
|
-
var import_firestore2,
|
|
288368
|
+
var import_firestore2, getFirestore3;
|
|
286843
288369
|
var init_firestore = __esm({
|
|
286844
288370
|
"../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/esm/firestore/index.js"() {
|
|
286845
288371
|
import_firestore2 = __toESM(require_firestore(), 1);
|
|
@@ -286867,7 +288393,7 @@ var init_firestore = __esm({
|
|
|
286867
288393
|
import_firestore2.default.Transaction;
|
|
286868
288394
|
import_firestore2.default.WriteBatch;
|
|
286869
288395
|
import_firestore2.default.WriteResult;
|
|
286870
|
-
|
|
288396
|
+
getFirestore3 = import_firestore2.default.getFirestore;
|
|
286871
288397
|
import_firestore2.default.initializeFirestore;
|
|
286872
288398
|
import_firestore2.default.setLogFunction;
|
|
286873
288399
|
import_firestore2.default.v1;
|
|
@@ -287173,7 +288699,7 @@ var init_billing_pl_field = __esm({
|
|
|
287173
288699
|
json: { error: "access_denied", reason: accessCheck.reason }
|
|
287174
288700
|
};
|
|
287175
288701
|
}
|
|
287176
|
-
const firestore =
|
|
288702
|
+
const firestore = getFirestore3();
|
|
287177
288703
|
const tenantPaths = new TenantPaths(tenantId);
|
|
287178
288704
|
let targetProjectId = projectId;
|
|
287179
288705
|
if (isNaturalLanguage && projectNameFromNL && !projectId) {
|
|
@@ -287495,7 +289021,7 @@ function createCompetitorsField() {
|
|
|
287495
289021
|
const maxRoundsRaw = typeof ctx.parsed.options["max-rounds"] === "string" ? Number(ctx.parsed.options["max-rounds"]) : 10;
|
|
287496
289022
|
const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(20, Math.floor(maxRoundsRaw))) : 10;
|
|
287497
289023
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
287498
|
-
const outBaseRel = (outBase || "competitors").replace(/\\/g, "/");
|
|
289024
|
+
const outBaseRel = (outBase || ".maria/desktop/competitors").replace(/\\/g, "/");
|
|
287499
289025
|
const inOpt = typeof ctx.parsed.options.in === "string" ? String(ctx.parsed.options.in).trim() : "";
|
|
287500
289026
|
let productDesc = ctx.parsed.args.join(" ").trim();
|
|
287501
289027
|
if (inOpt) {
|
|
@@ -288179,8 +289705,9 @@ function computeWeekWindows(untilDateStr, totalDays) {
|
|
|
288179
289705
|
}
|
|
288180
289706
|
function sparkline(scores, min = 1, max = 5) {
|
|
288181
289707
|
return scores.map((s2) => {
|
|
288182
|
-
const
|
|
288183
|
-
|
|
289708
|
+
const safe = Number.isFinite(s2) ? s2 : 3;
|
|
289709
|
+
const idx = Math.round((safe - min) / (max - min) * (SPARK_CHARS.length - 1));
|
|
289710
|
+
return SPARK_CHARS[Math.max(0, Math.min(SPARK_CHARS.length - 1, idx))] ?? SPARK_CHARS[4];
|
|
288184
289711
|
}).join("");
|
|
288185
289712
|
}
|
|
288186
289713
|
function computeTrend(scores) {
|
|
@@ -288191,7 +289718,7 @@ function computeTrend(scores) {
|
|
|
288191
289718
|
let num = 0;
|
|
288192
289719
|
let den = 0;
|
|
288193
289720
|
for (let i2 = 0; i2 < n; i2++) {
|
|
288194
|
-
num += (i2 - xMean) * ((scores[i2] ?? 3) - yMean);
|
|
289721
|
+
num += (i2 - xMean) * (safeScore(scores[i2] ?? 3) - yMean);
|
|
288195
289722
|
den += (i2 - xMean) ** 2;
|
|
288196
289723
|
}
|
|
288197
289724
|
const slope = den === 0 ? 0 : num / den;
|
|
@@ -288209,6 +289736,28 @@ function calcMean(values) {
|
|
|
288209
289736
|
if (values.length === 0) return 0;
|
|
288210
289737
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
288211
289738
|
}
|
|
289739
|
+
function clamp(v, lo, hi) {
|
|
289740
|
+
return Math.min(hi, Math.max(lo, v));
|
|
289741
|
+
}
|
|
289742
|
+
function safeScore(v) {
|
|
289743
|
+
const n = typeof v === "string" ? parseFloat(v) : Number(v);
|
|
289744
|
+
if (!Number.isFinite(n)) return 3;
|
|
289745
|
+
return clamp(n, 1, 5);
|
|
289746
|
+
}
|
|
289747
|
+
function sanitizeSubDimensions(raw, expected) {
|
|
289748
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
289749
|
+
return expected.map((s2) => ({ id: s2.id, score: 3, evidence: ["Parse incomplete"] }));
|
|
289750
|
+
}
|
|
289751
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
289752
|
+
for (const r2 of raw) {
|
|
289753
|
+
if (r2 && typeof r2.id === "string") lookup.set(r2.id.trim().toLowerCase(), r2);
|
|
289754
|
+
}
|
|
289755
|
+
return expected.map((s2) => {
|
|
289756
|
+
const found = lookup.get(s2.id.toLowerCase());
|
|
289757
|
+
if (found) return { id: s2.id, score: safeScore(found.score), evidence: Array.isArray(found.evidence) ? found.evidence : ["(no evidence)"] };
|
|
289758
|
+
return { id: s2.id, score: 3, evidence: ["Sub-dimension not returned by evaluator"] };
|
|
289759
|
+
});
|
|
289760
|
+
}
|
|
288212
289761
|
function trendEmoji(t2) {
|
|
288213
289762
|
return t2 === "improving" ? "\u2191" : t2 === "declining" ? "\u2193" : "\u2192";
|
|
288214
289763
|
}
|
|
@@ -288226,7 +289775,7 @@ function computeAllTrends(weeks) {
|
|
|
288226
289775
|
const weeklyScores = weeks.map((w) => {
|
|
288227
289776
|
const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
|
|
288228
289777
|
const sd = de?.subDimensions.find((s2) => s2.id === sub.id);
|
|
288229
|
-
return sd?.score ?? 3;
|
|
289778
|
+
return safeScore(sd?.score ?? 3);
|
|
288230
289779
|
});
|
|
288231
289780
|
trends.push({
|
|
288232
289781
|
id: sub.id,
|
|
@@ -288241,7 +289790,7 @@ function computeAllTrends(weeks) {
|
|
|
288241
289790
|
}
|
|
288242
289791
|
const weeklyOverall = weeks.map((w) => {
|
|
288243
289792
|
const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
|
|
288244
|
-
return de?.overallScore ?? 3;
|
|
289793
|
+
return safeScore(de?.overallScore ?? 3);
|
|
288245
289794
|
});
|
|
288246
289795
|
trends.push({
|
|
288247
289796
|
id: dim.id,
|
|
@@ -288277,7 +289826,7 @@ function computeQuantity(ghData) {
|
|
|
288277
289826
|
};
|
|
288278
289827
|
}
|
|
288279
289828
|
async function loadPreviousWeekDominants(cwd, username, currentOutDir) {
|
|
288280
|
-
const baseDir = path87__namespace.resolve(cwd, "dev-adviser");
|
|
289829
|
+
const baseDir = path87__namespace.resolve(cwd, ".maria/desktop/dev-adviser");
|
|
288281
289830
|
let entries;
|
|
288282
289831
|
try {
|
|
288283
289832
|
entries = await fsp10__namespace.readdir(baseDir);
|
|
@@ -288338,6 +289887,75 @@ function neutralWorkClassification() {
|
|
|
288338
289887
|
dominantCategory: "new-feature"
|
|
288339
289888
|
};
|
|
288340
289889
|
}
|
|
289890
|
+
function validateEvalRaw(raw, numWeeks) {
|
|
289891
|
+
const issues = [];
|
|
289892
|
+
if (!raw || !Array.isArray(raw.weeks)) {
|
|
289893
|
+
issues.push("Top-level 'weeks' array is missing or not an array.");
|
|
289894
|
+
return { ok: false, issues };
|
|
289895
|
+
}
|
|
289896
|
+
if (raw.weeks.length < numWeeks) {
|
|
289897
|
+
issues.push(`Expected ${numWeeks} weeks but got ${raw.weeks.length}.`);
|
|
289898
|
+
}
|
|
289899
|
+
for (let wi = 0; wi < numWeeks; wi++) {
|
|
289900
|
+
const w = raw.weeks[wi];
|
|
289901
|
+
if (!w) {
|
|
289902
|
+
issues.push(`Week ${wi} is missing entirely.`);
|
|
289903
|
+
continue;
|
|
289904
|
+
}
|
|
289905
|
+
if (!w.architectQuality) {
|
|
289906
|
+
issues.push(`Week ${wi}: architectQuality is missing.`);
|
|
289907
|
+
} else {
|
|
289908
|
+
if (!Array.isArray(w.architectQuality.subDimensions) || w.architectQuality.subDimensions.length === 0) {
|
|
289909
|
+
issues.push(`Week ${wi}: architectQuality.subDimensions is empty or missing.`);
|
|
289910
|
+
} else {
|
|
289911
|
+
const returnedIds = new Set(w.architectQuality.subDimensions.map((s2) => String(s2?.id || "")));
|
|
289912
|
+
for (const eid of EXPECTED_AQ_IDS) {
|
|
289913
|
+
if (!returnedIds.has(eid)) issues.push(`Week ${wi}: architectQuality sub-dimension "${eid}" is missing.`);
|
|
289914
|
+
}
|
|
289915
|
+
}
|
|
289916
|
+
if (typeof w.architectQuality.overallScore !== "number") {
|
|
289917
|
+
issues.push(`Week ${wi}: architectQuality.overallScore is not a number.`);
|
|
289918
|
+
}
|
|
289919
|
+
}
|
|
289920
|
+
if (!w.semanticQuality) {
|
|
289921
|
+
issues.push(`Week ${wi}: semanticQuality is missing.`);
|
|
289922
|
+
} else {
|
|
289923
|
+
if (!Array.isArray(w.semanticQuality.subDimensions) || w.semanticQuality.subDimensions.length === 0) {
|
|
289924
|
+
issues.push(`Week ${wi}: semanticQuality.subDimensions is empty or missing.`);
|
|
289925
|
+
} else {
|
|
289926
|
+
const returnedIds = new Set(w.semanticQuality.subDimensions.map((s2) => String(s2?.id || "")));
|
|
289927
|
+
for (const eid of EXPECTED_SQ_IDS) {
|
|
289928
|
+
if (!returnedIds.has(eid)) issues.push(`Week ${wi}: semanticQuality sub-dimension "${eid}" is missing.`);
|
|
289929
|
+
}
|
|
289930
|
+
}
|
|
289931
|
+
if (typeof w.semanticQuality.overallScore !== "number") {
|
|
289932
|
+
issues.push(`Week ${wi}: semanticQuality.overallScore is not a number.`);
|
|
289933
|
+
}
|
|
289934
|
+
}
|
|
289935
|
+
}
|
|
289936
|
+
return { ok: issues.length === 0, issues };
|
|
289937
|
+
}
|
|
289938
|
+
function buildRepairPrompt(brokenText, issues, numWeeks) {
|
|
289939
|
+
const aqIds = ARCHITECT_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
|
|
289940
|
+
const sqIds = SEMANTIC_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
|
|
289941
|
+
return [
|
|
289942
|
+
"MODEL_FIX: Your previous evaluation output is malformed. Repair it and return valid JSON only.",
|
|
289943
|
+
"",
|
|
289944
|
+
"Issues found:",
|
|
289945
|
+
...issues.map((i2) => ` - ${i2}`),
|
|
289946
|
+
"",
|
|
289947
|
+
`Required: ${numWeeks} weeks, each with architectQuality and semanticQuality.`,
|
|
289948
|
+
`architectQuality sub-dimension IDs: [${aqIds}]`,
|
|
289949
|
+
`semanticQuality sub-dimension IDs: [${sqIds}]`,
|
|
289950
|
+
"Each sub-dimension needs: { id, score (1.0-5.0), evidence (string[]) }",
|
|
289951
|
+
"Each dimension needs: { subDimensions, overallScore (1.0-5.0), summary (string) }",
|
|
289952
|
+
"",
|
|
289953
|
+
"Your broken output (fix this and return correct JSON only):",
|
|
289954
|
+
"```",
|
|
289955
|
+
brokenText.slice(0, 12e3),
|
|
289956
|
+
"```"
|
|
289957
|
+
].join("\n");
|
|
289958
|
+
}
|
|
288341
289959
|
async function loadDevAdviserIgnore(cwd) {
|
|
288342
289960
|
const filePath4 = path87__namespace.resolve(cwd, ".devadviserignore");
|
|
288343
289961
|
try {
|
|
@@ -289080,7 +290698,7 @@ function buildMarkdownReport(result, username, scope) {
|
|
|
289080
290698
|
function createDevAdviserField() {
|
|
289081
290699
|
return { commandId: "dev-adviser", worker: new DevAdviserWorker(), checker: new AlwaysPassChecker20() };
|
|
289082
290700
|
}
|
|
289083
|
-
var BOT_AUTHOR_FILTER, TRAIT_DEFS, TRAIT_THRESHOLD, ARCHITECT_QUALITY, SEMANTIC_QUALITY, ALL_DIMENSIONS, WORK_CATEGORY_LABELS, SPARK_CHARS, FIX_CHASE_PATTERN, MAX_TRIAGE_PER_WEEK, MAX_TRIAGE_PATCH_LINES, MAX_LINES_PER_FILE, SKIP_PATCH_PATTERN, DevAdviserWorker, AlwaysPassChecker20;
|
|
290701
|
+
var BOT_AUTHOR_FILTER, TRAIT_DEFS, TRAIT_THRESHOLD, ARCHITECT_QUALITY, SEMANTIC_QUALITY, ALL_DIMENSIONS, WORK_CATEGORY_LABELS, SPARK_CHARS, MAX_EVAL_REPAIR_ATTEMPTS, EXPECTED_AQ_IDS, EXPECTED_SQ_IDS, FIX_CHASE_PATTERN, MAX_TRIAGE_PER_WEEK, MAX_TRIAGE_PATCH_LINES, MAX_LINES_PER_FILE, SKIP_PATCH_PATTERN, DevAdviserWorker, AlwaysPassChecker20;
|
|
289084
290702
|
var init_dev_adviser_field = __esm({
|
|
289085
290703
|
"commands/dev-adviser.field.ts"() {
|
|
289086
290704
|
init_base2();
|
|
@@ -289138,6 +290756,9 @@ var init_dev_adviser_field = __esm({
|
|
|
289138
290756
|
"dependency-updates": "Deps"
|
|
289139
290757
|
};
|
|
289140
290758
|
SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
290759
|
+
MAX_EVAL_REPAIR_ATTEMPTS = 2;
|
|
290760
|
+
EXPECTED_AQ_IDS = new Set(ARCHITECT_QUALITY.subDimensions.map((s2) => s2.id));
|
|
290761
|
+
EXPECTED_SQ_IDS = new Set(SEMANTIC_QUALITY.subDimensions.map((s2) => s2.id));
|
|
289141
290762
|
FIX_CHASE_PATTERN = /\b(fix|bug|patch|hotfix|typo|修正|バグ)\b/i;
|
|
289142
290763
|
MAX_TRIAGE_PER_WEEK = 30;
|
|
289143
290764
|
MAX_TRIAGE_PATCH_LINES = 6e3;
|
|
@@ -289305,6 +290926,42 @@ Check the username and date range.`
|
|
|
289305
290926
|
"",
|
|
289306
290927
|
patchContext
|
|
289307
290928
|
].join("\n");
|
|
290929
|
+
const tryParseEval = (text) => {
|
|
290930
|
+
const rawText = String(text || "");
|
|
290931
|
+
const result2 = parseJsonFromModelText({ text: rawText, label: "dev-adviser.eval" });
|
|
290932
|
+
if (!result2.ok) return { parsed: null, rawText, issues: [`JSON parse failed: ${result2.error.slice(0, 200)}`] };
|
|
290933
|
+
const v = validateEvalRaw(result2.value, numWeeks);
|
|
290934
|
+
if (!v.ok) return { parsed: result2.value, rawText, issues: v.issues };
|
|
290935
|
+
return { parsed: result2.value, rawText, issues: [] };
|
|
290936
|
+
};
|
|
290937
|
+
const evalRawToWeekDimEvals = (raw) => {
|
|
290938
|
+
const result2 = [];
|
|
290939
|
+
for (let wi = 0; wi < numWeeks; wi++) {
|
|
290940
|
+
const wData = raw.weeks[wi];
|
|
290941
|
+
if (wData) {
|
|
290942
|
+
result2.push([
|
|
290943
|
+
{
|
|
290944
|
+
dimensionId: "architect-quality",
|
|
290945
|
+
subDimensions: sanitizeSubDimensions(wData.architectQuality?.subDimensions, ARCHITECT_QUALITY.subDimensions),
|
|
290946
|
+
overallScore: safeScore(wData.architectQuality?.overallScore ?? 3),
|
|
290947
|
+
summary: wData.architectQuality?.summary ?? ""
|
|
290948
|
+
},
|
|
290949
|
+
{
|
|
290950
|
+
dimensionId: "semantic-quality",
|
|
290951
|
+
subDimensions: sanitizeSubDimensions(wData.semanticQuality?.subDimensions, SEMANTIC_QUALITY.subDimensions),
|
|
290952
|
+
overallScore: safeScore(wData.semanticQuality?.overallScore ?? 3),
|
|
290953
|
+
summary: wData.semanticQuality?.summary ?? ""
|
|
290954
|
+
}
|
|
290955
|
+
]);
|
|
290956
|
+
} else {
|
|
290957
|
+
result2.push([
|
|
290958
|
+
neutralDimensionEval(ARCHITECT_QUALITY, "No data for this week"),
|
|
290959
|
+
neutralDimensionEval(SEMANTIC_QUALITY, "No data for this week")
|
|
290960
|
+
]);
|
|
290961
|
+
}
|
|
290962
|
+
}
|
|
290963
|
+
return result2;
|
|
290964
|
+
};
|
|
289308
290965
|
const evalResult = await withLiteSpinner(
|
|
289309
290966
|
`Evaluating Architect Quality & Semantic Quality (${selectedShas.size} commits examined)...`,
|
|
289310
290967
|
() => this.aiPromptStructured(ctx, {
|
|
@@ -289314,36 +290971,44 @@ Check the username and date range.`
|
|
|
289314
290971
|
signal: ctx.abortSignal
|
|
289315
290972
|
})
|
|
289316
290973
|
);
|
|
289317
|
-
|
|
290974
|
+
let weekDimensionEvals = [];
|
|
290975
|
+
let evalAccepted = false;
|
|
289318
290976
|
if (evalResult.status === "ok") {
|
|
289319
|
-
const
|
|
289320
|
-
if (
|
|
289321
|
-
|
|
289322
|
-
|
|
289323
|
-
|
|
289324
|
-
|
|
289325
|
-
|
|
289326
|
-
|
|
289327
|
-
|
|
289328
|
-
|
|
289329
|
-
|
|
289330
|
-
})
|
|
289331
|
-
|
|
289332
|
-
|
|
289333
|
-
|
|
289334
|
-
|
|
289335
|
-
|
|
289336
|
-
})
|
|
289337
|
-
|
|
289338
|
-
|
|
289339
|
-
|
|
289340
|
-
|
|
289341
|
-
|
|
289342
|
-
|
|
290977
|
+
const attempt0 = tryParseEval(evalResult.text || "");
|
|
290978
|
+
if (attempt0.issues.length === 0 && attempt0.parsed) {
|
|
290979
|
+
weekDimensionEvals = evalRawToWeekDimEvals(attempt0.parsed);
|
|
290980
|
+
evalAccepted = true;
|
|
290981
|
+
} else {
|
|
290982
|
+
let lastRawText = attempt0.rawText;
|
|
290983
|
+
let lastIssues = attempt0.issues;
|
|
290984
|
+
for (let ri = 1; ri <= MAX_EVAL_REPAIR_ATTEMPTS; ri++) {
|
|
290985
|
+
await emitLog("lite.dev-adviser.eval_repair", { attempt: ri, issues: lastIssues.slice(0, 5) });
|
|
290986
|
+
const repairPrompt = buildRepairPrompt(lastRawText, lastIssues, numWeeks);
|
|
290987
|
+
const repairResult = await withLiteSpinner(
|
|
290988
|
+
`Repairing evaluation format (attempt ${ri}/${MAX_EVAL_REPAIR_ATTEMPTS})...`,
|
|
290989
|
+
() => this.aiPromptStructured(ctx, {
|
|
290990
|
+
taskType: "dev-adviser.eval",
|
|
290991
|
+
systemPrompt: "You are a JSON repair assistant. Fix the broken JSON to match the required schema. Return valid JSON only.",
|
|
290992
|
+
prompt: repairPrompt,
|
|
290993
|
+
signal: ctx.abortSignal
|
|
290994
|
+
})
|
|
290995
|
+
);
|
|
290996
|
+
if (repairResult.status !== "ok") continue;
|
|
290997
|
+
const attemptN = tryParseEval(repairResult.text || "");
|
|
290998
|
+
if (attemptN.issues.length === 0 && attemptN.parsed) {
|
|
290999
|
+
weekDimensionEvals = evalRawToWeekDimEvals(attemptN.parsed);
|
|
291000
|
+
evalAccepted = true;
|
|
291001
|
+
await emitLog("lite.dev-adviser.eval_repair_ok", { attempt: ri });
|
|
291002
|
+
break;
|
|
289343
291003
|
}
|
|
291004
|
+
lastRawText = attemptN.rawText;
|
|
291005
|
+
lastIssues = attemptN.issues;
|
|
289344
291006
|
}
|
|
289345
291007
|
}
|
|
289346
291008
|
}
|
|
291009
|
+
if (!evalAccepted) {
|
|
291010
|
+
await emitLog("lite.dev-adviser.eval_fallback", { reason: "All eval attempts failed" });
|
|
291011
|
+
}
|
|
289347
291012
|
while (weekDimensionEvals.length < numWeeks) {
|
|
289348
291013
|
weekDimensionEvals.push([
|
|
289349
291014
|
neutralDimensionEval(ARCHITECT_QUALITY, "LLM evaluation unavailable"),
|
|
@@ -289382,7 +291047,7 @@ Check the username and date range.`
|
|
|
289382
291047
|
}
|
|
289383
291048
|
const stamp = nowStamp3();
|
|
289384
291049
|
const titleBase = sanitizeBasename(`dev-assessment-${username}`);
|
|
289385
|
-
const outDirRel = outputPath ||
|
|
291050
|
+
const outDirRel = outputPath || `.maria/desktop/dev-adviser/${stamp}_${sanitizeBasename(username)}`;
|
|
289386
291051
|
const outDirAbs = path87__namespace.resolve(ctx.cwd, outDirRel);
|
|
289387
291052
|
const prevDominants = await loadPreviousWeekDominants(ctx.cwd, username, outDirAbs);
|
|
289388
291053
|
const classificationChangelog = computeClassificationChangelog(weeks, prevDominants);
|
|
@@ -289534,6 +291199,69 @@ Check the username and date range.`
|
|
|
289534
291199
|
};
|
|
289535
291200
|
}
|
|
289536
291201
|
});
|
|
291202
|
+
async function fetchRepoTree(repo, cwd, signal) {
|
|
291203
|
+
const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/git/trees/HEAD?recursive=1`, "--jq", ".tree[] | [.path, .type, (.size // 0)] | @tsv"] });
|
|
291204
|
+
if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
|
|
291205
|
+
const entries = r2.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
291206
|
+
const [p, t2, s2] = line.split(" ");
|
|
291207
|
+
return { path: p ?? "", type: t2 ?? "blob", size: Number(s2 || 0) };
|
|
291208
|
+
});
|
|
291209
|
+
return { ok: true, entries };
|
|
291210
|
+
}
|
|
291211
|
+
function buildCompactTree(entries) {
|
|
291212
|
+
const SKIP = /^(node_modules|\.git|dist|build|\.next|__pycache__|\.cache|vendor)\//;
|
|
291213
|
+
const SKIP_FILES = /\.(lock|min\.js|min\.css|map|snap|svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$/;
|
|
291214
|
+
const filtered = entries.filter((e2) => !SKIP.test(e2.path) && !SKIP_FILES.test(e2.path));
|
|
291215
|
+
return filtered.map((e2) => `${e2.type === "tree" ? "D" : "F"} ${e2.path}`).join("\n");
|
|
291216
|
+
}
|
|
291217
|
+
async function fetchFileContents(repo, paths, cwd, signal) {
|
|
291218
|
+
const MAX_FILES = 20;
|
|
291219
|
+
const MAX_BYTES_PER_FILE = 15e3;
|
|
291220
|
+
const result = /* @__PURE__ */ new Map();
|
|
291221
|
+
const selected = paths.slice(0, MAX_FILES);
|
|
291222
|
+
for (const p of selected) {
|
|
291223
|
+
if (signal.aborted) break;
|
|
291224
|
+
const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/contents/${p}`, "--jq", ".content"] });
|
|
291225
|
+
if (r2.exitCode !== 0) continue;
|
|
291226
|
+
try {
|
|
291227
|
+
const decoded = Buffer.from(r2.stdout.trim(), "base64").toString("utf8");
|
|
291228
|
+
result.set(p, decoded.slice(0, MAX_BYTES_PER_FILE));
|
|
291229
|
+
} catch {
|
|
291230
|
+
}
|
|
291231
|
+
}
|
|
291232
|
+
return result;
|
|
291233
|
+
}
|
|
291234
|
+
async function publishTaskAsIssue(repo, task, projectTitle, cwd, signal) {
|
|
291235
|
+
const body = [
|
|
291236
|
+
`## ${task.id}: ${task.title}`,
|
|
291237
|
+
"",
|
|
291238
|
+
task.description,
|
|
291239
|
+
"",
|
|
291240
|
+
`**Assignee:** ${task.assignee}`,
|
|
291241
|
+
`**Domain:** ${task.domain}`,
|
|
291242
|
+
`**Effort:** Optimistic ${task.effort.optimistic.hours}h / Realistic ${task.effort.realistic.hours}h / Pessimistic ${task.effort.pessimistic.hours}h`,
|
|
291243
|
+
`**Deadline:** ${task.deadline}`,
|
|
291244
|
+
`**Slack:** ${task.slackDays >= 0 ? `${task.slackDays} days` : "unknown"}`,
|
|
291245
|
+
task.onCriticalPath ? "**On Critical Path:** YES" : "",
|
|
291246
|
+
task.dependencies.length > 0 ? `**Dependencies:** ${task.dependencies.join(", ")}` : "",
|
|
291247
|
+
task.relatedFiles.length > 0 ? `
|
|
291248
|
+
**Related Files:**
|
|
291249
|
+
${task.relatedFiles.map((f3) => `- \`${f3}\``).join("\n")}` : "",
|
|
291250
|
+
"",
|
|
291251
|
+
`**Rationale:** ${task.rationale}`,
|
|
291252
|
+
"",
|
|
291253
|
+
`---`,
|
|
291254
|
+
`*Generated by MARIA OS /task-distribution \u2014 ${projectTitle}*`
|
|
291255
|
+
].filter(Boolean).join("\n");
|
|
291256
|
+
const r2 = await runGhCapture({
|
|
291257
|
+
cwd,
|
|
291258
|
+
signal,
|
|
291259
|
+
args: ["issue", "create", "--repo", repo, "--title", `[${task.id}] ${task.title}`, "--body", body]
|
|
291260
|
+
});
|
|
291261
|
+
if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
|
|
291262
|
+
const url = r2.stdout.trim();
|
|
291263
|
+
return { ok: true, url };
|
|
291264
|
+
}
|
|
289537
291265
|
function renderTaskDistMarkdown(result) {
|
|
289538
291266
|
const lines = [];
|
|
289539
291267
|
lines.push(`# ${result.projectTitle}`);
|
|
@@ -289627,6 +291355,9 @@ function renderTaskDistMarkdown(result) {
|
|
|
289627
291355
|
lines.push(`- **Slack:** ${typeof t2.slackDays === "number" ? `${t2.slackDays} days` : "unknown"}`);
|
|
289628
291356
|
lines.push(`- **Deadline:** ${t2.deadline}`);
|
|
289629
291357
|
if (t2.dependencies.length > 0) lines.push(`- **Dependencies:** ${t2.dependencies.join(", ")}`);
|
|
291358
|
+
if (Array.isArray(t2.relatedFiles) && t2.relatedFiles.length > 0) {
|
|
291359
|
+
lines.push(`- **Related Files:** ${t2.relatedFiles.map((f3) => `\`${f3}\``).join(", ")}`);
|
|
291360
|
+
}
|
|
289630
291361
|
lines.push(`- **Rationale:** ${t2.rationale}`);
|
|
289631
291362
|
lines.push("");
|
|
289632
291363
|
}
|
|
@@ -289769,13 +291500,17 @@ function createTaskDistributionField() {
|
|
|
289769
291500
|
"Examples:",
|
|
289770
291501
|
' /task-distribution "Build MVP for customer portal" --members "Alice,Bob,Charlie" --apply',
|
|
289771
291502
|
' /task-distribution --in @project-brief.md --members "Dev1,Dev2,Designer,PM" --apply',
|
|
291503
|
+
' /task-distribution "Add auth module" --members "Dev1,Dev2" --repo "owner/repo" --apply',
|
|
291504
|
+
' /task-distribution "Refactor payments" --members "A,B" --repo "org/app" --publish-issues --apply',
|
|
289772
291505
|
"",
|
|
289773
291506
|
"Options:",
|
|
289774
291507
|
" --members <list>",
|
|
291508
|
+
" --repo <owner/repo>",
|
|
289775
291509
|
" --in <file>",
|
|
289776
291510
|
" --out <dir>",
|
|
289777
291511
|
" --docx",
|
|
289778
291512
|
" --pdf",
|
|
291513
|
+
" --publish-issues",
|
|
289779
291514
|
" --apply",
|
|
289780
291515
|
" --json"
|
|
289781
291516
|
].join("\n")
|
|
@@ -289789,8 +291524,10 @@ function createTaskDistributionField() {
|
|
|
289789
291524
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
289790
291525
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
289791
291526
|
const wantsJson = hasLiteFlag(ctx.parsed, "json");
|
|
291527
|
+
const wantsPublishIssues = hasLiteFlag(ctx.parsed, "publish-issues");
|
|
291528
|
+
const repoSlug = typeof ctx.parsed.options.repo === "string" ? String(ctx.parsed.options.repo).trim() : "";
|
|
289792
291529
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
289793
|
-
const outBaseRel = (outBase || "task-distribution").replace(/\\/g, "/");
|
|
291530
|
+
const outBaseRel = (outBase || ".maria/desktop/task-distribution").replace(/\\/g, "/");
|
|
289794
291531
|
const membersRaw = typeof ctx.parsed.options.members === "string" ? String(ctx.parsed.options.members).trim() : "";
|
|
289795
291532
|
const members = membersRaw.split(",").map((m2) => m2.trim()).filter(Boolean);
|
|
289796
291533
|
if (members.length === 0) return { text: `Error: --members is required.
|
|
@@ -289812,17 +291549,77 @@ ${this.help.usage}` };
|
|
|
289812
291549
|
}
|
|
289813
291550
|
if (!goal) return { text: `Usage:
|
|
289814
291551
|
${this.help.usage}` };
|
|
291552
|
+
let repoContext = "";
|
|
291553
|
+
let repoTreeEntries = [];
|
|
291554
|
+
if (repoSlug) {
|
|
291555
|
+
const ghCheck = await ensureGhInstalled2();
|
|
291556
|
+
if (!ghCheck.ok) return { text: `Error: ${ghCheck.message}
|
|
291557
|
+
--repo requires GitHub CLI.` };
|
|
291558
|
+
const tree = await fetchRepoTree(repoSlug, ctx.cwd, ctx.abortSignal);
|
|
291559
|
+
if (!tree.ok) return { text: `Error fetching repo tree: ${tree.error}` };
|
|
291560
|
+
repoTreeEntries = tree.entries;
|
|
291561
|
+
const compactTree = buildCompactTree(repoTreeEntries);
|
|
291562
|
+
const triagePrompt = [
|
|
291563
|
+
"You are a code-aware task planner. Given a project goal and a repository file tree, select the files most relevant to understanding the codebase for task distribution.",
|
|
291564
|
+
"",
|
|
291565
|
+
`Goal: ${goal}`,
|
|
291566
|
+
additionalContext ? `
|
|
291567
|
+
Additional context:
|
|
291568
|
+
${additionalContext.slice(0, 1e4)}` : "",
|
|
291569
|
+
"",
|
|
291570
|
+
"## Repository File Tree",
|
|
291571
|
+
compactTree.slice(0, 8e4),
|
|
291572
|
+
"",
|
|
291573
|
+
"Select up to 20 files that are MOST relevant for understanding the codebase structure, architecture, and the areas this goal touches.",
|
|
291574
|
+
"Prefer: entry points, config files, core modules, type definitions, route definitions, schema files.",
|
|
291575
|
+
"Skip: tests, generated files, assets, lock files.",
|
|
291576
|
+
"",
|
|
291577
|
+
"Respond with JSON only:",
|
|
291578
|
+
"```json",
|
|
291579
|
+
'{ "selectedFiles": ["path/to/file1.ts", "path/to/file2.ts", ...],',
|
|
291580
|
+
' "rationale": "Brief explanation" }',
|
|
291581
|
+
"```"
|
|
291582
|
+
].join("\n");
|
|
291583
|
+
const triageRes = await this.aiPromptStructured(ctx, {
|
|
291584
|
+
taskType: "chat",
|
|
291585
|
+
prompt: triagePrompt,
|
|
291586
|
+
signal: ctx.abortSignal,
|
|
291587
|
+
spinnerTextOverride: "Analyzing repo structure"
|
|
291588
|
+
});
|
|
291589
|
+
const triageParsed = parseJsonFromModelText({
|
|
291590
|
+
text: String(triageRes.text || ""),
|
|
291591
|
+
label: "repo-triage"
|
|
291592
|
+
});
|
|
291593
|
+
const selectedPaths = triageParsed.ok ? triageParsed.value.selectedFiles || [] : [];
|
|
291594
|
+
const fileContents = selectedPaths.length > 0 ? await fetchFileContents(repoSlug, selectedPaths, ctx.cwd, ctx.abortSignal) : /* @__PURE__ */ new Map();
|
|
291595
|
+
const contextParts = [
|
|
291596
|
+
`
|
|
291597
|
+
## Repository: ${repoSlug}`,
|
|
291598
|
+
"",
|
|
291599
|
+
"### File Tree (filtered)",
|
|
291600
|
+
compactTree.slice(0, 4e4)
|
|
291601
|
+
];
|
|
291602
|
+
if (fileContents.size > 0) {
|
|
291603
|
+
contextParts.push("", "### Key File Contents");
|
|
291604
|
+
for (const [fp, content] of fileContents) {
|
|
291605
|
+
contextParts.push("", `#### ${fp}`, "```", content.slice(0, 1e4), "```");
|
|
291606
|
+
}
|
|
291607
|
+
}
|
|
291608
|
+
repoContext = contextParts.join("\n");
|
|
291609
|
+
}
|
|
289815
291610
|
if (!apply) {
|
|
289816
291611
|
return {
|
|
289817
291612
|
text: [
|
|
289818
291613
|
"Plan: /task-distribution",
|
|
289819
291614
|
`goal=${goal.slice(0, 120)}${goal.length > 120 ? "..." : ""}`,
|
|
289820
291615
|
`members=${members.join(", ")}`,
|
|
291616
|
+
repoSlug ? `repo=${repoSlug}` : "",
|
|
289821
291617
|
`outDir=${outBaseRel}`,
|
|
289822
291618
|
`docx=${wantsDocx ? "yes" : "no"} pdf=${wantsPdf ? "yes" : "no"}`,
|
|
291619
|
+
wantsPublishIssues ? "publish-issues=yes" : "",
|
|
289823
291620
|
"",
|
|
289824
291621
|
"Run with: --apply"
|
|
289825
|
-
].join("\n")
|
|
291622
|
+
].filter(Boolean).join("\n")
|
|
289826
291623
|
};
|
|
289827
291624
|
}
|
|
289828
291625
|
const stamp = nowStamp3();
|
|
@@ -289833,7 +291630,7 @@ ${this.help.usage}` };
|
|
|
289833
291630
|
schemaVersion: "maria_lite_task_dist_run_v1",
|
|
289834
291631
|
runId: ctx.runId,
|
|
289835
291632
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
289836
|
-
inputs: { goal: goal.slice(0, 500), members, docx: wantsDocx, pdf: wantsPdf }
|
|
291633
|
+
inputs: { goal: goal.slice(0, 500), members, repo: repoSlug || void 0, docx: wantsDocx, pdf: wantsPdf, publishIssues: wantsPublishIssues }
|
|
289837
291634
|
});
|
|
289838
291635
|
const logger = new LiteLogger({ cwd: ctx.cwd });
|
|
289839
291636
|
const emitLog = async (kind, payload) => {
|
|
@@ -289860,7 +291657,7 @@ ${this.help.usage}` };
|
|
|
289860
291657
|
`{"schemaVersion":"maria_lite_task_dist_v1","projectTitle":"...","goal":"...","members":["..."],`,
|
|
289861
291658
|
`"tasks":[{"id":"T-001","title":"...","description":"...","assignee":"member_name","domain":"...","dependencies":["T-xxx"],`,
|
|
289862
291659
|
`"effort":{"optimistic":{"hours":N,"days":N},"realistic":{"hours":N,"days":N},"pessimistic":{"hours":N,"days":N}},`,
|
|
289863
|
-
`"deadline":"YYYY-MM-DD","rationale":"...","slackDays":N,"onCriticalPath":bool}],`,
|
|
291660
|
+
`"deadline":"YYYY-MM-DD","rationale":"...","slackDays":N,"onCriticalPath":bool,"relatedFiles":["path/to/file.ts"]}],`,
|
|
289864
291661
|
`"interfaces":[{"id":"IF-001","between":["member_A","member_B"],"contract":"...","dataFormat":"...","timing":"...","owner":"manager",`,
|
|
289865
291662
|
`"boundaryScenarios":["scenario that falls in the gap between both owners' responsibilities"]}],`,
|
|
289866
291663
|
`"criticalPath":{"path":["T-001","T-003","T-006"],"totalRealisticDays":N,"totalPessimisticDays":N,"bottleneckTaskId":"T-xxx","bottleneckReason":"..."},`,
|
|
@@ -289969,6 +291766,16 @@ ${this.help.usage}` };
|
|
|
289969
291766
|
" - vulnerabilities: at least 3 structural weaknesses with mitigations.",
|
|
289970
291767
|
" - idleGaps: per-member idle periods with gap-filling activity recommendations.",
|
|
289971
291768
|
"",
|
|
291769
|
+
repoSlug ? [
|
|
291770
|
+
"",
|
|
291771
|
+
"11. **Repository-Aware Task Distribution (when repo context is provided):**",
|
|
291772
|
+
" - Use the repository file tree and key file contents to understand the codebase architecture.",
|
|
291773
|
+
" - For each task, populate `relatedFiles` with specific file/directory paths from the repo that the task will touch or depend on.",
|
|
291774
|
+
" - Use actual file paths from the tree \u2014 do NOT invent paths that don't exist in the repo.",
|
|
291775
|
+
" - Task descriptions should reference specific modules, services, and components observed in the codebase.",
|
|
291776
|
+
" - Domain assignments should reflect the actual code organization (e.g., if the repo has `src/auth/`, `src/payments/`, use those as domains)."
|
|
291777
|
+
].join("\n") : "",
|
|
291778
|
+
"",
|
|
289972
291779
|
`Today's date: ${todayStr}`,
|
|
289973
291780
|
`Team members: ${members.join(", ")}`,
|
|
289974
291781
|
"",
|
|
@@ -289977,6 +291784,8 @@ ${this.help.usage}` };
|
|
|
289977
291784
|
additionalContext ? `
|
|
289978
291785
|
Additional context:
|
|
289979
291786
|
${additionalContext.slice(0, 6e4)}` : "",
|
|
291787
|
+
repoContext ? `
|
|
291788
|
+
${repoContext.slice(0, 8e4)}` : "",
|
|
289980
291789
|
fix ? `
|
|
289981
291790
|
|
|
289982
291791
|
${fix}
|
|
@@ -290007,6 +291816,7 @@ ${fix}
|
|
|
290007
291816
|
for (const t2 of v.tasks) {
|
|
290008
291817
|
if (typeof t2.slackDays !== "number" || !Number.isFinite(t2.slackDays)) t2.slackDays = -1;
|
|
290009
291818
|
if (typeof t2.onCriticalPath !== "boolean") t2.onCriticalPath = false;
|
|
291819
|
+
if (!Array.isArray(t2.relatedFiles)) t2.relatedFiles = [];
|
|
290010
291820
|
}
|
|
290011
291821
|
if (!Array.isArray(v.interfaces)) v.interfaces = [];
|
|
290012
291822
|
for (const iface of v.interfaces) {
|
|
@@ -290081,16 +291891,28 @@ outDir=${outDirRel}` };
|
|
|
290081
291891
|
failures.push(`PDF_EXPORT_FAILED: ${msg || "unknown"}`);
|
|
290082
291892
|
}
|
|
290083
291893
|
}
|
|
291894
|
+
const issueUrls = [];
|
|
291895
|
+
const issueFailures = [];
|
|
291896
|
+
if (wantsPublishIssues && repoSlug) {
|
|
291897
|
+
for (const task of result.tasks) {
|
|
291898
|
+
const ir = await publishTaskAsIssue(repoSlug, task, result.projectTitle, ctx.cwd, ctx.abortSignal);
|
|
291899
|
+
if (ir.ok && ir.url) issueUrls.push(`${task.id}: ${ir.url}`);
|
|
291900
|
+
else issueFailures.push(`${task.id}: ${ir.error || "unknown"}`);
|
|
291901
|
+
}
|
|
291902
|
+
await emitLog("lite.task_dist.issues", { published: issueUrls.length, failed: issueFailures.length });
|
|
291903
|
+
}
|
|
290084
291904
|
const ok = failures.length === 0 && ((wantsDocx ? Boolean(exports2.docxRel) : true) && (wantsPdf ? Boolean(exports2.pdfRel) : true));
|
|
290085
291905
|
await writeJson9(path87__namespace.join(outDirAbs, "summary.json"), {
|
|
290086
291906
|
schemaVersion: "maria_lite_task_dist_summary_v1",
|
|
290087
291907
|
ok,
|
|
290088
291908
|
outDir: outDirRel,
|
|
291909
|
+
repo: repoSlug || void 0,
|
|
290089
291910
|
taskCount: result.tasks.length,
|
|
290090
291911
|
interfaceCount: result.interfaces.length,
|
|
290091
291912
|
memberCount: result.members.length,
|
|
290092
291913
|
exports: exports2,
|
|
290093
|
-
failures
|
|
291914
|
+
failures,
|
|
291915
|
+
issues: issueUrls.length > 0 ? { published: issueUrls, failures: issueFailures } : void 0
|
|
290094
291916
|
});
|
|
290095
291917
|
const primaryArtifacts = [];
|
|
290096
291918
|
if (exports2.docxRel) primaryArtifacts.push({ path: exports2.docxRel, kind: "primary" });
|
|
@@ -290108,18 +291930,25 @@ outDir=${outDirRel}` };
|
|
|
290108
291930
|
idleGaps: result.idleGaps.length,
|
|
290109
291931
|
docx: Boolean(exports2.docxRel),
|
|
290110
291932
|
pdf: Boolean(exports2.pdfRel),
|
|
290111
|
-
failures: failures.length
|
|
291933
|
+
failures: failures.length,
|
|
291934
|
+
issuesPublished: issueUrls.length
|
|
290112
291935
|
});
|
|
290113
291936
|
const lines = [];
|
|
290114
291937
|
lines.push(ok ? "OK: /task-distribution" : "WARN: /task-distribution (partial)");
|
|
290115
291938
|
lines.push(`outDir=${outDirRel}`);
|
|
290116
291939
|
lines.push(`tasks=${result.tasks.length} members=${result.members.length} interfaces=${result.interfaces.length}`);
|
|
291940
|
+
if (repoSlug) lines.push(`repo=${repoSlug}`);
|
|
290117
291941
|
if (exports2.docxRel) lines.push(`docx=${exports2.docxRel}`);
|
|
290118
291942
|
if (exports2.pdfRel) lines.push(`pdf=${exports2.pdfRel}`);
|
|
291943
|
+
if (issueUrls.length > 0) {
|
|
291944
|
+
lines.push(`issues_published=${issueUrls.length}`);
|
|
291945
|
+
for (const u of issueUrls) lines.push(` ${u}`);
|
|
291946
|
+
}
|
|
291947
|
+
if (issueFailures.length > 0) lines.push(`issue_failures=${issueFailures.length}`);
|
|
290119
291948
|
if (!ok) lines.push(`failures=${failures.length}`);
|
|
290120
291949
|
return {
|
|
290121
291950
|
text: lines.join("\n"),
|
|
290122
|
-
json: wantsJson ? { ok, outDir: outDirRel, taskCount: result.tasks.length, interfaces: result.interfaces.length, exports: exports2, failures, data: result } : { text: lines.join("\n") },
|
|
291951
|
+
json: wantsJson ? { ok, outDir: outDirRel, repo: repoSlug || void 0, taskCount: result.tasks.length, interfaces: result.interfaces.length, exports: exports2, failures, issues: { published: issueUrls, failures: issueFailures }, data: result } : { text: lines.join("\n") },
|
|
290123
291952
|
artifacts: [...primaryArtifacts, ...intermediateArtifacts]
|
|
290124
291953
|
};
|
|
290125
291954
|
}
|
|
@@ -290147,6 +291976,7 @@ var init_task_distribution_field = __esm({
|
|
|
290147
291976
|
init_soffice();
|
|
290148
291977
|
init_slides_helpers();
|
|
290149
291978
|
init_logger();
|
|
291979
|
+
init_gh_cli();
|
|
290150
291980
|
}
|
|
290151
291981
|
});
|
|
290152
291982
|
|
|
@@ -291460,6 +293290,254 @@ var init_phone_init_field = __esm({
|
|
|
291460
293290
|
}
|
|
291461
293291
|
});
|
|
291462
293292
|
|
|
293293
|
+
// commands/resource-manager.field.ts
|
|
293294
|
+
async function gFetch3(token, url, opts) {
|
|
293295
|
+
return fetch(url, {
|
|
293296
|
+
...opts,
|
|
293297
|
+
headers: { Authorization: `Bearer ${token}`, ...opts?.headers || {} }
|
|
293298
|
+
});
|
|
293299
|
+
}
|
|
293300
|
+
async function listBuildings2(token, _signal) {
|
|
293301
|
+
const res = await gFetch3(token, `${ADMIN_RESOURCES_BASE3}/buildings?maxResults=500`);
|
|
293302
|
+
if (!res.ok) {
|
|
293303
|
+
return { text: `Resource Manager: Admin SDK error ${res.status}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293304
|
+
}
|
|
293305
|
+
const data = await res.json();
|
|
293306
|
+
const items = Array.isArray(data.buildings) ? data.buildings : [];
|
|
293307
|
+
if (items.length === 0) {
|
|
293308
|
+
return { text: "No buildings found.", json: { ok: true, buildings: [] } };
|
|
293309
|
+
}
|
|
293310
|
+
const lines = ["## Buildings", ""];
|
|
293311
|
+
lines.push("| ID | Name | Floors | Description |");
|
|
293312
|
+
lines.push("|---|---|---|---|");
|
|
293313
|
+
for (const b of items) {
|
|
293314
|
+
const floors = Array.isArray(b.floorNames) ? b.floorNames.join(", ") : "";
|
|
293315
|
+
lines.push(`| ${b.buildingId || ""} | ${b.buildingName || ""} | ${floors} | ${b.description || ""} |`);
|
|
293316
|
+
}
|
|
293317
|
+
lines.push("", `Total: ${items.length} building(s)`);
|
|
293318
|
+
return { text: lines.join("\n"), json: { ok: true, buildings: items } };
|
|
293319
|
+
}
|
|
293320
|
+
async function listResources2(token, buildingId, capacity, _signal) {
|
|
293321
|
+
let url = `${ADMIN_RESOURCES_BASE3}/calendars?maxResults=500`;
|
|
293322
|
+
const res = await gFetch3(token, url);
|
|
293323
|
+
if (!res.ok) {
|
|
293324
|
+
return { text: `Resource Manager: Admin SDK error ${res.status}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293325
|
+
}
|
|
293326
|
+
const data = await res.json();
|
|
293327
|
+
let items = Array.isArray(data.items) ? data.items : [];
|
|
293328
|
+
if (buildingId) items = items.filter((r2) => String(r2.buildingId || "") === buildingId);
|
|
293329
|
+
if (capacity) items = items.filter((r2) => Number(r2.capacity || 0) >= capacity);
|
|
293330
|
+
if (items.length === 0) {
|
|
293331
|
+
return { text: "No resources found.", json: { ok: true, resources: [] } };
|
|
293332
|
+
}
|
|
293333
|
+
const lines = ["## Calendar Resources", ""];
|
|
293334
|
+
lines.push("| Name | Building | Capacity | Type | Floor | Email |");
|
|
293335
|
+
lines.push("|---|---|---|---|---|---|");
|
|
293336
|
+
for (const r2 of items) {
|
|
293337
|
+
lines.push(`| ${r2.resourceName || ""} | ${r2.buildingId || ""} | ${r2.capacity || ""} | ${r2.resourceType || ""} | ${r2.floorName || ""} | ${r2.resourceEmail || ""} |`);
|
|
293338
|
+
}
|
|
293339
|
+
lines.push("", `Total: ${items.length} resource(s)`);
|
|
293340
|
+
return { text: lines.join("\n"), json: { ok: true, resources: items } };
|
|
293341
|
+
}
|
|
293342
|
+
async function createBuilding2(token, name, floorsRaw, description, _signal) {
|
|
293343
|
+
const buildingId = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `bld-${Date.now()}`;
|
|
293344
|
+
const body = { buildingId, buildingName: name };
|
|
293345
|
+
if (description) body.description = description;
|
|
293346
|
+
if (floorsRaw) {
|
|
293347
|
+
const n = Number(floorsRaw);
|
|
293348
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
293349
|
+
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
293350
|
+
} else {
|
|
293351
|
+
body.floorNames = floorsRaw.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
293352
|
+
}
|
|
293353
|
+
}
|
|
293354
|
+
const res = await gFetch3(token, `${ADMIN_RESOURCES_BASE3}/buildings`, {
|
|
293355
|
+
method: "POST",
|
|
293356
|
+
headers: { "Content-Type": "application/json" },
|
|
293357
|
+
body: JSON.stringify(body)
|
|
293358
|
+
});
|
|
293359
|
+
if (!res.ok) {
|
|
293360
|
+
const errText = await res.text().catch(() => "");
|
|
293361
|
+
if (res.status === 403) return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293362
|
+
return { text: `Failed to create building: ${res.status} ${errText}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293363
|
+
}
|
|
293364
|
+
const created = await res.json();
|
|
293365
|
+
return {
|
|
293366
|
+
text: `Building created: ${created.buildingName} (${created.buildingId})`,
|
|
293367
|
+
json: { ok: true, building: created }
|
|
293368
|
+
};
|
|
293369
|
+
}
|
|
293370
|
+
async function createResource2(token, opts, _signal) {
|
|
293371
|
+
const resourceId = opts.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `res-${Date.now()}`;
|
|
293372
|
+
const body = { resourceId, resourceName: opts.name, resourceType: opts.resourceType };
|
|
293373
|
+
if (opts.buildingId) body.buildingId = opts.buildingId;
|
|
293374
|
+
if (opts.capacity) body.capacity = opts.capacity;
|
|
293375
|
+
if (opts.floorName) body.floorName = opts.floorName;
|
|
293376
|
+
if (opts.description) body.resourceDescription = opts.description;
|
|
293377
|
+
const res = await gFetch3(token, `${ADMIN_RESOURCES_BASE3}/calendars`, {
|
|
293378
|
+
method: "POST",
|
|
293379
|
+
headers: { "Content-Type": "application/json" },
|
|
293380
|
+
body: JSON.stringify(body)
|
|
293381
|
+
});
|
|
293382
|
+
if (!res.ok) {
|
|
293383
|
+
const errText = await res.text().catch(() => "");
|
|
293384
|
+
if (res.status === 403) return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293385
|
+
return { text: `Failed to create resource: ${res.status} ${errText}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293386
|
+
}
|
|
293387
|
+
const created = await res.json();
|
|
293388
|
+
return {
|
|
293389
|
+
text: `Resource created: ${created.resourceName} (${created.resourceEmail})`,
|
|
293390
|
+
json: { ok: true, resource: created }
|
|
293391
|
+
};
|
|
293392
|
+
}
|
|
293393
|
+
async function updateBuilding(token, buildingId, body, _signal) {
|
|
293394
|
+
const res = await gFetch3(token, `${ADMIN_RESOURCES_BASE3}/buildings/${encodeURIComponent(buildingId)}`, {
|
|
293395
|
+
method: "PATCH",
|
|
293396
|
+
headers: { "Content-Type": "application/json" },
|
|
293397
|
+
body: JSON.stringify(body)
|
|
293398
|
+
});
|
|
293399
|
+
if (!res.ok) {
|
|
293400
|
+
const errText = await res.text().catch(() => "");
|
|
293401
|
+
if (res.status === 403) return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293402
|
+
if (res.status === 404) return { text: `Building "${buildingId}" not found.`, json: { ok: false, error: "not_found" } };
|
|
293403
|
+
return { text: `Failed to update building: ${res.status} ${errText}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293404
|
+
}
|
|
293405
|
+
const updated = await res.json();
|
|
293406
|
+
return {
|
|
293407
|
+
text: `Building updated: ${updated.buildingName} (${updated.buildingId})`,
|
|
293408
|
+
json: { ok: true, building: updated }
|
|
293409
|
+
};
|
|
293410
|
+
}
|
|
293411
|
+
async function updateResource(token, resourceId, body, _signal) {
|
|
293412
|
+
const res = await gFetch3(token, `${ADMIN_RESOURCES_BASE3}/calendars/${encodeURIComponent(resourceId)}`, {
|
|
293413
|
+
method: "PATCH",
|
|
293414
|
+
headers: { "Content-Type": "application/json" },
|
|
293415
|
+
body: JSON.stringify(body)
|
|
293416
|
+
});
|
|
293417
|
+
if (!res.ok) {
|
|
293418
|
+
const errText = await res.text().catch(() => "");
|
|
293419
|
+
if (res.status === 403) return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293420
|
+
if (res.status === 404) return { text: `Resource "${resourceId}" not found.`, json: { ok: false, error: "not_found" } };
|
|
293421
|
+
return { text: `Failed to update resource: ${res.status} ${errText}`, json: { ok: false, error: `Admin SDK: ${res.status}` } };
|
|
293422
|
+
}
|
|
293423
|
+
const updated = await res.json();
|
|
293424
|
+
return {
|
|
293425
|
+
text: `Resource updated: ${updated.resourceName} (${updated.resourceEmail})`,
|
|
293426
|
+
json: { ok: true, resource: updated }
|
|
293427
|
+
};
|
|
293428
|
+
}
|
|
293429
|
+
function createResourceManagerField() {
|
|
293430
|
+
const worker = new ResourceManagerWorker();
|
|
293431
|
+
const checker = new ResourceManagerChecker();
|
|
293432
|
+
return { worker, checker };
|
|
293433
|
+
}
|
|
293434
|
+
var ADMIN_RESOURCES_BASE3, ResourceManagerWorker, ResourceManagerChecker;
|
|
293435
|
+
var init_resource_manager_field = __esm({
|
|
293436
|
+
"commands/resource-manager.field.ts"() {
|
|
293437
|
+
init_base2();
|
|
293438
|
+
init_google_oauth();
|
|
293439
|
+
ADMIN_RESOURCES_BASE3 = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
|
|
293440
|
+
ResourceManagerWorker = class extends LiteWorkerAgent {
|
|
293441
|
+
commandId = "resource-manager";
|
|
293442
|
+
help = {
|
|
293443
|
+
command: "/resource-manager",
|
|
293444
|
+
description: "Manage Google Workspace buildings and calendar resources.",
|
|
293445
|
+
usage: '/resource-manager buildings\n/resource-manager resources [--building <buildingId>] [--capacity <n>]\n/resource-manager create-building --name "Main Office" [--floors <n>] [--description <text>]\n/resource-manager create-resource --name "Room A" --building <buildingId> [--capacity <n>] [--type CONFERENCE_ROOM] [--floor <floor>] [--description <text>]\n/resource-manager update-building --id <buildingId> [--name <name>] [--floors <n>] [--description <text>]\n/resource-manager update-resource --id <resourceId> [--name <name>] [--building <buildingId>] [--capacity <n>] [--type <type>] [--floor <floor>] [--description <text>]',
|
|
293446
|
+
runOnEmpty: true
|
|
293447
|
+
};
|
|
293448
|
+
steps = [
|
|
293449
|
+
{
|
|
293450
|
+
id: "resource-manager.exec",
|
|
293451
|
+
title: "Resource Manager",
|
|
293452
|
+
async run(ctx) {
|
|
293453
|
+
const mgr = new GoogleOAuthManager();
|
|
293454
|
+
const token = await mgr.getValidToken(ctx.abortSignal);
|
|
293455
|
+
if (!token) {
|
|
293456
|
+
return {
|
|
293457
|
+
text: "Resource Manager: Google account not connected. Run /google-connect first.",
|
|
293458
|
+
json: { ok: false, error: "not_connected" }
|
|
293459
|
+
};
|
|
293460
|
+
}
|
|
293461
|
+
const sub = (ctx.parsed.subcommand || ctx.parsed.args?.[0] || "buildings").toLowerCase();
|
|
293462
|
+
if (sub === "buildings" || sub === "list-buildings") {
|
|
293463
|
+
return await listBuildings2(token, ctx.abortSignal);
|
|
293464
|
+
}
|
|
293465
|
+
if (sub === "resources" || sub === "list-resources") {
|
|
293466
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
|
|
293467
|
+
const capacityRaw = String(ctx.parsed.options.capacity || "").trim();
|
|
293468
|
+
const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
|
|
293469
|
+
return await listResources2(token, buildingId, capacity, ctx.abortSignal);
|
|
293470
|
+
}
|
|
293471
|
+
if (sub === "create-building") {
|
|
293472
|
+
const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
|
|
293473
|
+
if (!name) return { text: "resource-manager create-building: --name is required" };
|
|
293474
|
+
const floorsRaw = String(ctx.parsed.options.floors || "").trim();
|
|
293475
|
+
const description = String(ctx.parsed.options.description || "").trim() || void 0;
|
|
293476
|
+
return await createBuilding2(token, name, floorsRaw, description, ctx.abortSignal);
|
|
293477
|
+
}
|
|
293478
|
+
if (sub === "create-resource") {
|
|
293479
|
+
const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
|
|
293480
|
+
if (!name) return { text: "resource-manager create-resource: --name is required" };
|
|
293481
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
|
|
293482
|
+
const capacity = Number(ctx.parsed.options.capacity || 0) || void 0;
|
|
293483
|
+
const resourceType = String(ctx.parsed.options.type || "CONFERENCE_ROOM").trim();
|
|
293484
|
+
const floorName = String(ctx.parsed.options.floor || "").trim() || void 0;
|
|
293485
|
+
const description = String(ctx.parsed.options.description || "").trim() || void 0;
|
|
293486
|
+
return await createResource2(token, { name, buildingId, capacity, resourceType, floorName, description }, ctx.abortSignal);
|
|
293487
|
+
}
|
|
293488
|
+
if (sub === "update-building") {
|
|
293489
|
+
const id = String(ctx.parsed.options.id || "").trim();
|
|
293490
|
+
if (!id) return { text: "resource-manager update-building: --id is required" };
|
|
293491
|
+
const body = {};
|
|
293492
|
+
const name = String(ctx.parsed.options.name || "").trim();
|
|
293493
|
+
if (name) body.buildingName = name;
|
|
293494
|
+
const floorsRaw = String(ctx.parsed.options.floors || "").trim();
|
|
293495
|
+
if (floorsRaw) {
|
|
293496
|
+
const n = Number(floorsRaw);
|
|
293497
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
293498
|
+
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
293499
|
+
} else {
|
|
293500
|
+
body.floorNames = floorsRaw.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
293501
|
+
}
|
|
293502
|
+
}
|
|
293503
|
+
const description = String(ctx.parsed.options.description || "").trim();
|
|
293504
|
+
if (description) body.description = description;
|
|
293505
|
+
if (Object.keys(body).length === 0) return { text: "resource-manager update-building: at least one field to update is required (--name, --floors, --description)" };
|
|
293506
|
+
return await updateBuilding(token, id, body, ctx.abortSignal);
|
|
293507
|
+
}
|
|
293508
|
+
if (sub === "update-resource") {
|
|
293509
|
+
const id = String(ctx.parsed.options.id || "").trim();
|
|
293510
|
+
if (!id) return { text: "resource-manager update-resource: --id is required" };
|
|
293511
|
+
const body = {};
|
|
293512
|
+
const name = String(ctx.parsed.options.name || "").trim();
|
|
293513
|
+
if (name) body.resourceName = name;
|
|
293514
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim();
|
|
293515
|
+
if (buildingId) body.buildingId = buildingId;
|
|
293516
|
+
const capacity = Number(ctx.parsed.options.capacity || 0);
|
|
293517
|
+
if (capacity) body.capacity = capacity;
|
|
293518
|
+
const resourceType = String(ctx.parsed.options.type || "").trim();
|
|
293519
|
+
if (resourceType) body.resourceType = resourceType;
|
|
293520
|
+
const floorName = String(ctx.parsed.options.floor || "").trim();
|
|
293521
|
+
if (floorName) body.floorName = floorName;
|
|
293522
|
+
const description = String(ctx.parsed.options.description || "").trim();
|
|
293523
|
+
if (description) body.resourceDescription = description;
|
|
293524
|
+
if (Object.keys(body).length === 0) return { text: "resource-manager update-resource: at least one field to update is required" };
|
|
293525
|
+
return await updateResource(token, id, body, ctx.abortSignal);
|
|
293526
|
+
}
|
|
293527
|
+
return { text: `resource-manager: unknown subcommand "${sub}". Use buildings, resources, create-building, create-resource, update-building, or update-resource.` };
|
|
293528
|
+
}
|
|
293529
|
+
}
|
|
293530
|
+
];
|
|
293531
|
+
};
|
|
293532
|
+
ResourceManagerChecker = class extends LiteCheckerAgent {
|
|
293533
|
+
commandId = "resource-manager";
|
|
293534
|
+
async check(_ctx, _input) {
|
|
293535
|
+
return { outcome: "PASS", reasons: ["resource_manager_ok"] };
|
|
293536
|
+
}
|
|
293537
|
+
};
|
|
293538
|
+
}
|
|
293539
|
+
});
|
|
293540
|
+
|
|
291463
293541
|
// runtime/ext/ext-loader.ts
|
|
291464
293542
|
var ext_loader_exports = {};
|
|
291465
293543
|
__export(ext_loader_exports, {
|
|
@@ -291987,6 +294065,7 @@ function registerCoreLiteCommands(registry) {
|
|
|
291987
294065
|
safeRegister(registry, "gcal", createGcalField);
|
|
291988
294066
|
safeRegister(registry, "gdrive", createGdriveField);
|
|
291989
294067
|
safeRegister(registry, "gmeet", createGmeetField);
|
|
294068
|
+
safeRegister(registry, "resource-manager", createResourceManagerField);
|
|
291990
294069
|
}
|
|
291991
294070
|
function createLiteCommandProviders() {
|
|
291992
294071
|
const core2 = {
|
|
@@ -292116,6 +294195,7 @@ var init_command_providers = __esm({
|
|
|
292116
294195
|
init_phone_hp_import_field();
|
|
292117
294196
|
init_phone_deploy_field();
|
|
292118
294197
|
init_phone_init_field();
|
|
294198
|
+
init_resource_manager_field();
|
|
292119
294199
|
init_ext_loader();
|
|
292120
294200
|
init_beta_loader();
|
|
292121
294201
|
}
|