@bonginkan/maria-lite 6.3.0 → 6.3.2
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 +32 -2
- package/dist/cli.cjs +2143 -211
- 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.2";
|
|
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,1529 @@ var init_phone_routes = __esm({
|
|
|
256282
256283
|
init_desktop_server_helpers();
|
|
256283
256284
|
}
|
|
256284
256285
|
});
|
|
256286
|
+
|
|
256287
|
+
// services/google/admin-resources.ts
|
|
256288
|
+
async function adminFetch(token, url, init) {
|
|
256289
|
+
const headers = {
|
|
256290
|
+
Authorization: `Bearer ${token}`,
|
|
256291
|
+
...init?.headers || {}
|
|
256292
|
+
};
|
|
256293
|
+
return fetch(url, { ...init, headers });
|
|
256294
|
+
}
|
|
256295
|
+
function slugify2(name, prefix) {
|
|
256296
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `${prefix}-${Date.now()}`;
|
|
256297
|
+
}
|
|
256298
|
+
function parseFloorNames(raw) {
|
|
256299
|
+
const trimmed = raw.trim();
|
|
256300
|
+
if (!trimmed) return [];
|
|
256301
|
+
const n = Number(trimmed);
|
|
256302
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
256303
|
+
return Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
256304
|
+
}
|
|
256305
|
+
return trimmed.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
256306
|
+
}
|
|
256307
|
+
function toBuilding(raw) {
|
|
256308
|
+
return {
|
|
256309
|
+
buildingId: String(raw.buildingId || ""),
|
|
256310
|
+
buildingName: String(raw.buildingName || ""),
|
|
256311
|
+
description: String(raw.description || ""),
|
|
256312
|
+
floorNames: Array.isArray(raw.floorNames) ? raw.floorNames.map(String) : []
|
|
256313
|
+
};
|
|
256314
|
+
}
|
|
256315
|
+
function toResource(raw) {
|
|
256316
|
+
return {
|
|
256317
|
+
resourceId: String(raw.resourceId || ""),
|
|
256318
|
+
resourceEmail: String(raw.resourceEmail || ""),
|
|
256319
|
+
resourceName: String(raw.resourceName || ""),
|
|
256320
|
+
resourceType: String(raw.resourceType || ""),
|
|
256321
|
+
capacity: Number(raw.capacity || 0),
|
|
256322
|
+
buildingId: String(raw.buildingId || ""),
|
|
256323
|
+
floorName: String(raw.floorName || ""),
|
|
256324
|
+
userVisibleDescription: String(raw.userVisibleDescription || ""),
|
|
256325
|
+
resourceDescription: String(raw.resourceDescription || ""),
|
|
256326
|
+
generatedResourceName: String(raw.generatedResourceName || "")
|
|
256327
|
+
};
|
|
256328
|
+
}
|
|
256329
|
+
async function parseErrorText(res) {
|
|
256330
|
+
return res.text().catch(() => "");
|
|
256331
|
+
}
|
|
256332
|
+
function apiError(status, errText) {
|
|
256333
|
+
if (status === 403) return { ok: false, error: "permission_denied", status };
|
|
256334
|
+
if (status === 404) return { ok: false, error: "not_found", status };
|
|
256335
|
+
return { ok: false, error: `Admin SDK: ${status} ${errText.slice(0, 200)}`, status };
|
|
256336
|
+
}
|
|
256337
|
+
async function listBuildingsRaw(token, signal) {
|
|
256338
|
+
const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings?maxResults=500`, signal ? { signal } : void 0);
|
|
256339
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256340
|
+
const data = await res.json();
|
|
256341
|
+
const items = Array.isArray(data.buildings) ? data.buildings : [];
|
|
256342
|
+
return { ok: true, data: items.map(toBuilding) };
|
|
256343
|
+
}
|
|
256344
|
+
async function createBuildingRaw(token, params, signal) {
|
|
256345
|
+
const buildingId = params.buildingId || slugify2(params.name, "bld");
|
|
256346
|
+
const body = { buildingId, buildingName: params.name };
|
|
256347
|
+
if (params.description) body.description = params.description;
|
|
256348
|
+
if (params.floorNames) {
|
|
256349
|
+
body.floorNames = params.floorNames;
|
|
256350
|
+
} else if (params.floors) {
|
|
256351
|
+
body.floorNames = parseFloorNames(params.floors);
|
|
256352
|
+
}
|
|
256353
|
+
const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings`, {
|
|
256354
|
+
method: "POST",
|
|
256355
|
+
headers: { "Content-Type": "application/json" },
|
|
256356
|
+
body: JSON.stringify(body),
|
|
256357
|
+
...signal ? { signal } : {}
|
|
256358
|
+
});
|
|
256359
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256360
|
+
return { ok: true, data: toBuilding(await res.json()) };
|
|
256361
|
+
}
|
|
256362
|
+
async function updateBuildingRaw(token, buildingId, body, signal) {
|
|
256363
|
+
const res = await adminFetch(
|
|
256364
|
+
token,
|
|
256365
|
+
`${ADMIN_RESOURCES_BASE}/buildings/${encodeURIComponent(buildingId)}`,
|
|
256366
|
+
{ method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), ...signal ? { signal } : {} }
|
|
256367
|
+
);
|
|
256368
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256369
|
+
return { ok: true, data: toBuilding(await res.json()) };
|
|
256370
|
+
}
|
|
256371
|
+
async function listResourcesRaw(token, opts, signal) {
|
|
256372
|
+
let url = `${ADMIN_RESOURCES_BASE}/calendars?maxResults=500`;
|
|
256373
|
+
if (opts?.buildingId) {
|
|
256374
|
+
url += `&query=buildingId="${encodeURIComponent(opts.buildingId)}"`;
|
|
256375
|
+
}
|
|
256376
|
+
const res = await adminFetch(token, url, signal ? { signal } : void 0);
|
|
256377
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256378
|
+
const data = await res.json();
|
|
256379
|
+
let items = Array.isArray(data.items) ? data.items : [];
|
|
256380
|
+
if (opts?.capacity && opts.capacity > 0) {
|
|
256381
|
+
items = items.filter((r2) => Number(r2.capacity || 0) >= opts.capacity);
|
|
256382
|
+
}
|
|
256383
|
+
return { ok: true, data: items.map(toResource) };
|
|
256384
|
+
}
|
|
256385
|
+
async function getResourceRaw(token, resourceId, signal) {
|
|
256386
|
+
const res = await adminFetch(
|
|
256387
|
+
token,
|
|
256388
|
+
`${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`,
|
|
256389
|
+
signal ? { signal } : void 0
|
|
256390
|
+
);
|
|
256391
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256392
|
+
return { ok: true, data: toResource(await res.json()) };
|
|
256393
|
+
}
|
|
256394
|
+
async function createResourceRaw(token, params, signal) {
|
|
256395
|
+
const body = {
|
|
256396
|
+
resourceName: params.name,
|
|
256397
|
+
resourceType: params.resourceType || "CONFERENCE_ROOM"
|
|
256398
|
+
};
|
|
256399
|
+
if (params.resourceId) body.resourceId = params.resourceId;
|
|
256400
|
+
if (params.buildingId) body.buildingId = params.buildingId;
|
|
256401
|
+
if (params.capacity) body.capacity = params.capacity;
|
|
256402
|
+
if (params.floorName) body.floorName = params.floorName;
|
|
256403
|
+
if (params.description) body.resourceDescription = params.description;
|
|
256404
|
+
const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars`, {
|
|
256405
|
+
method: "POST",
|
|
256406
|
+
headers: { "Content-Type": "application/json" },
|
|
256407
|
+
body: JSON.stringify(body),
|
|
256408
|
+
...signal ? { signal } : {}
|
|
256409
|
+
});
|
|
256410
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256411
|
+
return { ok: true, data: toResource(await res.json()) };
|
|
256412
|
+
}
|
|
256413
|
+
async function updateResourceRaw(token, resourceId, body, signal) {
|
|
256414
|
+
const res = await adminFetch(
|
|
256415
|
+
token,
|
|
256416
|
+
`${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`,
|
|
256417
|
+
{ method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), ...signal ? { signal } : {} }
|
|
256418
|
+
);
|
|
256419
|
+
if (!res.ok) return apiError(res.status, await parseErrorText(res));
|
|
256420
|
+
return { ok: true, data: toResource(await res.json()) };
|
|
256421
|
+
}
|
|
256422
|
+
async function fetchExistingBuildingIds(token, signal) {
|
|
256423
|
+
const ids = /* @__PURE__ */ new Set();
|
|
256424
|
+
try {
|
|
256425
|
+
const result = await listBuildingsRaw(token, signal);
|
|
256426
|
+
if (result.ok && result.data) {
|
|
256427
|
+
for (const b of result.data) {
|
|
256428
|
+
if (b.buildingId) ids.add(b.buildingId);
|
|
256429
|
+
}
|
|
256430
|
+
}
|
|
256431
|
+
} catch {
|
|
256432
|
+
}
|
|
256433
|
+
return ids;
|
|
256434
|
+
}
|
|
256435
|
+
var ADMIN_RESOURCES_BASE;
|
|
256436
|
+
var init_admin_resources = __esm({
|
|
256437
|
+
"services/google/admin-resources.ts"() {
|
|
256438
|
+
ADMIN_RESOURCES_BASE = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
|
|
256439
|
+
}
|
|
256440
|
+
});
|
|
256441
|
+
|
|
256442
|
+
// services/desktop/routes/calendar-api-routes.ts
|
|
256443
|
+
async function getToken2() {
|
|
256444
|
+
const mgr = new GoogleOAuthManager();
|
|
256445
|
+
return mgr.getValidToken();
|
|
256446
|
+
}
|
|
256447
|
+
function notConnected2(res) {
|
|
256448
|
+
respondJson2(res, 200, { ok: false, error: "not_connected" });
|
|
256449
|
+
return true;
|
|
256450
|
+
}
|
|
256451
|
+
function resolveProjectId2() {
|
|
256452
|
+
return process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || process.env.FIREBASE_PROJECT_ID || process.env.GCP_PROJECT_ID || "";
|
|
256453
|
+
}
|
|
256454
|
+
async function getFirestore2() {
|
|
256455
|
+
if (_firestoreInstance2) return _firestoreInstance2;
|
|
256456
|
+
if (_firestoreInitPromise2) return _firestoreInitPromise2;
|
|
256457
|
+
_firestoreInitPromise2 = (async () => {
|
|
256458
|
+
const projectId = resolveProjectId2();
|
|
256459
|
+
if (!projectId) return null;
|
|
256460
|
+
try {
|
|
256461
|
+
const { Firestore: Firestore3 } = await Promise.resolve().then(() => __toESM(require_src21(), 1));
|
|
256462
|
+
_firestoreInstance2 = new Firestore3({ projectId });
|
|
256463
|
+
return _firestoreInstance2;
|
|
256464
|
+
} catch {
|
|
256465
|
+
return null;
|
|
256466
|
+
}
|
|
256467
|
+
})();
|
|
256468
|
+
return _firestoreInitPromise2;
|
|
256469
|
+
}
|
|
256470
|
+
function firestoreUnavailable(res) {
|
|
256471
|
+
respondJson2(res, 503, {
|
|
256472
|
+
ok: false,
|
|
256473
|
+
error: "firestore_unavailable",
|
|
256474
|
+
message: "Firestore is not configured. Set GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT env var."
|
|
256475
|
+
});
|
|
256476
|
+
return true;
|
|
256477
|
+
}
|
|
256478
|
+
function generateReservationId() {
|
|
256479
|
+
const now = /* @__PURE__ */ new Date();
|
|
256480
|
+
const datePart = [
|
|
256481
|
+
now.getFullYear(),
|
|
256482
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
256483
|
+
String(now.getDate()).padStart(2, "0")
|
|
256484
|
+
].join("");
|
|
256485
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
256486
|
+
let random = "";
|
|
256487
|
+
for (let i2 = 0; i2 < 5; i2++) {
|
|
256488
|
+
random += chars[Math.floor(Math.random() * chars.length)];
|
|
256489
|
+
}
|
|
256490
|
+
return `R-${datePart}-${random}`;
|
|
256491
|
+
}
|
|
256492
|
+
function buildRrule(type2, count, until) {
|
|
256493
|
+
const freqMap = {
|
|
256494
|
+
daily: "DAILY",
|
|
256495
|
+
weekly: "WEEKLY",
|
|
256496
|
+
biweekly: "WEEKLY",
|
|
256497
|
+
monthly: "MONTHLY"
|
|
256498
|
+
};
|
|
256499
|
+
const freq = freqMap[type2];
|
|
256500
|
+
if (!freq) return null;
|
|
256501
|
+
let rule = `RRULE:FREQ=${freq}`;
|
|
256502
|
+
if (type2 === "biweekly") rule += ";INTERVAL=2";
|
|
256503
|
+
if (count) rule += `;COUNT=${count}`;
|
|
256504
|
+
else if (until) rule += `;UNTIL=${until.replace(/-/g, "")}T235959Z`;
|
|
256505
|
+
return rule;
|
|
256506
|
+
}
|
|
256507
|
+
async function fetchResources(token, buildingId, minCapacity) {
|
|
256508
|
+
const result = await listResourcesRaw(token, { buildingId, capacity: minCapacity });
|
|
256509
|
+
if (!result.ok || !result.data) return [];
|
|
256510
|
+
return result.data;
|
|
256511
|
+
}
|
|
256512
|
+
function findSlots(params) {
|
|
256513
|
+
const {
|
|
256514
|
+
busyMap,
|
|
256515
|
+
required,
|
|
256516
|
+
optional,
|
|
256517
|
+
resourceEmails,
|
|
256518
|
+
durationMs,
|
|
256519
|
+
dateFrom,
|
|
256520
|
+
dateTo,
|
|
256521
|
+
workStartHour,
|
|
256522
|
+
workEndHour,
|
|
256523
|
+
offset,
|
|
256524
|
+
bufferMinutes = DEFAULT_BUFFER_MINUTES,
|
|
256525
|
+
timeZone = DEFAULT_TIMEZONE
|
|
256526
|
+
} = params;
|
|
256527
|
+
const minBufferMs = bufferMinutes * 60 * 1e3;
|
|
256528
|
+
const allTargets = [...resourceEmails, ...required, ...optional];
|
|
256529
|
+
const candidates = [];
|
|
256530
|
+
const [fromY, fromM, fromD] = dateFrom.split("-").map(Number);
|
|
256531
|
+
const [toY, toM, toD] = dateTo.split("-").map(Number);
|
|
256532
|
+
const dStart = new Date(Date.UTC(fromY, fromM - 1, fromD));
|
|
256533
|
+
const dEnd = new Date(Date.UTC(toY, toM - 1, toD));
|
|
256534
|
+
for (let d = new Date(dStart); d <= dEnd; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
256535
|
+
const yy = d.getUTCFullYear();
|
|
256536
|
+
const mm = d.getUTCMonth() + 1;
|
|
256537
|
+
const dd = d.getUTCDate();
|
|
256538
|
+
const dayStr = `${yy}-${String(mm).padStart(2, "0")}-${String(dd).padStart(2, "0")}`;
|
|
256539
|
+
const weekdayRef = new Date(Date.UTC(yy, mm - 1, dd, 12, 0, 0));
|
|
256540
|
+
if (weekdayRef.getUTCDay() === 0 || weekdayRef.getUTCDay() === 6) continue;
|
|
256541
|
+
const slotDayStart = makeDateInTz(dayStr, workStartHour, 0, timeZone);
|
|
256542
|
+
const slotDayEnd = makeDateInTz(dayStr, workEndHour, 0, timeZone);
|
|
256543
|
+
for (let t2 = slotDayStart.getTime(); t2 + durationMs <= slotDayEnd.getTime(); t2 += SLOT_STEP_MS) {
|
|
256544
|
+
const slotS = t2;
|
|
256545
|
+
const slotE = t2 + durationMs;
|
|
256546
|
+
let requiredOk = true;
|
|
256547
|
+
let resourcesOk = true;
|
|
256548
|
+
let availCount = 0;
|
|
256549
|
+
const missingOpt = [];
|
|
256550
|
+
const availableResourceEmails = [];
|
|
256551
|
+
for (const email of resourceEmails) {
|
|
256552
|
+
const busy = busyMap[email] || [];
|
|
256553
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256554
|
+
if (isBusy) {
|
|
256555
|
+
resourcesOk = false;
|
|
256556
|
+
} else {
|
|
256557
|
+
availableResourceEmails.push(email);
|
|
256558
|
+
availCount++;
|
|
256559
|
+
}
|
|
256560
|
+
}
|
|
256561
|
+
if (resourceEmails.length > 0 && !resourcesOk && availableResourceEmails.length === 0) continue;
|
|
256562
|
+
for (const email of required) {
|
|
256563
|
+
const busy = busyMap[email] || [];
|
|
256564
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256565
|
+
if (isBusy) {
|
|
256566
|
+
requiredOk = false;
|
|
256567
|
+
} else {
|
|
256568
|
+
availCount++;
|
|
256569
|
+
}
|
|
256570
|
+
}
|
|
256571
|
+
if (!requiredOk) continue;
|
|
256572
|
+
for (const email of optional) {
|
|
256573
|
+
const busy = busyMap[email] || [];
|
|
256574
|
+
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
256575
|
+
if (isBusy) {
|
|
256576
|
+
missingOpt.push(email);
|
|
256577
|
+
} else {
|
|
256578
|
+
availCount++;
|
|
256579
|
+
}
|
|
256580
|
+
}
|
|
256581
|
+
const mustBeAvailable = [...resourceEmails.filter((e2) => availableResourceEmails.includes(e2)), ...required];
|
|
256582
|
+
let bufBefore = Infinity;
|
|
256583
|
+
let bufAfter = Infinity;
|
|
256584
|
+
for (const email of mustBeAvailable) {
|
|
256585
|
+
const busy = busyMap[email] || [];
|
|
256586
|
+
let gapBefore = slotS - slotDayStart.getTime();
|
|
256587
|
+
let gapAfter = slotDayEnd.getTime() - slotE;
|
|
256588
|
+
for (const b of busy) {
|
|
256589
|
+
if (b.e <= slotS) gapBefore = Math.min(gapBefore, slotS - b.e);
|
|
256590
|
+
if (b.s >= slotE) gapAfter = Math.min(gapAfter, b.s - slotE);
|
|
256591
|
+
}
|
|
256592
|
+
bufBefore = Math.min(bufBefore, gapBefore);
|
|
256593
|
+
bufAfter = Math.min(bufAfter, gapAfter);
|
|
256594
|
+
}
|
|
256595
|
+
if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
|
|
256596
|
+
if (!isFinite(bufAfter)) bufAfter = 12 * 60 * 60 * 1e3;
|
|
256597
|
+
candidates.push({
|
|
256598
|
+
start: slotS,
|
|
256599
|
+
end: slotE,
|
|
256600
|
+
allAvailable: availCount === allTargets.length,
|
|
256601
|
+
availableCount: availCount,
|
|
256602
|
+
totalParticipants: allTargets.length,
|
|
256603
|
+
missingOptional: missingOpt,
|
|
256604
|
+
bufferBefore: bufBefore,
|
|
256605
|
+
bufferAfter: bufAfter,
|
|
256606
|
+
resourceEmails: availableResourceEmails
|
|
256607
|
+
});
|
|
256608
|
+
}
|
|
256609
|
+
}
|
|
256610
|
+
const hasSufficientBuffer = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
|
|
256611
|
+
const bufferImbalance = (c) => Math.abs(c.bufferBefore - c.bufferAfter);
|
|
256612
|
+
candidates.sort((a, b) => {
|
|
256613
|
+
if (a.allAvailable !== b.allAvailable) return a.allAvailable ? -1 : 1;
|
|
256614
|
+
if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
|
|
256615
|
+
if (a.missingOptional.length !== b.missingOptional.length) return a.missingOptional.length - b.missingOptional.length;
|
|
256616
|
+
const optIdx = (m2) => {
|
|
256617
|
+
let worst = -1;
|
|
256618
|
+
for (const e2 of m2) {
|
|
256619
|
+
const i2 = optional.indexOf(e2);
|
|
256620
|
+
if (i2 > worst) worst = i2;
|
|
256621
|
+
}
|
|
256622
|
+
return worst;
|
|
256623
|
+
};
|
|
256624
|
+
const ai = optIdx(a.missingOptional);
|
|
256625
|
+
const bi = optIdx(b.missingOptional);
|
|
256626
|
+
if (ai !== bi) return ai - bi;
|
|
256627
|
+
const aSuf = hasSufficientBuffer(a);
|
|
256628
|
+
const bSuf = hasSufficientBuffer(b);
|
|
256629
|
+
if (aSuf && bSuf) {
|
|
256630
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
256631
|
+
return bufferImbalance(a) - bufferImbalance(b);
|
|
256632
|
+
}
|
|
256633
|
+
if (aSuf !== bSuf) return aSuf ? -1 : 1;
|
|
256634
|
+
const aTot = a.bufferBefore + a.bufferAfter;
|
|
256635
|
+
const bTot = b.bufferBefore + b.bufferAfter;
|
|
256636
|
+
if (aTot !== bTot) return bTot - aTot;
|
|
256637
|
+
return bufferImbalance(a) - bufferImbalance(b);
|
|
256638
|
+
});
|
|
256639
|
+
const nonOverlapping = [];
|
|
256640
|
+
const needed = (offset + 1) * TOP_SLOT_COUNT + 1;
|
|
256641
|
+
for (const c of candidates) {
|
|
256642
|
+
const overlaps = nonOverlapping.some((t2) => t2.start < c.end && t2.end > c.start);
|
|
256643
|
+
if (!overlaps) nonOverlapping.push(c);
|
|
256644
|
+
if (nonOverlapping.length >= needed) break;
|
|
256645
|
+
}
|
|
256646
|
+
const start = offset * TOP_SLOT_COUNT;
|
|
256647
|
+
const page = nonOverlapping.slice(start, start + TOP_SLOT_COUNT);
|
|
256648
|
+
return { slots: page, hasMore: nonOverlapping.length > start + TOP_SLOT_COUNT };
|
|
256649
|
+
}
|
|
256650
|
+
async function handleCalendarApiRoute(method, pathname, req, res) {
|
|
256651
|
+
if (!pathname.startsWith("/api/calendar/")) return false;
|
|
256652
|
+
if (method === "GET" && pathname === "/api/calendar/resources") {
|
|
256653
|
+
const token = await getToken2();
|
|
256654
|
+
if (!token) return notConnected2(res);
|
|
256655
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
256656
|
+
const buildingId = url.searchParams.get("buildingId") || void 0;
|
|
256657
|
+
const capacityRaw = url.searchParams.get("capacity");
|
|
256658
|
+
const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
|
|
256659
|
+
try {
|
|
256660
|
+
const resources = await fetchResources(token, buildingId, capacity);
|
|
256661
|
+
return respondJson2(res, 200, { ok: true, resources }), true;
|
|
256662
|
+
} catch (err) {
|
|
256663
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256664
|
+
return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${msg}` }), true;
|
|
256665
|
+
}
|
|
256666
|
+
}
|
|
256667
|
+
if (method === "GET" && pathname === "/api/calendar/buildings") {
|
|
256668
|
+
const token = await getToken2();
|
|
256669
|
+
if (!token) return notConnected2(res);
|
|
256670
|
+
const result = await listBuildingsRaw(token);
|
|
256671
|
+
if (!result.ok) return respondJson2(res, 200, { ok: false, error: result.error }), true;
|
|
256672
|
+
return respondJson2(res, 200, { ok: true, buildings: result.data }), true;
|
|
256673
|
+
}
|
|
256674
|
+
if (method === "POST" && pathname === "/api/calendar/buildings") {
|
|
256675
|
+
const token = await getToken2();
|
|
256676
|
+
if (!token) return notConnected2(res);
|
|
256677
|
+
const reqBody = await parseJsonBody2(req);
|
|
256678
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256679
|
+
const buildingName = String(reqBody.buildingName || "").trim();
|
|
256680
|
+
if (!buildingName) return respondJson2(res, 400, { ok: false, error: "buildingName is required" }), true;
|
|
256681
|
+
const buildingId = String(reqBody.buildingId || "").trim() || slugify2(buildingName, "bld");
|
|
256682
|
+
const floorNames = reqBody.floorNames ? Array.isArray(reqBody.floorNames) ? reqBody.floorNames.map(String) : void 0 : void 0;
|
|
256683
|
+
const floors = !floorNames && reqBody.floorNames ? String(reqBody.floorNames).trim() : void 0;
|
|
256684
|
+
const result = await createBuildingRaw(token, {
|
|
256685
|
+
name: buildingName,
|
|
256686
|
+
buildingId,
|
|
256687
|
+
description: reqBody.description ? String(reqBody.description).trim() : void 0,
|
|
256688
|
+
floorNames: floorNames || void 0,
|
|
256689
|
+
floors: floors || void 0
|
|
256690
|
+
});
|
|
256691
|
+
if (!result.ok) {
|
|
256692
|
+
if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256693
|
+
return respondJson2(res, 200, { ok: false, error: result.error }), true;
|
|
256694
|
+
}
|
|
256695
|
+
return respondJson2(res, 200, { ok: true, building: result.data }), true;
|
|
256696
|
+
}
|
|
256697
|
+
if (method === "POST" && pathname === "/api/calendar/resources") {
|
|
256698
|
+
const token = await getToken2();
|
|
256699
|
+
if (!token) return notConnected2(res);
|
|
256700
|
+
const reqBody = await parseJsonBody2(req);
|
|
256701
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256702
|
+
const resourceName = String(reqBody.resourceName || "").trim();
|
|
256703
|
+
if (!resourceName) return respondJson2(res, 400, { ok: false, error: "resourceName is required" }), true;
|
|
256704
|
+
const resourceId = String(reqBody.resourceId || "").trim() || slugify2(resourceName, "res");
|
|
256705
|
+
const result = await createResourceRaw(token, {
|
|
256706
|
+
name: resourceName,
|
|
256707
|
+
resourceId,
|
|
256708
|
+
buildingId: reqBody.buildingId ? String(reqBody.buildingId).trim() : void 0,
|
|
256709
|
+
capacity: reqBody.capacity ? Math.max(0, Number(reqBody.capacity) || 0) : void 0,
|
|
256710
|
+
resourceType: String(reqBody.resourceType || "CONFERENCE_ROOM").trim(),
|
|
256711
|
+
floorName: reqBody.floorName ? String(reqBody.floorName).trim() : void 0,
|
|
256712
|
+
description: reqBody.resourceDescription ? String(reqBody.resourceDescription).trim() : void 0
|
|
256713
|
+
});
|
|
256714
|
+
if (!result.ok) {
|
|
256715
|
+
if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256716
|
+
return respondJson2(res, 200, { ok: false, error: result.error }), true;
|
|
256717
|
+
}
|
|
256718
|
+
return respondJson2(res, 200, { ok: true, resource: result.data }), true;
|
|
256719
|
+
}
|
|
256720
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/buildings/")) {
|
|
256721
|
+
const buildingId = decodeURIComponent(pathname.slice("/api/calendar/buildings/".length));
|
|
256722
|
+
if (!buildingId) return respondJson2(res, 400, { ok: false, error: "buildingId is required" }), true;
|
|
256723
|
+
const token = await getToken2();
|
|
256724
|
+
if (!token) return notConnected2(res);
|
|
256725
|
+
const reqBody = await parseJsonBody2(req);
|
|
256726
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256727
|
+
const body = {};
|
|
256728
|
+
if (reqBody.buildingName) body.buildingName = String(reqBody.buildingName).trim();
|
|
256729
|
+
if (reqBody.description !== void 0) body.description = String(reqBody.description).trim();
|
|
256730
|
+
if (reqBody.floorNames) {
|
|
256731
|
+
if (Array.isArray(reqBody.floorNames)) {
|
|
256732
|
+
body.floorNames = reqBody.floorNames.map(String);
|
|
256733
|
+
} else {
|
|
256734
|
+
const raw = String(reqBody.floorNames).trim();
|
|
256735
|
+
const n = Number(raw);
|
|
256736
|
+
if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
|
|
256737
|
+
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
256738
|
+
} else {
|
|
256739
|
+
body.floorNames = raw.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
256740
|
+
}
|
|
256741
|
+
}
|
|
256742
|
+
}
|
|
256743
|
+
if (Object.keys(body).length === 0) {
|
|
256744
|
+
return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
|
|
256745
|
+
}
|
|
256746
|
+
const result = await updateBuildingRaw(token, buildingId, body);
|
|
256747
|
+
if (!result.ok) {
|
|
256748
|
+
if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256749
|
+
if (result.status === 404) return respondJson2(res, 404, { ok: false, error: "Building not found" }), true;
|
|
256750
|
+
return respondJson2(res, 200, { ok: false, error: result.error }), true;
|
|
256751
|
+
}
|
|
256752
|
+
return respondJson2(res, 200, { ok: true, building: result.data }), true;
|
|
256753
|
+
}
|
|
256754
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/resources/")) {
|
|
256755
|
+
const resourceId = decodeURIComponent(pathname.slice("/api/calendar/resources/".length));
|
|
256756
|
+
if (!resourceId) return respondJson2(res, 400, { ok: false, error: "resourceId is required" }), true;
|
|
256757
|
+
const token = await getToken2();
|
|
256758
|
+
if (!token) return notConnected2(res);
|
|
256759
|
+
const reqBody = await parseJsonBody2(req);
|
|
256760
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256761
|
+
const body = {};
|
|
256762
|
+
if (reqBody.resourceName) body.resourceName = String(reqBody.resourceName).trim();
|
|
256763
|
+
if (reqBody.buildingId !== void 0) body.buildingId = String(reqBody.buildingId).trim();
|
|
256764
|
+
if (reqBody.capacity !== void 0) body.capacity = Math.max(0, Number(reqBody.capacity) || 0);
|
|
256765
|
+
if (reqBody.resourceType) body.resourceType = String(reqBody.resourceType).trim();
|
|
256766
|
+
if (reqBody.floorName !== void 0) body.floorName = String(reqBody.floorName).trim();
|
|
256767
|
+
if (reqBody.resourceDescription !== void 0) body.resourceDescription = String(reqBody.resourceDescription).trim();
|
|
256768
|
+
if (Object.keys(body).length === 0) {
|
|
256769
|
+
return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
|
|
256770
|
+
}
|
|
256771
|
+
const result = await updateResourceRaw(token, resourceId, body);
|
|
256772
|
+
if (!result.ok) {
|
|
256773
|
+
if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
|
|
256774
|
+
if (result.status === 404) return respondJson2(res, 404, { ok: false, error: "Resource not found" }), true;
|
|
256775
|
+
return respondJson2(res, 200, { ok: false, error: result.error }), true;
|
|
256776
|
+
}
|
|
256777
|
+
return respondJson2(res, 200, { ok: true, resource: result.data }), true;
|
|
256778
|
+
}
|
|
256779
|
+
if (method === "POST" && pathname === "/api/calendar/slots") {
|
|
256780
|
+
const token = await getToken2();
|
|
256781
|
+
if (!token) return notConnected2(res);
|
|
256782
|
+
const reqBody = await parseJsonBody2(req);
|
|
256783
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256784
|
+
const resourceId = String(reqBody.resourceId || "").trim();
|
|
256785
|
+
const buildingId = String(reqBody.buildingId || "").trim();
|
|
256786
|
+
const capacity = Number(reqBody.capacity || 0) || 0;
|
|
256787
|
+
const dateSingle = String(reqBody.date || "").trim();
|
|
256788
|
+
const dateFrom = dateSingle || String(reqBody.dateFrom || "").trim();
|
|
256789
|
+
const dateTo = dateSingle || String(reqBody.dateTo || "").trim();
|
|
256790
|
+
const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
|
|
256791
|
+
const workStartHour = Number(reqBody.workStartHour ?? DEFAULT_WORK_START_HOUR);
|
|
256792
|
+
const workEndHour = Number(reqBody.workEndHour ?? DEFAULT_WORK_END_HOUR);
|
|
256793
|
+
const offset = Math.max(0, Number(reqBody.offset || 0) || 0);
|
|
256794
|
+
const bufferMinutes = Math.max(0, Math.min(60, Number(reqBody.bufferMinutes ?? DEFAULT_BUFFER_MINUTES) || 0));
|
|
256795
|
+
const required = Array.isArray(reqBody.required) ? reqBody.required.map(String).filter(Boolean) : [];
|
|
256796
|
+
const optional = Array.isArray(reqBody.optional) ? reqBody.optional.map(String).filter(Boolean) : [];
|
|
256797
|
+
if (!dateFrom || !dateTo) {
|
|
256798
|
+
return respondJson2(res, 400, { ok: false, error: "date or dateFrom/dateTo is required (YYYY-MM-DD)" }), true;
|
|
256799
|
+
}
|
|
256800
|
+
let resourceEmails = [];
|
|
256801
|
+
let resolvedResources = [];
|
|
256802
|
+
if (resourceId) {
|
|
256803
|
+
resourceEmails = [resourceId];
|
|
256804
|
+
} else if (buildingId || capacity > 0) {
|
|
256805
|
+
resolvedResources = await fetchResources(
|
|
256806
|
+
token,
|
|
256807
|
+
buildingId || void 0,
|
|
256808
|
+
capacity > 0 ? capacity : void 0
|
|
256809
|
+
);
|
|
256810
|
+
resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
|
|
256811
|
+
} else if (required.length === 0 && optional.length === 0) {
|
|
256812
|
+
resolvedResources = await fetchResources(token);
|
|
256813
|
+
resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
|
|
256814
|
+
}
|
|
256815
|
+
const allTargets = [...resourceEmails, ...required, ...optional];
|
|
256816
|
+
if (allTargets.length === 0) {
|
|
256817
|
+
return respondJson2(res, 400, { ok: false, error: "At least one target is required (resourceId, buildingId, required, or optional)" }), true;
|
|
256818
|
+
}
|
|
256819
|
+
const tz = String(reqBody.timeZone || "").trim() || DEFAULT_TIMEZONE;
|
|
256820
|
+
const timeMin = makeDateInTz(dateFrom, 0, 0, tz).toISOString();
|
|
256821
|
+
const timeMax = makeDateInTz(dateTo, 23, 59, tz).toISOString();
|
|
256822
|
+
const fbBody = {
|
|
256823
|
+
timeMin,
|
|
256824
|
+
timeMax,
|
|
256825
|
+
timeZone: tz,
|
|
256826
|
+
items: allTargets.map((e2) => ({ id: e2 }))
|
|
256827
|
+
};
|
|
256828
|
+
const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256829
|
+
method: "POST",
|
|
256830
|
+
headers: { "Content-Type": "application/json" },
|
|
256831
|
+
body: JSON.stringify(fbBody)
|
|
256832
|
+
});
|
|
256833
|
+
if (!fbRes.ok) {
|
|
256834
|
+
const err = await fbRes.text().catch(() => "");
|
|
256835
|
+
return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
|
|
256836
|
+
}
|
|
256837
|
+
const fbData = await fbRes.json();
|
|
256838
|
+
const calendars = fbData.calendars || {};
|
|
256839
|
+
const busyMap = {};
|
|
256840
|
+
for (const email of allTargets) {
|
|
256841
|
+
const cal = calendars[email];
|
|
256842
|
+
busyMap[email] = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ s: new Date(b.start).getTime(), e: new Date(b.end).getTime() })) : [];
|
|
256843
|
+
}
|
|
256844
|
+
const durationMs = durationMinutes * 60 * 1e3;
|
|
256845
|
+
const { slots: top, hasMore } = findSlots({
|
|
256846
|
+
busyMap,
|
|
256847
|
+
required,
|
|
256848
|
+
optional,
|
|
256849
|
+
resourceEmails,
|
|
256850
|
+
durationMs,
|
|
256851
|
+
dateFrom,
|
|
256852
|
+
dateTo,
|
|
256853
|
+
workStartHour,
|
|
256854
|
+
workEndHour,
|
|
256855
|
+
offset,
|
|
256856
|
+
bufferMinutes,
|
|
256857
|
+
timeZone: tz
|
|
256858
|
+
});
|
|
256859
|
+
const slots = top.map((c, i2) => ({
|
|
256860
|
+
rank: i2 + 1,
|
|
256861
|
+
start: new Date(c.start).toISOString(),
|
|
256862
|
+
end: new Date(c.end).toISOString(),
|
|
256863
|
+
durationMinutes,
|
|
256864
|
+
allAvailable: c.allAvailable,
|
|
256865
|
+
availableCount: c.availableCount,
|
|
256866
|
+
totalParticipants: c.totalParticipants,
|
|
256867
|
+
missingOptional: c.missingOptional,
|
|
256868
|
+
bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
|
|
256869
|
+
bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
|
|
256870
|
+
resourceEmails: c.resourceEmails
|
|
256871
|
+
}));
|
|
256872
|
+
const resourceInfo = resolvedResources.length > 0 ? resolvedResources.map((r2) => ({
|
|
256873
|
+
resourceEmail: r2.resourceEmail,
|
|
256874
|
+
resourceName: r2.resourceName,
|
|
256875
|
+
capacity: r2.capacity
|
|
256876
|
+
})) : void 0;
|
|
256877
|
+
return respondJson2(res, 200, {
|
|
256878
|
+
ok: true,
|
|
256879
|
+
slots,
|
|
256880
|
+
hasMore,
|
|
256881
|
+
...resourceInfo ? { resources: resourceInfo } : {},
|
|
256882
|
+
durationMinutes
|
|
256883
|
+
}), true;
|
|
256884
|
+
}
|
|
256885
|
+
if (method === "POST" && pathname === "/api/calendar/check") {
|
|
256886
|
+
const token = await getToken2();
|
|
256887
|
+
if (!token) return notConnected2(res);
|
|
256888
|
+
const reqBody = await parseJsonBody2(req);
|
|
256889
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256890
|
+
const targets = Array.isArray(reqBody.targets) ? reqBody.targets.map(String).filter(Boolean) : [];
|
|
256891
|
+
const datetime = String(reqBody.datetime || "").trim();
|
|
256892
|
+
const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
|
|
256893
|
+
if (!targets.length) return respondJson2(res, 400, { ok: false, error: "targets array is required" }), true;
|
|
256894
|
+
if (!datetime) return respondJson2(res, 400, { ok: false, error: "datetime is required (ISO 8601)" }), true;
|
|
256895
|
+
const startDate = new Date(datetime);
|
|
256896
|
+
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1e3);
|
|
256897
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
256898
|
+
const fbBody = {
|
|
256899
|
+
timeMin: startDate.toISOString(),
|
|
256900
|
+
timeMax: endDate.toISOString(),
|
|
256901
|
+
timeZone: tz,
|
|
256902
|
+
items: targets.map((e2) => ({ id: e2 }))
|
|
256903
|
+
};
|
|
256904
|
+
const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256905
|
+
method: "POST",
|
|
256906
|
+
headers: { "Content-Type": "application/json" },
|
|
256907
|
+
body: JSON.stringify(fbBody)
|
|
256908
|
+
});
|
|
256909
|
+
if (!fbRes.ok) {
|
|
256910
|
+
const err = await fbRes.text().catch(() => "");
|
|
256911
|
+
return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
|
|
256912
|
+
}
|
|
256913
|
+
const fbData = await fbRes.json();
|
|
256914
|
+
const calendars = fbData.calendars || {};
|
|
256915
|
+
let allAvailable = true;
|
|
256916
|
+
const statuses = targets.map((id) => {
|
|
256917
|
+
const cal = calendars[id];
|
|
256918
|
+
const conflicts = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ start: b.start, end: b.end })) : [];
|
|
256919
|
+
const available = conflicts.length === 0;
|
|
256920
|
+
if (!available) allAvailable = false;
|
|
256921
|
+
return { id, available, conflicts };
|
|
256922
|
+
});
|
|
256923
|
+
return respondJson2(res, 200, {
|
|
256924
|
+
ok: true,
|
|
256925
|
+
allAvailable,
|
|
256926
|
+
datetime: startDate.toISOString(),
|
|
256927
|
+
durationMinutes,
|
|
256928
|
+
statuses
|
|
256929
|
+
}), true;
|
|
256930
|
+
}
|
|
256931
|
+
if (method === "POST" && pathname === "/api/calendar/reserve") {
|
|
256932
|
+
const token = await getToken2();
|
|
256933
|
+
if (!token) return notConnected2(res);
|
|
256934
|
+
const db = await getFirestore2();
|
|
256935
|
+
if (!db) return firestoreUnavailable(res);
|
|
256936
|
+
const reqBody = await parseJsonBody2(req);
|
|
256937
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
256938
|
+
const resourceEmail = String(reqBody.resourceEmail || "").trim();
|
|
256939
|
+
const start = String(reqBody.start || "").trim();
|
|
256940
|
+
const end = String(reqBody.end || "").trim();
|
|
256941
|
+
const title = String(reqBody.title || "").trim();
|
|
256942
|
+
const description = String(reqBody.description || "").trim();
|
|
256943
|
+
const bookerName = String(reqBody.bookerName || "").trim();
|
|
256944
|
+
const bookerPhone = String(reqBody.bookerPhone || "").trim();
|
|
256945
|
+
const bookerEmail = String(reqBody.bookerEmail || "").trim();
|
|
256946
|
+
const callSid = String(reqBody.callSid || "").trim();
|
|
256947
|
+
const bookedVia = String(reqBody.bookedVia || "phone").trim();
|
|
256948
|
+
const isAllDay = reqBody.isAllDay === true;
|
|
256949
|
+
const recurrenceTypeRaw = String(reqBody.recurrenceType || "").trim();
|
|
256950
|
+
const recurrenceType = VALID_RECURRENCE_TYPES.has(recurrenceTypeRaw) ? recurrenceTypeRaw : null;
|
|
256951
|
+
const recurrenceCount = recurrenceType && reqBody.recurrenceCount ? Math.max(1, Math.min(365, Number(reqBody.recurrenceCount) || 0)) : void 0;
|
|
256952
|
+
const recurrenceUntil = recurrenceType && typeof reqBody.recurrenceUntil === "string" ? reqBody.recurrenceUntil.trim() : void 0;
|
|
256953
|
+
if (!start) return respondJson2(res, 400, { ok: false, error: "start is required (ISO 8601)" }), true;
|
|
256954
|
+
if (!end) return respondJson2(res, 400, { ok: false, error: "end is required (ISO 8601)" }), true;
|
|
256955
|
+
if (!title) return respondJson2(res, 400, { ok: false, error: "title is required" }), true;
|
|
256956
|
+
const rrule = recurrenceType ? buildRrule(recurrenceType, recurrenceCount, recurrenceUntil) : null;
|
|
256957
|
+
const calendarId = resourceEmail || "primary";
|
|
256958
|
+
if (resourceEmail) {
|
|
256959
|
+
try {
|
|
256960
|
+
const fbBody = {
|
|
256961
|
+
timeMin: start,
|
|
256962
|
+
timeMax: end,
|
|
256963
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
256964
|
+
items: [{ id: calendarId }]
|
|
256965
|
+
};
|
|
256966
|
+
const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
256967
|
+
method: "POST",
|
|
256968
|
+
headers: { "Content-Type": "application/json" },
|
|
256969
|
+
body: JSON.stringify(fbBody)
|
|
256970
|
+
});
|
|
256971
|
+
if (fbRes.ok) {
|
|
256972
|
+
const fbData = await fbRes.json();
|
|
256973
|
+
const fbCalendars = fbData.calendars || {};
|
|
256974
|
+
const busyPeriods = fbCalendars[calendarId]?.busy || [];
|
|
256975
|
+
if (busyPeriods.length > 0) {
|
|
256976
|
+
return respondJson2(res, 409, {
|
|
256977
|
+
ok: false,
|
|
256978
|
+
error: "slot_conflict",
|
|
256979
|
+
message: "The requested time slot is already booked. Please choose a different time.",
|
|
256980
|
+
conflictingPeriods: busyPeriods
|
|
256981
|
+
}), true;
|
|
256982
|
+
}
|
|
256983
|
+
} else {
|
|
256984
|
+
const fbErr = await fbRes.text().catch(() => "");
|
|
256985
|
+
console.warn(
|
|
256986
|
+
`FreeBusy pre-check failed (${fbRes.status}), proceeding without check: ${fbErr}`
|
|
256987
|
+
);
|
|
256988
|
+
}
|
|
256989
|
+
} catch (fbError) {
|
|
256990
|
+
console.warn(
|
|
256991
|
+
"FreeBusy pre-check threw an error, proceeding without check:",
|
|
256992
|
+
fbError instanceof Error ? fbError.message : String(fbError)
|
|
256993
|
+
);
|
|
256994
|
+
}
|
|
256995
|
+
}
|
|
256996
|
+
const eventBody = {
|
|
256997
|
+
summary: title,
|
|
256998
|
+
start: isAllDay ? { date: start } : { dateTime: start },
|
|
256999
|
+
end: isAllDay ? { date: end } : { dateTime: end }
|
|
257000
|
+
};
|
|
257001
|
+
if (description) eventBody.description = description;
|
|
257002
|
+
if (resourceEmail) {
|
|
257003
|
+
eventBody.attendees = [{ email: resourceEmail, resource: true }];
|
|
257004
|
+
}
|
|
257005
|
+
if (rrule) {
|
|
257006
|
+
eventBody.recurrence = [rrule];
|
|
257007
|
+
}
|
|
257008
|
+
const createRes = await adminFetch(
|
|
257009
|
+
token,
|
|
257010
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events`,
|
|
257011
|
+
{
|
|
257012
|
+
method: "POST",
|
|
257013
|
+
headers: { "Content-Type": "application/json" },
|
|
257014
|
+
body: JSON.stringify(eventBody)
|
|
257015
|
+
}
|
|
257016
|
+
);
|
|
257017
|
+
if (!createRes.ok) {
|
|
257018
|
+
const err = await createRes.text().catch(() => "");
|
|
257019
|
+
return respondJson2(res, 200, { ok: false, error: `Calendar create failed: ${createRes.status} ${err}` }), true;
|
|
257020
|
+
}
|
|
257021
|
+
const calEvent = await createRes.json();
|
|
257022
|
+
const calendarEventId = String(calEvent.id || "");
|
|
257023
|
+
let resourceName = "";
|
|
257024
|
+
if (resourceEmail) {
|
|
257025
|
+
try {
|
|
257026
|
+
const resources = await fetchResources(token);
|
|
257027
|
+
const match = resources.find((r2) => r2.resourceEmail === resourceEmail);
|
|
257028
|
+
if (match) resourceName = match.resourceName;
|
|
257029
|
+
} catch {
|
|
257030
|
+
}
|
|
257031
|
+
}
|
|
257032
|
+
const reservationId = generateReservationId();
|
|
257033
|
+
const nowIso40 = (/* @__PURE__ */ new Date()).toISOString();
|
|
257034
|
+
const reservation = {
|
|
257035
|
+
reservationId,
|
|
257036
|
+
calendarEventId,
|
|
257037
|
+
resourceCalendarId: resourceEmail,
|
|
257038
|
+
resourceName,
|
|
257039
|
+
startTime: start,
|
|
257040
|
+
endTime: end,
|
|
257041
|
+
title,
|
|
257042
|
+
isAllDay,
|
|
257043
|
+
status: "confirmed",
|
|
257044
|
+
bookerName,
|
|
257045
|
+
bookerPhone,
|
|
257046
|
+
bookerEmail,
|
|
257047
|
+
callSid,
|
|
257048
|
+
bookedVia,
|
|
257049
|
+
createdAt: nowIso40,
|
|
257050
|
+
updatedAt: nowIso40
|
|
257051
|
+
};
|
|
257052
|
+
if (rrule && recurrenceType) {
|
|
257053
|
+
reservation.isRecurring = true;
|
|
257054
|
+
reservation.recurrenceRule = rrule;
|
|
257055
|
+
reservation.recurrenceType = recurrenceType;
|
|
257056
|
+
reservation.recurrenceCount = recurrenceCount || null;
|
|
257057
|
+
reservation.recurrenceUntil = recurrenceUntil || null;
|
|
257058
|
+
}
|
|
257059
|
+
try {
|
|
257060
|
+
if (resourceEmail) {
|
|
257061
|
+
await db.runTransaction(async (transaction) => {
|
|
257062
|
+
const conflictQuery = db.collection("reservations").where("resourceCalendarId", "==", resourceEmail).where("status", "==", "confirmed").where("startTime", "<", end);
|
|
257063
|
+
const conflictSnap = await transaction.get(conflictQuery);
|
|
257064
|
+
const hasConflict = conflictSnap.docs.some((doc) => {
|
|
257065
|
+
const data = doc.data();
|
|
257066
|
+
return data.endTime > start;
|
|
257067
|
+
});
|
|
257068
|
+
if (hasConflict) {
|
|
257069
|
+
throw new Error("RESERVATION_CONFLICT");
|
|
257070
|
+
}
|
|
257071
|
+
transaction.set(
|
|
257072
|
+
db.collection("reservations").doc(reservationId),
|
|
257073
|
+
reservation
|
|
257074
|
+
);
|
|
257075
|
+
});
|
|
257076
|
+
} else {
|
|
257077
|
+
await db.collection("reservations").doc(reservationId).set(reservation);
|
|
257078
|
+
}
|
|
257079
|
+
} catch (err) {
|
|
257080
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257081
|
+
if (calendarEventId) {
|
|
257082
|
+
try {
|
|
257083
|
+
await adminFetch(
|
|
257084
|
+
token,
|
|
257085
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257086
|
+
{ method: "DELETE" }
|
|
257087
|
+
);
|
|
257088
|
+
} catch (rollbackErr) {
|
|
257089
|
+
console.warn(
|
|
257090
|
+
"Failed to rollback Calendar event after Firestore error:",
|
|
257091
|
+
rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
|
|
257092
|
+
);
|
|
257093
|
+
}
|
|
257094
|
+
}
|
|
257095
|
+
if (msg === "RESERVATION_CONFLICT") {
|
|
257096
|
+
return respondJson2(res, 409, {
|
|
257097
|
+
ok: false,
|
|
257098
|
+
error: "slot_conflict",
|
|
257099
|
+
message: "The requested time slot is already booked. Please choose a different time."
|
|
257100
|
+
}), true;
|
|
257101
|
+
}
|
|
257102
|
+
return respondJson2(res, 200, {
|
|
257103
|
+
ok: false,
|
|
257104
|
+
error: `Firestore write failed: ${msg}`
|
|
257105
|
+
}), true;
|
|
257106
|
+
}
|
|
257107
|
+
return respondJson2(res, 200, {
|
|
257108
|
+
ok: true,
|
|
257109
|
+
reservationId,
|
|
257110
|
+
calendarEventId: calEvent.id,
|
|
257111
|
+
resourceName,
|
|
257112
|
+
start,
|
|
257113
|
+
end,
|
|
257114
|
+
title,
|
|
257115
|
+
status: "confirmed",
|
|
257116
|
+
...rrule ? {
|
|
257117
|
+
isRecurring: true,
|
|
257118
|
+
recurrenceType,
|
|
257119
|
+
recurrenceRule: rrule
|
|
257120
|
+
} : {}
|
|
257121
|
+
}), true;
|
|
257122
|
+
}
|
|
257123
|
+
if (method === "GET" && /^\/api\/calendar\/reserve\/[^/]+\/instances$/.test(pathname)) {
|
|
257124
|
+
const parts = pathname.split("/");
|
|
257125
|
+
const reservationId = decodeURIComponent(parts[4]);
|
|
257126
|
+
const db = await getFirestore2();
|
|
257127
|
+
if (!db) return firestoreUnavailable(res);
|
|
257128
|
+
const docSnap = await db.collection("reservations").doc(reservationId).get();
|
|
257129
|
+
if (!docSnap.exists) {
|
|
257130
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257131
|
+
}
|
|
257132
|
+
const data = docSnap.data();
|
|
257133
|
+
if (!data.isRecurring || !data.calendarEventId) {
|
|
257134
|
+
return respondJson2(res, 400, { ok: false, error: "Not a recurring reservation" }), true;
|
|
257135
|
+
}
|
|
257136
|
+
const token = await getToken2();
|
|
257137
|
+
if (!token) return notConnected2(res);
|
|
257138
|
+
const calendarId = String(data.resourceCalendarId || "") || "primary";
|
|
257139
|
+
const eventId = String(data.calendarEventId);
|
|
257140
|
+
const instancesRes = await adminFetch(
|
|
257141
|
+
token,
|
|
257142
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}/instances?maxResults=50`
|
|
257143
|
+
);
|
|
257144
|
+
if (!instancesRes.ok) {
|
|
257145
|
+
const errText = await instancesRes.text().catch(() => "");
|
|
257146
|
+
return respondJson2(res, 502, {
|
|
257147
|
+
ok: false,
|
|
257148
|
+
error: `Failed to fetch instances: ${instancesRes.status}`,
|
|
257149
|
+
detail: errText
|
|
257150
|
+
}), true;
|
|
257151
|
+
}
|
|
257152
|
+
const instancesData = await instancesRes.json();
|
|
257153
|
+
const items = Array.isArray(instancesData.items) ? instancesData.items : [];
|
|
257154
|
+
const instances = items.map((item) => {
|
|
257155
|
+
const startObj = item.start;
|
|
257156
|
+
const endObj = item.end;
|
|
257157
|
+
return {
|
|
257158
|
+
eventId: String(item.id || ""),
|
|
257159
|
+
start: startObj?.dateTime || startObj?.date || "",
|
|
257160
|
+
end: endObj?.dateTime || endObj?.date || "",
|
|
257161
|
+
status: String(item.status || "confirmed")
|
|
257162
|
+
};
|
|
257163
|
+
});
|
|
257164
|
+
return respondJson2(res, 200, { ok: true, reservationId, instances }), true;
|
|
257165
|
+
}
|
|
257166
|
+
if (method === "PATCH" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257167
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257168
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257169
|
+
const reqBody = await parseJsonBody2(req);
|
|
257170
|
+
if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
|
|
257171
|
+
const start = String(reqBody.start || "").trim();
|
|
257172
|
+
const end = String(reqBody.end || "").trim();
|
|
257173
|
+
const resourceEmail = reqBody.resourceEmail ? String(reqBody.resourceEmail).trim() : "";
|
|
257174
|
+
const isAllDay = reqBody.isAllDay !== void 0 ? reqBody.isAllDay === true : void 0;
|
|
257175
|
+
if (!start || !end) {
|
|
257176
|
+
return respondJson2(res, 400, { ok: false, error: "start and end are required" }), true;
|
|
257177
|
+
}
|
|
257178
|
+
const db = await getFirestore2();
|
|
257179
|
+
if (!db) return firestoreUnavailable(res);
|
|
257180
|
+
const docRef = db.collection("reservations").doc(reservationId);
|
|
257181
|
+
let docSnap = await docRef.get();
|
|
257182
|
+
if (!docSnap.exists && reservationId.startsWith("gcal-")) {
|
|
257183
|
+
const gcalEventId = reservationId.slice(5);
|
|
257184
|
+
const calendarIdForUpsert = resourceEmail || String(reqBody.calendarId || "").trim();
|
|
257185
|
+
if (!calendarIdForUpsert) {
|
|
257186
|
+
return respondJson2(res, 400, { ok: false, error: "calendarId is required for direct calendar bookings" }), true;
|
|
257187
|
+
}
|
|
257188
|
+
const upsertToken = await getToken2();
|
|
257189
|
+
if (!upsertToken) return notConnected2(res);
|
|
257190
|
+
const eventRes = await adminFetch(
|
|
257191
|
+
upsertToken,
|
|
257192
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarIdForUpsert)}/events/${encodeURIComponent(gcalEventId)}`
|
|
257193
|
+
);
|
|
257194
|
+
if (!eventRes.ok) {
|
|
257195
|
+
return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
|
|
257196
|
+
}
|
|
257197
|
+
const gcalEvent = await eventRes.json();
|
|
257198
|
+
const gcalStart = gcalEvent.start;
|
|
257199
|
+
const gcalEnd = gcalEvent.end;
|
|
257200
|
+
const creator = gcalEvent.creator;
|
|
257201
|
+
const isAllDay2 = Boolean(gcalStart?.date && !gcalStart?.dateTime);
|
|
257202
|
+
const upsertDoc = {
|
|
257203
|
+
reservationId,
|
|
257204
|
+
calendarEventId: gcalEventId,
|
|
257205
|
+
resourceCalendarId: calendarIdForUpsert,
|
|
257206
|
+
resourceName: String(gcalEvent.location || ""),
|
|
257207
|
+
startTime: gcalStart?.dateTime || gcalStart?.date || "",
|
|
257208
|
+
endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
|
|
257209
|
+
title: String(gcalEvent.summary || "Direct Calendar Booking"),
|
|
257210
|
+
isAllDay: isAllDay2,
|
|
257211
|
+
status: "confirmed",
|
|
257212
|
+
bookerName: creator?.displayName || creator?.email || "",
|
|
257213
|
+
bookerPhone: "",
|
|
257214
|
+
bookerEmail: creator?.email || "",
|
|
257215
|
+
bookedVia: "direct_calendar",
|
|
257216
|
+
callSid: "",
|
|
257217
|
+
createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
|
|
257218
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257219
|
+
};
|
|
257220
|
+
await docRef.set(upsertDoc);
|
|
257221
|
+
docSnap = await docRef.get();
|
|
257222
|
+
}
|
|
257223
|
+
if (!docSnap.exists) {
|
|
257224
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257225
|
+
}
|
|
257226
|
+
const data = docSnap.data();
|
|
257227
|
+
if (data.status !== "confirmed") {
|
|
257228
|
+
return respondJson2(res, 400, { ok: false, error: "Only confirmed reservations can be modified" }), true;
|
|
257229
|
+
}
|
|
257230
|
+
const token = await getToken2();
|
|
257231
|
+
if (!token) return notConnected2(res);
|
|
257232
|
+
const effectiveResource = resourceEmail || String(data.resourceCalendarId || "");
|
|
257233
|
+
const calendarId = effectiveResource || "primary";
|
|
257234
|
+
const calendarEventId = String(data.calendarEventId || "");
|
|
257235
|
+
if (effectiveResource) {
|
|
257236
|
+
try {
|
|
257237
|
+
const fbBody = {
|
|
257238
|
+
timeMin: start,
|
|
257239
|
+
timeMax: end,
|
|
257240
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
257241
|
+
items: [{ id: calendarId }]
|
|
257242
|
+
};
|
|
257243
|
+
const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
|
|
257244
|
+
method: "POST",
|
|
257245
|
+
headers: { "Content-Type": "application/json" },
|
|
257246
|
+
body: JSON.stringify(fbBody)
|
|
257247
|
+
});
|
|
257248
|
+
if (fbRes.ok) {
|
|
257249
|
+
const fbData = await fbRes.json();
|
|
257250
|
+
const fbCalendars = fbData.calendars || {};
|
|
257251
|
+
const busyPeriods = fbCalendars[calendarId]?.busy || [];
|
|
257252
|
+
const currentStart = String(data.startTime || "");
|
|
257253
|
+
const currentEnd = String(data.endTime || "");
|
|
257254
|
+
const externalConflicts = busyPeriods.filter((b) => {
|
|
257255
|
+
const bStart = new Date(b.start).getTime();
|
|
257256
|
+
const bEnd = new Date(b.end).getTime();
|
|
257257
|
+
const curStart = currentStart ? new Date(currentStart).getTime() : 0;
|
|
257258
|
+
const curEnd = currentEnd ? new Date(currentEnd).getTime() : 0;
|
|
257259
|
+
return !(bStart === curStart && bEnd === curEnd);
|
|
257260
|
+
});
|
|
257261
|
+
if (externalConflicts.length > 0) {
|
|
257262
|
+
return respondJson2(res, 409, {
|
|
257263
|
+
ok: false,
|
|
257264
|
+
error: "slot_conflict",
|
|
257265
|
+
message: "The requested time slot is already booked by another reservation. Please choose a different time.",
|
|
257266
|
+
conflictingPeriods: externalConflicts
|
|
257267
|
+
}), true;
|
|
257268
|
+
}
|
|
257269
|
+
} else {
|
|
257270
|
+
const fbErr = await fbRes.text().catch(() => "");
|
|
257271
|
+
console.warn(
|
|
257272
|
+
`FreeBusy pre-check for modify failed (${fbRes.status}), proceeding: ${fbErr}`
|
|
257273
|
+
);
|
|
257274
|
+
}
|
|
257275
|
+
} catch (fbError) {
|
|
257276
|
+
console.warn(
|
|
257277
|
+
"FreeBusy pre-check for modify threw an error, proceeding:",
|
|
257278
|
+
fbError instanceof Error ? fbError.message : String(fbError)
|
|
257279
|
+
);
|
|
257280
|
+
}
|
|
257281
|
+
}
|
|
257282
|
+
if (calendarEventId) {
|
|
257283
|
+
const effectiveIsAllDay = isAllDay !== void 0 ? isAllDay : Boolean(data.isAllDay);
|
|
257284
|
+
const patchBody = {};
|
|
257285
|
+
if (start) patchBody.start = effectiveIsAllDay ? { date: start, dateTime: null } : { dateTime: start, date: null };
|
|
257286
|
+
if (end) patchBody.end = effectiveIsAllDay ? { date: end, dateTime: null } : { dateTime: end, date: null };
|
|
257287
|
+
if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
|
|
257288
|
+
patchBody.attendees = [{ email: resourceEmail, resource: true }];
|
|
257289
|
+
}
|
|
257290
|
+
const patchRes = await adminFetch(
|
|
257291
|
+
token,
|
|
257292
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257293
|
+
{ method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patchBody) }
|
|
257294
|
+
);
|
|
257295
|
+
if (!patchRes.ok) {
|
|
257296
|
+
const errText = await patchRes.text().catch(() => "");
|
|
257297
|
+
return respondJson2(res, 502, { ok: false, error: `Calendar update failed: ${patchRes.status}`, detail: errText }), true;
|
|
257298
|
+
}
|
|
257299
|
+
}
|
|
257300
|
+
const updateFields = {
|
|
257301
|
+
startTime: start,
|
|
257302
|
+
endTime: end,
|
|
257303
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257304
|
+
};
|
|
257305
|
+
if (isAllDay !== void 0) {
|
|
257306
|
+
updateFields.isAllDay = isAllDay;
|
|
257307
|
+
}
|
|
257308
|
+
if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
|
|
257309
|
+
updateFields.resourceCalendarId = resourceEmail;
|
|
257310
|
+
try {
|
|
257311
|
+
const resources = await fetchResources(token);
|
|
257312
|
+
const matched = resources.find((r2) => r2.resourceEmail === resourceEmail);
|
|
257313
|
+
if (matched) updateFields.resourceName = matched.resourceName || resourceEmail;
|
|
257314
|
+
} catch {
|
|
257315
|
+
}
|
|
257316
|
+
}
|
|
257317
|
+
await docRef.update(updateFields);
|
|
257318
|
+
return respondJson2(res, 200, {
|
|
257319
|
+
ok: true,
|
|
257320
|
+
reservationId,
|
|
257321
|
+
start,
|
|
257322
|
+
end,
|
|
257323
|
+
resourceName: String(updateFields.resourceName || data.resourceName || ""),
|
|
257324
|
+
status: "confirmed"
|
|
257325
|
+
}), true;
|
|
257326
|
+
}
|
|
257327
|
+
if (method === "DELETE" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257328
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257329
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257330
|
+
const db = await getFirestore2();
|
|
257331
|
+
if (!db) return firestoreUnavailable(res);
|
|
257332
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
257333
|
+
const docRef = db.collection("reservations").doc(reservationId);
|
|
257334
|
+
let docSnap = await docRef.get();
|
|
257335
|
+
if (!docSnap.exists && reservationId.startsWith("gcal-")) {
|
|
257336
|
+
const gcalEventId = reservationId.slice(5);
|
|
257337
|
+
const gcalCalendarId = url.searchParams.get("calendarId") || "";
|
|
257338
|
+
if (!gcalCalendarId) {
|
|
257339
|
+
return respondJson2(res, 400, { ok: false, error: "calendarId query param is required for direct calendar bookings" }), true;
|
|
257340
|
+
}
|
|
257341
|
+
const upsertToken = await getToken2();
|
|
257342
|
+
if (!upsertToken) return notConnected2(res);
|
|
257343
|
+
const eventRes = await adminFetch(
|
|
257344
|
+
upsertToken,
|
|
257345
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(gcalCalendarId)}/events/${encodeURIComponent(gcalEventId)}`
|
|
257346
|
+
);
|
|
257347
|
+
if (!eventRes.ok) {
|
|
257348
|
+
return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
|
|
257349
|
+
}
|
|
257350
|
+
const gcalEvent = await eventRes.json();
|
|
257351
|
+
const gcalStart = gcalEvent.start;
|
|
257352
|
+
const gcalEnd = gcalEvent.end;
|
|
257353
|
+
const creator = gcalEvent.creator;
|
|
257354
|
+
const isAllDay = Boolean(gcalStart?.date && !gcalStart?.dateTime);
|
|
257355
|
+
const upsertDoc = {
|
|
257356
|
+
reservationId,
|
|
257357
|
+
calendarEventId: gcalEventId,
|
|
257358
|
+
resourceCalendarId: gcalCalendarId,
|
|
257359
|
+
resourceName: String(gcalEvent.location || ""),
|
|
257360
|
+
startTime: gcalStart?.dateTime || gcalStart?.date || "",
|
|
257361
|
+
endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
|
|
257362
|
+
title: String(gcalEvent.summary || "Direct Calendar Booking"),
|
|
257363
|
+
isAllDay,
|
|
257364
|
+
status: "confirmed",
|
|
257365
|
+
bookerName: creator?.displayName || creator?.email || "",
|
|
257366
|
+
bookerPhone: "",
|
|
257367
|
+
bookerEmail: creator?.email || "",
|
|
257368
|
+
bookedVia: "direct_calendar",
|
|
257369
|
+
callSid: "",
|
|
257370
|
+
createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
|
|
257371
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257372
|
+
};
|
|
257373
|
+
await docRef.set(upsertDoc);
|
|
257374
|
+
docSnap = await docRef.get();
|
|
257375
|
+
}
|
|
257376
|
+
if (!docSnap.exists) {
|
|
257377
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257378
|
+
}
|
|
257379
|
+
const data = docSnap.data();
|
|
257380
|
+
const scope = url.searchParams.get("scope") || "all";
|
|
257381
|
+
const instanceEventId = url.searchParams.get("instanceEventId") || "";
|
|
257382
|
+
const calendarEventId = String(data.calendarEventId || "");
|
|
257383
|
+
const calendarId = String(data.resourceCalendarId || "") || "primary";
|
|
257384
|
+
const isRecurring = Boolean(data.isRecurring);
|
|
257385
|
+
const token = await getToken2();
|
|
257386
|
+
if (isRecurring && scope === "single") {
|
|
257387
|
+
if (!instanceEventId) {
|
|
257388
|
+
return respondJson2(res, 400, {
|
|
257389
|
+
ok: false,
|
|
257390
|
+
error: "instanceEventId is required when scope=single"
|
|
257391
|
+
}), true;
|
|
257392
|
+
}
|
|
257393
|
+
if (token) {
|
|
257394
|
+
try {
|
|
257395
|
+
await adminFetch(
|
|
257396
|
+
token,
|
|
257397
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`,
|
|
257398
|
+
{ method: "DELETE" }
|
|
257399
|
+
);
|
|
257400
|
+
} catch {
|
|
257401
|
+
}
|
|
257402
|
+
}
|
|
257403
|
+
return respondJson2(res, 200, {
|
|
257404
|
+
ok: true,
|
|
257405
|
+
reservationId,
|
|
257406
|
+
cancelledInstanceEventId: instanceEventId,
|
|
257407
|
+
scope: "single",
|
|
257408
|
+
status: "instance_cancelled"
|
|
257409
|
+
}), true;
|
|
257410
|
+
} else if (isRecurring && scope === "this_and_following") {
|
|
257411
|
+
if (!instanceEventId) {
|
|
257412
|
+
return respondJson2(res, 400, {
|
|
257413
|
+
ok: false,
|
|
257414
|
+
error: "instanceEventId is required when scope=this_and_following to determine the cut-off date"
|
|
257415
|
+
}), true;
|
|
257416
|
+
}
|
|
257417
|
+
if (token && calendarEventId) {
|
|
257418
|
+
try {
|
|
257419
|
+
const instanceRes = await adminFetch(
|
|
257420
|
+
token,
|
|
257421
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`
|
|
257422
|
+
);
|
|
257423
|
+
if (instanceRes.ok) {
|
|
257424
|
+
const instanceData = await instanceRes.json();
|
|
257425
|
+
const instanceStart = instanceData.start?.dateTime || "";
|
|
257426
|
+
if (instanceStart) {
|
|
257427
|
+
const cutoffDate = new Date(instanceStart);
|
|
257428
|
+
cutoffDate.setDate(cutoffDate.getDate() - 1);
|
|
257429
|
+
const untilStr = cutoffDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
|
257430
|
+
const parentRes = await adminFetch(
|
|
257431
|
+
token,
|
|
257432
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`
|
|
257433
|
+
);
|
|
257434
|
+
if (parentRes.ok) {
|
|
257435
|
+
const parentData = await parentRes.json();
|
|
257436
|
+
const existingRecurrence = Array.isArray(parentData.recurrence) ? parentData.recurrence : [];
|
|
257437
|
+
const updatedRecurrence = existingRecurrence.map((rule) => {
|
|
257438
|
+
if (!rule.startsWith("RRULE:")) return rule;
|
|
257439
|
+
const parts = rule.split(";").filter(
|
|
257440
|
+
(p) => !p.startsWith("COUNT=") && !p.startsWith("UNTIL=")
|
|
257441
|
+
);
|
|
257442
|
+
return [...parts, `UNTIL=${untilStr}`].join(";");
|
|
257443
|
+
});
|
|
257444
|
+
await adminFetch(
|
|
257445
|
+
token,
|
|
257446
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257447
|
+
{
|
|
257448
|
+
method: "PATCH",
|
|
257449
|
+
headers: { "Content-Type": "application/json" },
|
|
257450
|
+
body: JSON.stringify({ recurrence: updatedRecurrence })
|
|
257451
|
+
}
|
|
257452
|
+
);
|
|
257453
|
+
}
|
|
257454
|
+
}
|
|
257455
|
+
}
|
|
257456
|
+
} catch (err) {
|
|
257457
|
+
console.warn(
|
|
257458
|
+
"Failed to update RRULE for this_and_following cancel:",
|
|
257459
|
+
err instanceof Error ? err.message : String(err)
|
|
257460
|
+
);
|
|
257461
|
+
}
|
|
257462
|
+
}
|
|
257463
|
+
await docRef.update({
|
|
257464
|
+
recurrenceUntil: instanceEventId,
|
|
257465
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257466
|
+
});
|
|
257467
|
+
return respondJson2(res, 200, {
|
|
257468
|
+
ok: true,
|
|
257469
|
+
reservationId,
|
|
257470
|
+
scope: "this_and_following",
|
|
257471
|
+
status: "truncated"
|
|
257472
|
+
}), true;
|
|
257473
|
+
} else {
|
|
257474
|
+
if (calendarEventId && token) {
|
|
257475
|
+
try {
|
|
257476
|
+
await adminFetch(
|
|
257477
|
+
token,
|
|
257478
|
+
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
|
|
257479
|
+
{ method: "DELETE" }
|
|
257480
|
+
);
|
|
257481
|
+
} catch {
|
|
257482
|
+
}
|
|
257483
|
+
}
|
|
257484
|
+
await docRef.update({
|
|
257485
|
+
status: "cancelled",
|
|
257486
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
257487
|
+
});
|
|
257488
|
+
return respondJson2(res, 200, {
|
|
257489
|
+
ok: true,
|
|
257490
|
+
reservationId,
|
|
257491
|
+
scope: "all",
|
|
257492
|
+
status: "cancelled"
|
|
257493
|
+
}), true;
|
|
257494
|
+
}
|
|
257495
|
+
}
|
|
257496
|
+
if (method === "GET" && pathname.startsWith("/api/calendar/reserve/")) {
|
|
257497
|
+
const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
|
|
257498
|
+
if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
|
|
257499
|
+
const db = await getFirestore2();
|
|
257500
|
+
if (!db) return firestoreUnavailable(res);
|
|
257501
|
+
const docSnap = await db.collection("reservations").doc(reservationId).get();
|
|
257502
|
+
if (!docSnap.exists) {
|
|
257503
|
+
return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
|
|
257504
|
+
}
|
|
257505
|
+
return respondJson2(res, 200, { ok: true, reservation: docSnap.data() }), true;
|
|
257506
|
+
}
|
|
257507
|
+
if (method === "GET" && pathname === "/api/calendar/reservations") {
|
|
257508
|
+
const db = await getFirestore2();
|
|
257509
|
+
if (!db) return firestoreUnavailable(res);
|
|
257510
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
257511
|
+
const status = url.searchParams.get("status") || "";
|
|
257512
|
+
const date = url.searchParams.get("date") || "";
|
|
257513
|
+
const resourceEmail = url.searchParams.get("resourceEmail") || "";
|
|
257514
|
+
const buildingId = url.searchParams.get("buildingId") || "";
|
|
257515
|
+
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") || DEFAULT_RESERVATION_LIMIT) || DEFAULT_RESERVATION_LIMIT));
|
|
257516
|
+
let query = db.collection("reservations");
|
|
257517
|
+
if (status) {
|
|
257518
|
+
query = query.where("status", "==", status);
|
|
257519
|
+
}
|
|
257520
|
+
if (resourceEmail) {
|
|
257521
|
+
query = query.where("resourceCalendarId", "==", resourceEmail);
|
|
257522
|
+
}
|
|
257523
|
+
if (date) {
|
|
257524
|
+
const dayStart = `${date}T00:00:00`;
|
|
257525
|
+
const dayEnd = `${date}T23:59:59`;
|
|
257526
|
+
query = query.where("startTime", ">=", dayStart).where("startTime", "<=", dayEnd + "Z");
|
|
257527
|
+
}
|
|
257528
|
+
query = query.orderBy("createdAt", "desc").limit(limit);
|
|
257529
|
+
let firestoreDocs;
|
|
257530
|
+
try {
|
|
257531
|
+
const snap = await query.get();
|
|
257532
|
+
firestoreDocs = snap.docs;
|
|
257533
|
+
} catch (err) {
|
|
257534
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257535
|
+
return respondJson2(res, 200, { ok: false, error: `Firestore query failed: ${msg}` }), true;
|
|
257536
|
+
}
|
|
257537
|
+
const token = await getToken2();
|
|
257538
|
+
if (!token) {
|
|
257539
|
+
if (firestoreDocs.length === 0) {
|
|
257540
|
+
return respondJson2(res, 200, { ok: true, reservations: [], count: 0, dataSource: "firestore_fallback" }), true;
|
|
257541
|
+
}
|
|
257542
|
+
const reservations = firestoreDocs.map((doc) => doc.data());
|
|
257543
|
+
return respondJson2(res, 200, {
|
|
257544
|
+
ok: true,
|
|
257545
|
+
reservations,
|
|
257546
|
+
count: reservations.length,
|
|
257547
|
+
dataSource: "firestore_fallback"
|
|
257548
|
+
}), true;
|
|
257549
|
+
}
|
|
257550
|
+
const docsWithEventId = [];
|
|
257551
|
+
const docsWithoutEventId = [];
|
|
257552
|
+
for (const doc of firestoreDocs) {
|
|
257553
|
+
const data = doc.data();
|
|
257554
|
+
if (data.calendarEventId) {
|
|
257555
|
+
docsWithEventId.push(data);
|
|
257556
|
+
} else {
|
|
257557
|
+
docsWithoutEventId.push(data);
|
|
257558
|
+
}
|
|
257559
|
+
}
|
|
257560
|
+
const byCalendar = /* @__PURE__ */ new Map();
|
|
257561
|
+
for (const doc of docsWithEventId) {
|
|
257562
|
+
const calId = doc.resourceCalendarId || "primary";
|
|
257563
|
+
const group = byCalendar.get(calId);
|
|
257564
|
+
if (group) {
|
|
257565
|
+
group.push(doc);
|
|
257566
|
+
} else {
|
|
257567
|
+
byCalendar.set(calId, [doc]);
|
|
257568
|
+
}
|
|
257569
|
+
}
|
|
257570
|
+
const additionalCalendarIds = [];
|
|
257571
|
+
if (resourceEmail && !byCalendar.has(resourceEmail)) {
|
|
257572
|
+
additionalCalendarIds.push(resourceEmail);
|
|
257573
|
+
}
|
|
257574
|
+
if (!resourceEmail) {
|
|
257575
|
+
try {
|
|
257576
|
+
const resources = await fetchResources(token, buildingId || void 0);
|
|
257577
|
+
for (const r2 of resources) {
|
|
257578
|
+
if (r2.resourceEmail && !byCalendar.has(r2.resourceEmail)) {
|
|
257579
|
+
additionalCalendarIds.push(r2.resourceEmail);
|
|
257580
|
+
}
|
|
257581
|
+
}
|
|
257582
|
+
} catch (err) {
|
|
257583
|
+
console.warn(
|
|
257584
|
+
"Failed to fetch resource calendars from Admin SDK:",
|
|
257585
|
+
err instanceof Error ? err.message : String(err)
|
|
257586
|
+
);
|
|
257587
|
+
}
|
|
257588
|
+
}
|
|
257589
|
+
for (const calId of additionalCalendarIds) {
|
|
257590
|
+
if (!byCalendar.has(calId)) {
|
|
257591
|
+
byCalendar.set(calId, []);
|
|
257592
|
+
}
|
|
257593
|
+
}
|
|
257594
|
+
let timeMin;
|
|
257595
|
+
let timeMax;
|
|
257596
|
+
if (date) {
|
|
257597
|
+
timeMin = `${date}T00:00:00+09:00`;
|
|
257598
|
+
timeMax = `${date}T23:59:59+09:00`;
|
|
257599
|
+
} else {
|
|
257600
|
+
const startTimes = docsWithEventId.map((d) => d.startTime).filter(Boolean).sort();
|
|
257601
|
+
if (startTimes.length > 0) {
|
|
257602
|
+
const earliest = new Date(startTimes[0]);
|
|
257603
|
+
earliest.setDate(earliest.getDate() - 1);
|
|
257604
|
+
const latest = new Date(startTimes[startTimes.length - 1]);
|
|
257605
|
+
latest.setDate(latest.getDate() + 1);
|
|
257606
|
+
timeMin = earliest.toISOString();
|
|
257607
|
+
timeMax = latest.toISOString();
|
|
257608
|
+
} else {
|
|
257609
|
+
const now = /* @__PURE__ */ new Date();
|
|
257610
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
257611
|
+
const thirtyDaysAhead = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1e3);
|
|
257612
|
+
timeMin = thirtyDaysAgo.toISOString();
|
|
257613
|
+
timeMax = thirtyDaysAhead.toISOString();
|
|
257614
|
+
}
|
|
257615
|
+
}
|
|
257616
|
+
const gcalEventMap = /* @__PURE__ */ new Map();
|
|
257617
|
+
let gcalFetchFailed = false;
|
|
257618
|
+
try {
|
|
257619
|
+
const calendarFetches = Array.from(byCalendar.keys()).map(async (calendarId) => {
|
|
257620
|
+
const eventsUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?timeMin=${encodeURIComponent(timeMin)}&timeMax=${encodeURIComponent(timeMax)}&singleEvents=true&orderBy=startTime&maxResults=250`;
|
|
257621
|
+
const eventsRes = await adminFetch(token, eventsUrl);
|
|
257622
|
+
if (!eventsRes.ok) {
|
|
257623
|
+
console.warn(
|
|
257624
|
+
`GCal events.list failed for calendar ${calendarId}: ${eventsRes.status}`
|
|
257625
|
+
);
|
|
257626
|
+
return;
|
|
257627
|
+
}
|
|
257628
|
+
const eventsData = await eventsRes.json();
|
|
257629
|
+
const items = Array.isArray(eventsData.items) ? eventsData.items : [];
|
|
257630
|
+
for (const item of items) {
|
|
257631
|
+
const eventId = String(item.id || "");
|
|
257632
|
+
if (!eventId) continue;
|
|
257633
|
+
const startObj = item.start;
|
|
257634
|
+
const endObj = item.end;
|
|
257635
|
+
const isAllDay = Boolean(startObj?.date && !startObj?.dateTime);
|
|
257636
|
+
const eventStart = startObj?.dateTime || startObj?.date || "";
|
|
257637
|
+
const eventEnd = endObj?.dateTime || endObj?.date || "";
|
|
257638
|
+
const creatorObj = item.creator;
|
|
257639
|
+
const organizerObj = item.organizer;
|
|
257640
|
+
gcalEventMap.set(eventId, {
|
|
257641
|
+
startTime: eventStart,
|
|
257642
|
+
endTime: eventEnd,
|
|
257643
|
+
title: String(item.summary || ""),
|
|
257644
|
+
isAllDay,
|
|
257645
|
+
gcalStatus: String(item.status || "confirmed"),
|
|
257646
|
+
creatorEmail: creatorObj?.email || "",
|
|
257647
|
+
creatorDisplayName: creatorObj?.displayName || "",
|
|
257648
|
+
organizerEmail: organizerObj?.email || "",
|
|
257649
|
+
organizerDisplayName: organizerObj?.displayName || "",
|
|
257650
|
+
location: String(item.location || ""),
|
|
257651
|
+
created: String(item.created || ""),
|
|
257652
|
+
updated: String(item.updated || ""),
|
|
257653
|
+
calendarId
|
|
257654
|
+
});
|
|
257655
|
+
}
|
|
257656
|
+
});
|
|
257657
|
+
await Promise.all(calendarFetches);
|
|
257658
|
+
} catch (err) {
|
|
257659
|
+
console.warn(
|
|
257660
|
+
"GCal fetch failed, falling back to Firestore:",
|
|
257661
|
+
err instanceof Error ? err.message : String(err)
|
|
257662
|
+
);
|
|
257663
|
+
gcalFetchFailed = true;
|
|
257664
|
+
}
|
|
257665
|
+
if (gcalFetchFailed) {
|
|
257666
|
+
const reservations = firestoreDocs.map((doc) => doc.data());
|
|
257667
|
+
return respondJson2(res, 200, {
|
|
257668
|
+
ok: true,
|
|
257669
|
+
reservations,
|
|
257670
|
+
count: reservations.length,
|
|
257671
|
+
dataSource: "firestore_fallback"
|
|
257672
|
+
}), true;
|
|
257673
|
+
}
|
|
257674
|
+
const mergedReservations = [];
|
|
257675
|
+
for (const fsDoc of docsWithEventId) {
|
|
257676
|
+
const gcalEvent = gcalEventMap.get(fsDoc.calendarEventId);
|
|
257677
|
+
if (gcalEvent) {
|
|
257678
|
+
const effectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : fsDoc.status;
|
|
257679
|
+
mergedReservations.push({
|
|
257680
|
+
reservationId: fsDoc.reservationId,
|
|
257681
|
+
calendarEventId: fsDoc.calendarEventId,
|
|
257682
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257683
|
+
resourceName: fsDoc.resourceName,
|
|
257684
|
+
startTime: gcalEvent.startTime,
|
|
257685
|
+
// from GCal
|
|
257686
|
+
endTime: gcalEvent.endTime,
|
|
257687
|
+
// from GCal
|
|
257688
|
+
title: gcalEvent.title,
|
|
257689
|
+
// from GCal
|
|
257690
|
+
isAllDay: gcalEvent.isAllDay,
|
|
257691
|
+
status: effectiveStatus,
|
|
257692
|
+
bookerName: fsDoc.bookerName,
|
|
257693
|
+
// from Firestore
|
|
257694
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257695
|
+
// from Firestore
|
|
257696
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257697
|
+
// from Firestore
|
|
257698
|
+
callSid: fsDoc.callSid,
|
|
257699
|
+
// from Firestore
|
|
257700
|
+
bookedVia: fsDoc.bookedVia,
|
|
257701
|
+
// from Firestore
|
|
257702
|
+
createdAt: fsDoc.createdAt,
|
|
257703
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257704
|
+
dataSource: "google_calendar"
|
|
257705
|
+
});
|
|
257706
|
+
} else {
|
|
257707
|
+
mergedReservations.push({
|
|
257708
|
+
reservationId: fsDoc.reservationId,
|
|
257709
|
+
calendarEventId: fsDoc.calendarEventId,
|
|
257710
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257711
|
+
resourceName: fsDoc.resourceName,
|
|
257712
|
+
startTime: fsDoc.startTime,
|
|
257713
|
+
// from Firestore (stale)
|
|
257714
|
+
endTime: fsDoc.endTime,
|
|
257715
|
+
// from Firestore (stale)
|
|
257716
|
+
title: fsDoc.title,
|
|
257717
|
+
// from Firestore (stale)
|
|
257718
|
+
isAllDay: false,
|
|
257719
|
+
status: fsDoc.status === "confirmed" ? "cancelled" : fsDoc.status,
|
|
257720
|
+
bookerName: fsDoc.bookerName,
|
|
257721
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257722
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257723
|
+
callSid: fsDoc.callSid,
|
|
257724
|
+
bookedVia: fsDoc.bookedVia,
|
|
257725
|
+
createdAt: fsDoc.createdAt,
|
|
257726
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257727
|
+
dataSource: "stale"
|
|
257728
|
+
});
|
|
257729
|
+
}
|
|
257730
|
+
}
|
|
257731
|
+
for (const fsDoc of docsWithoutEventId) {
|
|
257732
|
+
mergedReservations.push({
|
|
257733
|
+
reservationId: fsDoc.reservationId,
|
|
257734
|
+
calendarEventId: "",
|
|
257735
|
+
resourceCalendarId: fsDoc.resourceCalendarId,
|
|
257736
|
+
resourceName: fsDoc.resourceName,
|
|
257737
|
+
startTime: fsDoc.startTime,
|
|
257738
|
+
endTime: fsDoc.endTime,
|
|
257739
|
+
title: fsDoc.title,
|
|
257740
|
+
isAllDay: false,
|
|
257741
|
+
status: fsDoc.status,
|
|
257742
|
+
bookerName: fsDoc.bookerName,
|
|
257743
|
+
bookerPhone: fsDoc.bookerPhone,
|
|
257744
|
+
bookerEmail: fsDoc.bookerEmail,
|
|
257745
|
+
callSid: fsDoc.callSid,
|
|
257746
|
+
bookedVia: fsDoc.bookedVia,
|
|
257747
|
+
createdAt: fsDoc.createdAt,
|
|
257748
|
+
...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
|
|
257749
|
+
dataSource: "firestore_fallback"
|
|
257750
|
+
});
|
|
257751
|
+
}
|
|
257752
|
+
const matchedEventIds = new Set(docsWithEventId.map((d) => d.calendarEventId));
|
|
257753
|
+
for (const [eventId, gcalEvent] of gcalEventMap) {
|
|
257754
|
+
if (matchedEventIds.has(eventId)) continue;
|
|
257755
|
+
if (status) {
|
|
257756
|
+
const gcalEffectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed";
|
|
257757
|
+
if (gcalEffectiveStatus !== status) continue;
|
|
257758
|
+
}
|
|
257759
|
+
mergedReservations.push({
|
|
257760
|
+
reservationId: `gcal-${eventId}`,
|
|
257761
|
+
calendarEventId: eventId,
|
|
257762
|
+
resourceCalendarId: gcalEvent.calendarId,
|
|
257763
|
+
resourceName: gcalEvent.location || gcalEvent.organizerDisplayName || "",
|
|
257764
|
+
startTime: gcalEvent.startTime,
|
|
257765
|
+
endTime: gcalEvent.endTime,
|
|
257766
|
+
title: gcalEvent.title || "Direct Calendar Booking",
|
|
257767
|
+
isAllDay: gcalEvent.isAllDay,
|
|
257768
|
+
status: gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed",
|
|
257769
|
+
bookerName: gcalEvent.creatorDisplayName || gcalEvent.creatorEmail || "",
|
|
257770
|
+
bookerPhone: "",
|
|
257771
|
+
bookerEmail: gcalEvent.creatorEmail,
|
|
257772
|
+
callSid: "",
|
|
257773
|
+
bookedVia: "direct_calendar",
|
|
257774
|
+
createdAt: gcalEvent.created,
|
|
257775
|
+
updatedAt: gcalEvent.updated,
|
|
257776
|
+
dataSource: "google_calendar_only"
|
|
257777
|
+
});
|
|
257778
|
+
}
|
|
257779
|
+
return respondJson2(res, 200, {
|
|
257780
|
+
ok: true,
|
|
257781
|
+
reservations: mergedReservations,
|
|
257782
|
+
count: mergedReservations.length,
|
|
257783
|
+
dataSource: "google_calendar"
|
|
257784
|
+
}), true;
|
|
257785
|
+
}
|
|
257786
|
+
return false;
|
|
257787
|
+
}
|
|
257788
|
+
var 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;
|
|
257789
|
+
var init_calendar_api_routes = __esm({
|
|
257790
|
+
"services/desktop/routes/calendar-api-routes.ts"() {
|
|
257791
|
+
init_desktop_server_helpers();
|
|
257792
|
+
init_google_oauth();
|
|
257793
|
+
init_date_tz();
|
|
257794
|
+
init_admin_resources();
|
|
257795
|
+
CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
|
|
257796
|
+
DEFAULT_WORK_START_HOUR = 9;
|
|
257797
|
+
DEFAULT_WORK_END_HOUR = 18;
|
|
257798
|
+
DEFAULT_DURATION_MINUTES = 60;
|
|
257799
|
+
DEFAULT_RESERVATION_LIMIT = 50;
|
|
257800
|
+
SLOT_STEP_MS = 5 * 60 * 1e3;
|
|
257801
|
+
DEFAULT_BUFFER_MINUTES = 10;
|
|
257802
|
+
TOP_SLOT_COUNT = 3;
|
|
257803
|
+
DEFAULT_TIMEZONE = "Asia/Tokyo";
|
|
257804
|
+
_firestoreInstance2 = null;
|
|
257805
|
+
_firestoreInitPromise2 = null;
|
|
257806
|
+
VALID_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["daily", "weekly", "biweekly", "monthly"]);
|
|
257807
|
+
}
|
|
257808
|
+
});
|
|
256285
257809
|
function resolveMaxConcurrency() {
|
|
256286
257810
|
const envRaw = String(process.env.MARIA_DESKTOP_MAX_CONCURRENCY || "").trim();
|
|
256287
257811
|
if (envRaw) {
|
|
@@ -257435,6 +258959,10 @@ function createDesktopRequestHandler(s2) {
|
|
|
257435
258959
|
const handled = await handlePhoneRoute(method, pathname, req, res);
|
|
257436
258960
|
if (handled) return;
|
|
257437
258961
|
}
|
|
258962
|
+
if (pathname.startsWith("/api/calendar/")) {
|
|
258963
|
+
const handled = await handleCalendarApiRoute(method, pathname, req, res);
|
|
258964
|
+
if (handled) return;
|
|
258965
|
+
}
|
|
257438
258966
|
{
|
|
257439
258967
|
const handled = await handleGuiCompatRoute(method, pathname, url, req, res, cwd, s2);
|
|
257440
258968
|
if (handled) return;
|
|
@@ -257649,6 +259177,7 @@ var init_desktop_server = __esm({
|
|
|
257649
259177
|
init_google_oauth_routes();
|
|
257650
259178
|
init_google_api_routes();
|
|
257651
259179
|
init_phone_routes();
|
|
259180
|
+
init_calendar_api_routes();
|
|
257652
259181
|
init_desktop_server_helpers();
|
|
257653
259182
|
init_desktop_data_readers();
|
|
257654
259183
|
init_desktop_job_queue();
|
|
@@ -257991,7 +259520,7 @@ function createSpreadsheetField() {
|
|
|
257991
259520
|
const wantsJson = hasLiteFlag(ctx.parsed, "json");
|
|
257992
259521
|
const titleOpt = typeof ctx.parsed.options.title === "string" ? String(ctx.parsed.options.title).trim() : "";
|
|
257993
259522
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
257994
|
-
const outBaseRel = (outBase || "spreadsheet").replace(/\\/g, "/");
|
|
259523
|
+
const outBaseRel = (outBase || ".maria/desktop/spreadsheet").replace(/\\/g, "/");
|
|
257995
259524
|
const sheetsN = parseSheetsCount(ctx.parsed.options.sheets, 1);
|
|
257996
259525
|
const stamp = nowStamp3();
|
|
257997
259526
|
const outDirRel = path87__namespace.posix.join(
|
|
@@ -259658,7 +261187,7 @@ function createPaperField() {
|
|
|
259658
261187
|
const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : 6;
|
|
259659
261188
|
return Number.isFinite(n) ? Math.max(3, Math.min(20, Math.floor(n))) : 6;
|
|
259660
261189
|
})();
|
|
259661
|
-
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "paper").replace(/\\/g, "/") || "paper";
|
|
261190
|
+
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/paper").replace(/\\/g, "/") || ".maria/desktop/paper";
|
|
259662
261191
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
259663
261192
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
259664
261193
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
@@ -260163,7 +261692,7 @@ function createExamField() {
|
|
|
260163
261692
|
const inlineArgs = ctx.parsed.args.join(" ").trim();
|
|
260164
261693
|
const langRaw = typeof ctx.parsed.options.lang === "string" ? String(ctx.parsed.options.lang).trim().toLowerCase() : "";
|
|
260165
261694
|
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";
|
|
261695
|
+
const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/exam").replace(/\\/g, "/") || ".maria/desktop/exam";
|
|
260167
261696
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
260168
261697
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
|
|
260169
261698
|
const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
|
|
@@ -262207,25 +263736,32 @@ async function queryFreeBusy(token, emails, from, to, signal) {
|
|
|
262207
263736
|
}
|
|
262208
263737
|
async function findMeetingSlots(token, required, optional, durationMin, dateFrom, dateTo, workStart, workEnd, signal, extra) {
|
|
262209
263738
|
let resourceCalendarIds = [];
|
|
263739
|
+
const resourceNameMap = {};
|
|
262210
263740
|
if (extra?.resourceId) {
|
|
262211
263741
|
resourceCalendarIds = [extra.resourceId];
|
|
262212
|
-
} else if (extra?.buildingId || extra?.capacity) {
|
|
262213
263742
|
try {
|
|
262214
|
-
const
|
|
262215
|
-
const
|
|
262216
|
-
if (extra?.buildingId) params.set("query", `buildingId="${extra.buildingId}"`);
|
|
262217
|
-
const rRes = await fetch(`${adminBaseUrl}?${params}`, {
|
|
262218
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
262219
|
-
...signal ? { signal } : {}
|
|
262220
|
-
});
|
|
263743
|
+
const qParams = new URLSearchParams({ maxResults: "5", query: `resourceEmail="${extra.resourceId}"` });
|
|
263744
|
+
const rRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars?${qParams}`, signal ? { signal } : void 0);
|
|
262221
263745
|
if (rRes.ok) {
|
|
262222
263746
|
const rData = await rRes.json();
|
|
262223
263747
|
const items = Array.isArray(rData.items) ? rData.items : [];
|
|
262224
|
-
const
|
|
262225
|
-
|
|
262226
|
-
|
|
262227
|
-
|
|
262228
|
-
|
|
263748
|
+
for (const r2 of items) {
|
|
263749
|
+
const email = String(r2.resourceEmail || "");
|
|
263750
|
+
const name = String(r2.resourceName || r2.generatedResourceName || "");
|
|
263751
|
+
if (email && name) resourceNameMap[email] = name;
|
|
263752
|
+
}
|
|
263753
|
+
}
|
|
263754
|
+
} catch {
|
|
263755
|
+
}
|
|
263756
|
+
} else if (extra?.buildingId || extra?.capacity) {
|
|
263757
|
+
try {
|
|
263758
|
+
const result = await listResourcesRaw(token, { buildingId: extra?.buildingId, capacity: extra?.capacity }, signal);
|
|
263759
|
+
if (result.ok && result.data) {
|
|
263760
|
+
resourceCalendarIds = result.data.map((r2) => r2.resourceEmail).filter(Boolean);
|
|
263761
|
+
for (const r2 of result.data) {
|
|
263762
|
+
const name = r2.resourceName || r2.generatedResourceName || "";
|
|
263763
|
+
if (r2.resourceEmail && name) resourceNameMap[r2.resourceEmail] = name;
|
|
263764
|
+
}
|
|
262229
263765
|
}
|
|
262230
263766
|
} catch {
|
|
262231
263767
|
}
|
|
@@ -262277,6 +263813,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262277
263813
|
let requiredOk = true;
|
|
262278
263814
|
let availCount = 0;
|
|
262279
263815
|
const missingOpt = [];
|
|
263816
|
+
const slotAvailResources = [];
|
|
262280
263817
|
for (const email of allEmails) {
|
|
262281
263818
|
const busy = busyMap[email] || [];
|
|
262282
263819
|
const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
|
|
@@ -262288,6 +263825,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262288
263825
|
}
|
|
262289
263826
|
} else {
|
|
262290
263827
|
availCount++;
|
|
263828
|
+
if (resourceEmails.includes(email)) {
|
|
263829
|
+
slotAvailResources.push(email);
|
|
263830
|
+
}
|
|
262291
263831
|
}
|
|
262292
263832
|
}
|
|
262293
263833
|
if (!requiredOk) continue;
|
|
@@ -262306,7 +263846,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262306
263846
|
}
|
|
262307
263847
|
if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
|
|
262308
263848
|
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 });
|
|
263849
|
+
candidates.push({ start: slotS, end: slotE, allAvailable: availCount === allEmails.length, availableCount: availCount, missingOptional: missingOpt, bufferBefore: bufBefore, bufferAfter: bufAfter, availableResources: slotAvailResources });
|
|
262310
263850
|
}
|
|
262311
263851
|
}
|
|
262312
263852
|
const hasSuf = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
|
|
@@ -262347,6 +263887,10 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262347
263887
|
const label = c.allAvailable ? `All ${allEmails.length} available` : `${c.availableCount}/${allEmails.length} available`;
|
|
262348
263888
|
lines.push(` #${i2 + 1}: ${s2.toLocaleString()} \u2192 ${e2.toLocaleTimeString()} (${label})`);
|
|
262349
263889
|
if (c.missingOptional.length) lines.push(` Unavailable (optional): ${c.missingOptional.join(", ")}`);
|
|
263890
|
+
if (c.availableResources.length > 0) {
|
|
263891
|
+
const names = c.availableResources.map((e3) => resourceNameMap[e3] || e3);
|
|
263892
|
+
lines.push(` Available resources: ${names.join(", ")}`);
|
|
263893
|
+
}
|
|
262350
263894
|
lines.push(` Buffer: ${Math.round(c.bufferBefore / 6e4)} min before, ${Math.round(c.bufferAfter / 6e4)} min after`);
|
|
262351
263895
|
lines.push("");
|
|
262352
263896
|
}
|
|
@@ -262364,7 +263908,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
|
|
|
262364
263908
|
missingOptional: c.missingOptional,
|
|
262365
263909
|
bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
|
|
262366
263910
|
bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
|
|
262367
|
-
bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4)
|
|
263911
|
+
bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4),
|
|
263912
|
+
resourceEmails: c.availableResources,
|
|
263913
|
+
resourceNames: c.availableResources.map((e2) => resourceNameMap[e2] || e2)
|
|
262368
263914
|
}))
|
|
262369
263915
|
}
|
|
262370
263916
|
};
|
|
@@ -262596,46 +264142,17 @@ function formatImportSummary(results, totalRecords, isDryRun) {
|
|
|
262596
264142
|
}
|
|
262597
264143
|
return lines;
|
|
262598
264144
|
}
|
|
262599
|
-
async function
|
|
262600
|
-
const
|
|
262601
|
-
|
|
262602
|
-
|
|
262603
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
262604
|
-
...signal ? { signal } : {}
|
|
262605
|
-
});
|
|
262606
|
-
if (res.ok) {
|
|
262607
|
-
const data = await res.json();
|
|
262608
|
-
const buildings = Array.isArray(data.buildings) ? data.buildings : [];
|
|
262609
|
-
for (const b of buildings) {
|
|
262610
|
-
if (typeof b.buildingId === "string" && b.buildingId) {
|
|
262611
|
-
ids.add(b.buildingId);
|
|
262612
|
-
}
|
|
262613
|
-
}
|
|
262614
|
-
}
|
|
262615
|
-
} catch {
|
|
262616
|
-
}
|
|
262617
|
-
return ids;
|
|
262618
|
-
}
|
|
262619
|
-
async function listResources(token, buildingId, signal) {
|
|
262620
|
-
const params = new URLSearchParams({ maxResults: "200" });
|
|
262621
|
-
if (buildingId) params.set("query", `buildingId="${buildingId}"`);
|
|
262622
|
-
const res = await fetch(`${ADMIN_RESOURCES_BASE}/calendars?${params}`, {
|
|
262623
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
262624
|
-
...signal ? { signal } : {}
|
|
262625
|
-
});
|
|
262626
|
-
if (!res.ok) {
|
|
262627
|
-
const err = await res.text().catch(() => "");
|
|
262628
|
-
return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
|
|
262629
|
-
}
|
|
262630
|
-
const data = await res.json();
|
|
262631
|
-
const items = Array.isArray(data.items) ? data.items : [];
|
|
264145
|
+
async function fmtListResources(token, buildingId, signal) {
|
|
264146
|
+
const result = await listResourcesRaw(token, { buildingId }, signal);
|
|
264147
|
+
if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
|
|
264148
|
+
const items = result.data;
|
|
262632
264149
|
if (items.length === 0) {
|
|
262633
264150
|
const filter = buildingId ? ` (building: ${buildingId})` : "";
|
|
262634
264151
|
return { text: `No resource calendars found${filter}.`, json: { ok: true, resources: [] } };
|
|
262635
264152
|
}
|
|
262636
264153
|
const lines = [`${items.length} resource calendar(s):`, ""];
|
|
262637
264154
|
for (const r2 of items) {
|
|
262638
|
-
const cap =
|
|
264155
|
+
const cap = r2.capacity ? ` (capacity: ${r2.capacity})` : "";
|
|
262639
264156
|
const bld = r2.buildingId ? ` [building: ${r2.buildingId}]` : "";
|
|
262640
264157
|
lines.push(` ${r2.resourceName || r2.generatedResourceName || "Unnamed"}${cap}${bld}`);
|
|
262641
264158
|
lines.push(` ID: ${r2.resourceId}`);
|
|
@@ -262646,22 +264163,16 @@ async function listResources(token, buildingId, signal) {
|
|
|
262646
264163
|
}
|
|
262647
264164
|
return { text: lines.join("\n"), json: { ok: true, resources: items } };
|
|
262648
264165
|
}
|
|
262649
|
-
async function
|
|
262650
|
-
const
|
|
262651
|
-
|
|
262652
|
-
|
|
262653
|
-
});
|
|
262654
|
-
if (!res.ok) {
|
|
262655
|
-
const err = await res.text().catch(() => "");
|
|
262656
|
-
return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
|
|
262657
|
-
}
|
|
262658
|
-
const r2 = await res.json();
|
|
264166
|
+
async function fmtGetResource(token, resourceId, signal) {
|
|
264167
|
+
const result = await getResourceRaw(token, resourceId, signal);
|
|
264168
|
+
if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
|
|
264169
|
+
const r2 = result.data;
|
|
262659
264170
|
const lines = [
|
|
262660
264171
|
`Resource: ${r2.resourceName || r2.generatedResourceName || "Unnamed"}`,
|
|
262661
264172
|
` ID: ${r2.resourceId}`,
|
|
262662
264173
|
` Email: ${r2.resourceEmail || "N/A"}`,
|
|
262663
264174
|
` Type: ${r2.resourceType || "N/A"}`,
|
|
262664
|
-
|
|
264175
|
+
r2.capacity ? ` Capacity: ${r2.capacity}` : "",
|
|
262665
264176
|
r2.buildingId ? ` Building: ${r2.buildingId}` : "",
|
|
262666
264177
|
r2.floorName ? ` Floor: ${r2.floorName}` : "",
|
|
262667
264178
|
r2.resourceDescription ? ` Description: ${r2.resourceDescription}` : "",
|
|
@@ -262669,26 +264180,10 @@ async function getResource(token, resourceId, signal) {
|
|
|
262669
264180
|
].filter(Boolean);
|
|
262670
264181
|
return { text: lines.join("\n"), json: { ok: true, resource: r2 } };
|
|
262671
264182
|
}
|
|
262672
|
-
async function
|
|
262673
|
-
const
|
|
262674
|
-
|
|
262675
|
-
|
|
262676
|
-
};
|
|
262677
|
-
if (params.buildingId) body.buildingId = params.buildingId;
|
|
262678
|
-
if (params.capacity) body.capacity = params.capacity;
|
|
262679
|
-
if (params.floorName) body.floorName = params.floorName;
|
|
262680
|
-
if (params.description) body.resourceDescription = params.description;
|
|
262681
|
-
const res = await fetch(`${ADMIN_RESOURCES_BASE}/calendars`, {
|
|
262682
|
-
method: "POST",
|
|
262683
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262684
|
-
body: JSON.stringify(body),
|
|
262685
|
-
...signal ? { signal } : {}
|
|
262686
|
-
});
|
|
262687
|
-
if (!res.ok) {
|
|
262688
|
-
const err = await res.text().catch(() => "");
|
|
262689
|
-
return { text: `Admin API create error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
|
|
262690
|
-
}
|
|
262691
|
-
const r2 = await res.json();
|
|
264183
|
+
async function fmtCreateResource(token, params, signal) {
|
|
264184
|
+
const result = await createResourceRaw(token, params, signal);
|
|
264185
|
+
if (!result.ok) return { text: `Admin API create error: ${result.error}`, json: { ok: false, error: result.error } };
|
|
264186
|
+
const r2 = result.data;
|
|
262692
264187
|
return {
|
|
262693
264188
|
text: `Created resource: "${r2.resourceName}" (id=${r2.resourceId}, email=${r2.resourceEmail})`,
|
|
262694
264189
|
json: { ok: true, created: true, resource: r2 }
|
|
@@ -262769,9 +264264,9 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
|
|
|
262769
264264
|
if (rec.floor || rec.floorName) body.floorName = rec.floor || rec.floorName;
|
|
262770
264265
|
if (rec.description || rec.resourceDescription) body.resourceDescription = rec.description || rec.resourceDescription;
|
|
262771
264266
|
try {
|
|
262772
|
-
const createRes = await
|
|
264267
|
+
const createRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars`, {
|
|
262773
264268
|
method: "POST",
|
|
262774
|
-
headers: {
|
|
264269
|
+
headers: { "Content-Type": "application/json" },
|
|
262775
264270
|
body: JSON.stringify(body),
|
|
262776
264271
|
...signal ? { signal } : {}
|
|
262777
264272
|
});
|
|
@@ -262799,54 +264294,25 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
|
|
|
262799
264294
|
}
|
|
262800
264295
|
};
|
|
262801
264296
|
}
|
|
262802
|
-
async function
|
|
262803
|
-
const
|
|
262804
|
-
|
|
262805
|
-
|
|
262806
|
-
});
|
|
262807
|
-
if (!res.ok) {
|
|
262808
|
-
const err = await res.text().catch(() => "");
|
|
262809
|
-
return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
|
|
262810
|
-
}
|
|
262811
|
-
const data = await res.json();
|
|
262812
|
-
const buildings = Array.isArray(data.buildings) ? data.buildings : [];
|
|
264297
|
+
async function fmtListBuildings(token, signal) {
|
|
264298
|
+
const result = await listBuildingsRaw(token, signal);
|
|
264299
|
+
if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
|
|
264300
|
+
const buildings = result.data;
|
|
262813
264301
|
if (buildings.length === 0) return { text: "No buildings found.", json: { ok: true, buildings: [] } };
|
|
262814
264302
|
const lines = [`${buildings.length} building(s):`, ""];
|
|
262815
264303
|
for (const b of buildings) {
|
|
262816
264304
|
lines.push(` ${b.buildingName || "Unnamed"}`);
|
|
262817
264305
|
lines.push(` ID: ${b.buildingId}`);
|
|
262818
264306
|
if (b.description) lines.push(` Description: ${b.description}`);
|
|
262819
|
-
if (
|
|
264307
|
+
if (b.floorNames.length) lines.push(` Floors: ${b.floorNames.join(", ")}`);
|
|
262820
264308
|
lines.push("");
|
|
262821
264309
|
}
|
|
262822
264310
|
return { text: lines.join("\n"), json: { ok: true, buildings } };
|
|
262823
264311
|
}
|
|
262824
|
-
async function
|
|
262825
|
-
const
|
|
262826
|
-
|
|
262827
|
-
|
|
262828
|
-
buildingName: params.name
|
|
262829
|
-
};
|
|
262830
|
-
if (params.description) body.description = params.description;
|
|
262831
|
-
if (params.floors) {
|
|
262832
|
-
const n = Number(params.floors);
|
|
262833
|
-
if (n > 0) {
|
|
262834
|
-
body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
|
|
262835
|
-
} else {
|
|
262836
|
-
body.floorNames = params.floors.split(",").map((f3) => f3.trim()).filter(Boolean);
|
|
262837
|
-
}
|
|
262838
|
-
}
|
|
262839
|
-
const res = await fetch(`${ADMIN_RESOURCES_BASE}/buildings`, {
|
|
262840
|
-
method: "POST",
|
|
262841
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
262842
|
-
body: JSON.stringify(body),
|
|
262843
|
-
...signal ? { signal } : {}
|
|
262844
|
-
});
|
|
262845
|
-
if (!res.ok) {
|
|
262846
|
-
const err = await res.text().catch(() => "");
|
|
262847
|
-
return { text: `Admin API create error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
|
|
262848
|
-
}
|
|
262849
|
-
const b = await res.json();
|
|
264312
|
+
async function fmtCreateBuilding(token, params, signal) {
|
|
264313
|
+
const result = await createBuildingRaw(token, params, signal);
|
|
264314
|
+
if (!result.ok) return { text: `Admin API create error: ${result.error}`, json: { ok: false, error: result.error } };
|
|
264315
|
+
const b = result.data;
|
|
262850
264316
|
return {
|
|
262851
264317
|
text: `Created building: "${b.buildingName}" (id=${b.buildingId})`,
|
|
262852
264318
|
json: { ok: true, created: true, building: b }
|
|
@@ -262915,18 +264381,16 @@ async function bulkImportBuildings(token, filePath4, signal, dryRun = false) {
|
|
|
262915
264381
|
const rec = records[i2];
|
|
262916
264382
|
const row = i2 + 2;
|
|
262917
264383
|
const name = rec.name || rec.buildingName || "";
|
|
262918
|
-
const bldId = (rec.id || rec.buildingId || name
|
|
264384
|
+
const bldId = slugify2(rec.id || rec.buildingId || name, "bld");
|
|
262919
264385
|
const body = { buildingId: bldId, buildingName: name };
|
|
262920
264386
|
if (rec.description) body.description = rec.description;
|
|
262921
264387
|
if (rec.floors || rec.floorNames) {
|
|
262922
|
-
|
|
262923
|
-
const n = Number(f3);
|
|
262924
|
-
body.floorNames = n > 0 ? Array.from({ length: n }, (_, i22) => String(i22 + 1)) : f3.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
264388
|
+
body.floorNames = parseFloorNames(rec.floors || rec.floorNames);
|
|
262925
264389
|
}
|
|
262926
264390
|
try {
|
|
262927
|
-
const createRes = await
|
|
264391
|
+
const createRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings`, {
|
|
262928
264392
|
method: "POST",
|
|
262929
|
-
headers: {
|
|
264393
|
+
headers: { "Content-Type": "application/json" },
|
|
262930
264394
|
body: JSON.stringify(body),
|
|
262931
264395
|
...signal ? { signal } : {}
|
|
262932
264396
|
});
|
|
@@ -263003,12 +264467,13 @@ async function checkAvailability(token, targets, datetime, durationMinutes, sign
|
|
|
263003
264467
|
function createGcalField() {
|
|
263004
264468
|
return { commandId: "gcal", worker: new GcalWorker(), checker: new AlwaysPassChecker11() };
|
|
263005
264469
|
}
|
|
263006
|
-
var GcalWorker,
|
|
264470
|
+
var GcalWorker, EMAIL_REGEX, AlwaysPassChecker11;
|
|
263007
264471
|
var init_gcal_field = __esm({
|
|
263008
264472
|
"commands/google/gcal.field.ts"() {
|
|
263009
264473
|
init_base2();
|
|
263010
264474
|
init_google_oauth();
|
|
263011
264475
|
init_date_tz();
|
|
264476
|
+
init_admin_resources();
|
|
263012
264477
|
GcalWorker = class extends LiteWorkerAgent {
|
|
263013
264478
|
commandId = "gcal";
|
|
263014
264479
|
help = {
|
|
@@ -263116,17 +264581,17 @@ var init_gcal_field = __esm({
|
|
|
263116
264581
|
const resSub = (ctx.parsed.args?.[1] || "list").toLowerCase();
|
|
263117
264582
|
if (resSub === "list") {
|
|
263118
264583
|
const buildingId = String(ctx.parsed.options.building || "").trim() || void 0;
|
|
263119
|
-
return await
|
|
264584
|
+
return await fmtListResources(token, buildingId, ctx.abortSignal);
|
|
263120
264585
|
}
|
|
263121
264586
|
if (resSub === "get") {
|
|
263122
264587
|
const resourceId = String(ctx.parsed.args?.[2] || ctx.parsed.options.id || "").trim();
|
|
263123
264588
|
if (!resourceId) return { text: "gcal resources get: resourceId is required" };
|
|
263124
|
-
return await
|
|
264589
|
+
return await fmtGetResource(token, resourceId, ctx.abortSignal);
|
|
263125
264590
|
}
|
|
263126
264591
|
if (resSub === "create") {
|
|
263127
264592
|
const name = String(ctx.parsed.options.name || "").trim();
|
|
263128
264593
|
if (!name) return { text: "gcal resources create: --name is required" };
|
|
263129
|
-
return await
|
|
264594
|
+
return await fmtCreateResource(token, {
|
|
263130
264595
|
name,
|
|
263131
264596
|
buildingId: String(ctx.parsed.options.building || "").trim() || void 0,
|
|
263132
264597
|
capacity: Number(ctx.parsed.options.capacity || "0") || void 0,
|
|
@@ -263146,12 +264611,12 @@ var init_gcal_field = __esm({
|
|
|
263146
264611
|
if (sub === "buildings") {
|
|
263147
264612
|
const bldSub = (ctx.parsed.args?.[1] || "list").toLowerCase();
|
|
263148
264613
|
if (bldSub === "list") {
|
|
263149
|
-
return await
|
|
264614
|
+
return await fmtListBuildings(token, ctx.abortSignal);
|
|
263150
264615
|
}
|
|
263151
264616
|
if (bldSub === "create") {
|
|
263152
264617
|
const name = String(ctx.parsed.options.name || "").trim();
|
|
263153
264618
|
if (!name) return { text: "gcal buildings create: --name is required" };
|
|
263154
|
-
return await
|
|
264619
|
+
return await fmtCreateBuilding(token, {
|
|
263155
264620
|
name,
|
|
263156
264621
|
floors: String(ctx.parsed.options.floors || "").trim() || void 0,
|
|
263157
264622
|
description: String(ctx.parsed.options.description || "").trim() || void 0
|
|
@@ -263180,7 +264645,6 @@ var init_gcal_field = __esm({
|
|
|
263180
264645
|
}
|
|
263181
264646
|
];
|
|
263182
264647
|
};
|
|
263183
|
-
ADMIN_RESOURCES_BASE = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
|
|
263184
264648
|
EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
263185
264649
|
AlwaysPassChecker11 = class extends LiteCheckerAgent {
|
|
263186
264650
|
commandId = "gcal";
|
|
@@ -286736,7 +288200,7 @@ var require_firestore = __commonJS({
|
|
|
286736
288200
|
"../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/firestore/index.js"(exports2) {
|
|
286737
288201
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
286738
288202
|
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 =
|
|
288203
|
+
exports2.getFirestore = getFirestore4;
|
|
286740
288204
|
exports2.initializeFirestore = initializeFirestore2;
|
|
286741
288205
|
var app_1 = require_app();
|
|
286742
288206
|
var firestore_internal_1 = require_firestore_internal();
|
|
@@ -286817,7 +288281,7 @@ var require_firestore = __commonJS({
|
|
|
286817
288281
|
Object.defineProperty(exports2, "setLogFunction", { enumerable: true, get: function() {
|
|
286818
288282
|
return firestore_1.setLogFunction;
|
|
286819
288283
|
} });
|
|
286820
|
-
function
|
|
288284
|
+
function getFirestore4(appOrDatabaseId, optionalDatabaseId) {
|
|
286821
288285
|
const app = typeof appOrDatabaseId === "object" ? appOrDatabaseId : (0, app_1.getApp)();
|
|
286822
288286
|
const databaseId = (typeof appOrDatabaseId === "string" ? appOrDatabaseId : optionalDatabaseId) || path_1.DEFAULT_DATABASE_ID;
|
|
286823
288287
|
const firebaseApp = app;
|
|
@@ -286839,7 +288303,7 @@ var require_firestore = __commonJS({
|
|
|
286839
288303
|
});
|
|
286840
288304
|
|
|
286841
288305
|
// ../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,
|
|
288306
|
+
var import_firestore2, getFirestore3;
|
|
286843
288307
|
var init_firestore = __esm({
|
|
286844
288308
|
"../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/esm/firestore/index.js"() {
|
|
286845
288309
|
import_firestore2 = __toESM(require_firestore(), 1);
|
|
@@ -286867,7 +288331,7 @@ var init_firestore = __esm({
|
|
|
286867
288331
|
import_firestore2.default.Transaction;
|
|
286868
288332
|
import_firestore2.default.WriteBatch;
|
|
286869
288333
|
import_firestore2.default.WriteResult;
|
|
286870
|
-
|
|
288334
|
+
getFirestore3 = import_firestore2.default.getFirestore;
|
|
286871
288335
|
import_firestore2.default.initializeFirestore;
|
|
286872
288336
|
import_firestore2.default.setLogFunction;
|
|
286873
288337
|
import_firestore2.default.v1;
|
|
@@ -287173,7 +288637,7 @@ var init_billing_pl_field = __esm({
|
|
|
287173
288637
|
json: { error: "access_denied", reason: accessCheck.reason }
|
|
287174
288638
|
};
|
|
287175
288639
|
}
|
|
287176
|
-
const firestore =
|
|
288640
|
+
const firestore = getFirestore3();
|
|
287177
288641
|
const tenantPaths = new TenantPaths(tenantId);
|
|
287178
288642
|
let targetProjectId = projectId;
|
|
287179
288643
|
if (isNaturalLanguage && projectNameFromNL && !projectId) {
|
|
@@ -287495,7 +288959,7 @@ function createCompetitorsField() {
|
|
|
287495
288959
|
const maxRoundsRaw = typeof ctx.parsed.options["max-rounds"] === "string" ? Number(ctx.parsed.options["max-rounds"]) : 10;
|
|
287496
288960
|
const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(20, Math.floor(maxRoundsRaw))) : 10;
|
|
287497
288961
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
287498
|
-
const outBaseRel = (outBase || "competitors").replace(/\\/g, "/");
|
|
288962
|
+
const outBaseRel = (outBase || ".maria/desktop/competitors").replace(/\\/g, "/");
|
|
287499
288963
|
const inOpt = typeof ctx.parsed.options.in === "string" ? String(ctx.parsed.options.in).trim() : "";
|
|
287500
288964
|
let productDesc = ctx.parsed.args.join(" ").trim();
|
|
287501
288965
|
if (inOpt) {
|
|
@@ -288179,8 +289643,9 @@ function computeWeekWindows(untilDateStr, totalDays) {
|
|
|
288179
289643
|
}
|
|
288180
289644
|
function sparkline(scores, min = 1, max = 5) {
|
|
288181
289645
|
return scores.map((s2) => {
|
|
288182
|
-
const
|
|
288183
|
-
|
|
289646
|
+
const safe = Number.isFinite(s2) ? s2 : 3;
|
|
289647
|
+
const idx = Math.round((safe - min) / (max - min) * (SPARK_CHARS.length - 1));
|
|
289648
|
+
return SPARK_CHARS[Math.max(0, Math.min(SPARK_CHARS.length - 1, idx))] ?? SPARK_CHARS[4];
|
|
288184
289649
|
}).join("");
|
|
288185
289650
|
}
|
|
288186
289651
|
function computeTrend(scores) {
|
|
@@ -288191,7 +289656,7 @@ function computeTrend(scores) {
|
|
|
288191
289656
|
let num = 0;
|
|
288192
289657
|
let den = 0;
|
|
288193
289658
|
for (let i2 = 0; i2 < n; i2++) {
|
|
288194
|
-
num += (i2 - xMean) * ((scores[i2] ?? 3) - yMean);
|
|
289659
|
+
num += (i2 - xMean) * (safeScore(scores[i2] ?? 3) - yMean);
|
|
288195
289660
|
den += (i2 - xMean) ** 2;
|
|
288196
289661
|
}
|
|
288197
289662
|
const slope = den === 0 ? 0 : num / den;
|
|
@@ -288209,6 +289674,28 @@ function calcMean(values) {
|
|
|
288209
289674
|
if (values.length === 0) return 0;
|
|
288210
289675
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
288211
289676
|
}
|
|
289677
|
+
function clamp(v, lo, hi) {
|
|
289678
|
+
return Math.min(hi, Math.max(lo, v));
|
|
289679
|
+
}
|
|
289680
|
+
function safeScore(v) {
|
|
289681
|
+
const n = typeof v === "string" ? parseFloat(v) : Number(v);
|
|
289682
|
+
if (!Number.isFinite(n)) return 3;
|
|
289683
|
+
return clamp(n, 1, 5);
|
|
289684
|
+
}
|
|
289685
|
+
function sanitizeSubDimensions(raw, expected) {
|
|
289686
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
289687
|
+
return expected.map((s2) => ({ id: s2.id, score: 3, evidence: ["Parse incomplete"] }));
|
|
289688
|
+
}
|
|
289689
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
289690
|
+
for (const r2 of raw) {
|
|
289691
|
+
if (r2 && typeof r2.id === "string") lookup.set(r2.id.trim().toLowerCase(), r2);
|
|
289692
|
+
}
|
|
289693
|
+
return expected.map((s2) => {
|
|
289694
|
+
const found = lookup.get(s2.id.toLowerCase());
|
|
289695
|
+
if (found) return { id: s2.id, score: safeScore(found.score), evidence: Array.isArray(found.evidence) ? found.evidence : ["(no evidence)"] };
|
|
289696
|
+
return { id: s2.id, score: 3, evidence: ["Sub-dimension not returned by evaluator"] };
|
|
289697
|
+
});
|
|
289698
|
+
}
|
|
288212
289699
|
function trendEmoji(t2) {
|
|
288213
289700
|
return t2 === "improving" ? "\u2191" : t2 === "declining" ? "\u2193" : "\u2192";
|
|
288214
289701
|
}
|
|
@@ -288226,7 +289713,7 @@ function computeAllTrends(weeks) {
|
|
|
288226
289713
|
const weeklyScores = weeks.map((w) => {
|
|
288227
289714
|
const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
|
|
288228
289715
|
const sd = de?.subDimensions.find((s2) => s2.id === sub.id);
|
|
288229
|
-
return sd?.score ?? 3;
|
|
289716
|
+
return safeScore(sd?.score ?? 3);
|
|
288230
289717
|
});
|
|
288231
289718
|
trends.push({
|
|
288232
289719
|
id: sub.id,
|
|
@@ -288241,7 +289728,7 @@ function computeAllTrends(weeks) {
|
|
|
288241
289728
|
}
|
|
288242
289729
|
const weeklyOverall = weeks.map((w) => {
|
|
288243
289730
|
const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
|
|
288244
|
-
return de?.overallScore ?? 3;
|
|
289731
|
+
return safeScore(de?.overallScore ?? 3);
|
|
288245
289732
|
});
|
|
288246
289733
|
trends.push({
|
|
288247
289734
|
id: dim.id,
|
|
@@ -288277,7 +289764,7 @@ function computeQuantity(ghData) {
|
|
|
288277
289764
|
};
|
|
288278
289765
|
}
|
|
288279
289766
|
async function loadPreviousWeekDominants(cwd, username, currentOutDir) {
|
|
288280
|
-
const baseDir = path87__namespace.resolve(cwd, "dev-adviser");
|
|
289767
|
+
const baseDir = path87__namespace.resolve(cwd, ".maria/desktop/dev-adviser");
|
|
288281
289768
|
let entries;
|
|
288282
289769
|
try {
|
|
288283
289770
|
entries = await fsp10__namespace.readdir(baseDir);
|
|
@@ -288338,6 +289825,75 @@ function neutralWorkClassification() {
|
|
|
288338
289825
|
dominantCategory: "new-feature"
|
|
288339
289826
|
};
|
|
288340
289827
|
}
|
|
289828
|
+
function validateEvalRaw(raw, numWeeks) {
|
|
289829
|
+
const issues = [];
|
|
289830
|
+
if (!raw || !Array.isArray(raw.weeks)) {
|
|
289831
|
+
issues.push("Top-level 'weeks' array is missing or not an array.");
|
|
289832
|
+
return { ok: false, issues };
|
|
289833
|
+
}
|
|
289834
|
+
if (raw.weeks.length < numWeeks) {
|
|
289835
|
+
issues.push(`Expected ${numWeeks} weeks but got ${raw.weeks.length}.`);
|
|
289836
|
+
}
|
|
289837
|
+
for (let wi = 0; wi < numWeeks; wi++) {
|
|
289838
|
+
const w = raw.weeks[wi];
|
|
289839
|
+
if (!w) {
|
|
289840
|
+
issues.push(`Week ${wi} is missing entirely.`);
|
|
289841
|
+
continue;
|
|
289842
|
+
}
|
|
289843
|
+
if (!w.architectQuality) {
|
|
289844
|
+
issues.push(`Week ${wi}: architectQuality is missing.`);
|
|
289845
|
+
} else {
|
|
289846
|
+
if (!Array.isArray(w.architectQuality.subDimensions) || w.architectQuality.subDimensions.length === 0) {
|
|
289847
|
+
issues.push(`Week ${wi}: architectQuality.subDimensions is empty or missing.`);
|
|
289848
|
+
} else {
|
|
289849
|
+
const returnedIds = new Set(w.architectQuality.subDimensions.map((s2) => String(s2?.id || "")));
|
|
289850
|
+
for (const eid of EXPECTED_AQ_IDS) {
|
|
289851
|
+
if (!returnedIds.has(eid)) issues.push(`Week ${wi}: architectQuality sub-dimension "${eid}" is missing.`);
|
|
289852
|
+
}
|
|
289853
|
+
}
|
|
289854
|
+
if (typeof w.architectQuality.overallScore !== "number") {
|
|
289855
|
+
issues.push(`Week ${wi}: architectQuality.overallScore is not a number.`);
|
|
289856
|
+
}
|
|
289857
|
+
}
|
|
289858
|
+
if (!w.semanticQuality) {
|
|
289859
|
+
issues.push(`Week ${wi}: semanticQuality is missing.`);
|
|
289860
|
+
} else {
|
|
289861
|
+
if (!Array.isArray(w.semanticQuality.subDimensions) || w.semanticQuality.subDimensions.length === 0) {
|
|
289862
|
+
issues.push(`Week ${wi}: semanticQuality.subDimensions is empty or missing.`);
|
|
289863
|
+
} else {
|
|
289864
|
+
const returnedIds = new Set(w.semanticQuality.subDimensions.map((s2) => String(s2?.id || "")));
|
|
289865
|
+
for (const eid of EXPECTED_SQ_IDS) {
|
|
289866
|
+
if (!returnedIds.has(eid)) issues.push(`Week ${wi}: semanticQuality sub-dimension "${eid}" is missing.`);
|
|
289867
|
+
}
|
|
289868
|
+
}
|
|
289869
|
+
if (typeof w.semanticQuality.overallScore !== "number") {
|
|
289870
|
+
issues.push(`Week ${wi}: semanticQuality.overallScore is not a number.`);
|
|
289871
|
+
}
|
|
289872
|
+
}
|
|
289873
|
+
}
|
|
289874
|
+
return { ok: issues.length === 0, issues };
|
|
289875
|
+
}
|
|
289876
|
+
function buildRepairPrompt(brokenText, issues, numWeeks) {
|
|
289877
|
+
const aqIds = ARCHITECT_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
|
|
289878
|
+
const sqIds = SEMANTIC_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
|
|
289879
|
+
return [
|
|
289880
|
+
"MODEL_FIX: Your previous evaluation output is malformed. Repair it and return valid JSON only.",
|
|
289881
|
+
"",
|
|
289882
|
+
"Issues found:",
|
|
289883
|
+
...issues.map((i2) => ` - ${i2}`),
|
|
289884
|
+
"",
|
|
289885
|
+
`Required: ${numWeeks} weeks, each with architectQuality and semanticQuality.`,
|
|
289886
|
+
`architectQuality sub-dimension IDs: [${aqIds}]`,
|
|
289887
|
+
`semanticQuality sub-dimension IDs: [${sqIds}]`,
|
|
289888
|
+
"Each sub-dimension needs: { id, score (1.0-5.0), evidence (string[]) }",
|
|
289889
|
+
"Each dimension needs: { subDimensions, overallScore (1.0-5.0), summary (string) }",
|
|
289890
|
+
"",
|
|
289891
|
+
"Your broken output (fix this and return correct JSON only):",
|
|
289892
|
+
"```",
|
|
289893
|
+
brokenText.slice(0, 12e3),
|
|
289894
|
+
"```"
|
|
289895
|
+
].join("\n");
|
|
289896
|
+
}
|
|
288341
289897
|
async function loadDevAdviserIgnore(cwd) {
|
|
288342
289898
|
const filePath4 = path87__namespace.resolve(cwd, ".devadviserignore");
|
|
288343
289899
|
try {
|
|
@@ -289080,7 +290636,7 @@ function buildMarkdownReport(result, username, scope) {
|
|
|
289080
290636
|
function createDevAdviserField() {
|
|
289081
290637
|
return { commandId: "dev-adviser", worker: new DevAdviserWorker(), checker: new AlwaysPassChecker20() };
|
|
289082
290638
|
}
|
|
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;
|
|
290639
|
+
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
290640
|
var init_dev_adviser_field = __esm({
|
|
289085
290641
|
"commands/dev-adviser.field.ts"() {
|
|
289086
290642
|
init_base2();
|
|
@@ -289138,6 +290694,9 @@ var init_dev_adviser_field = __esm({
|
|
|
289138
290694
|
"dependency-updates": "Deps"
|
|
289139
290695
|
};
|
|
289140
290696
|
SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
290697
|
+
MAX_EVAL_REPAIR_ATTEMPTS = 2;
|
|
290698
|
+
EXPECTED_AQ_IDS = new Set(ARCHITECT_QUALITY.subDimensions.map((s2) => s2.id));
|
|
290699
|
+
EXPECTED_SQ_IDS = new Set(SEMANTIC_QUALITY.subDimensions.map((s2) => s2.id));
|
|
289141
290700
|
FIX_CHASE_PATTERN = /\b(fix|bug|patch|hotfix|typo|修正|バグ)\b/i;
|
|
289142
290701
|
MAX_TRIAGE_PER_WEEK = 30;
|
|
289143
290702
|
MAX_TRIAGE_PATCH_LINES = 6e3;
|
|
@@ -289305,6 +290864,42 @@ Check the username and date range.`
|
|
|
289305
290864
|
"",
|
|
289306
290865
|
patchContext
|
|
289307
290866
|
].join("\n");
|
|
290867
|
+
const tryParseEval = (text) => {
|
|
290868
|
+
const rawText = String(text || "");
|
|
290869
|
+
const result2 = parseJsonFromModelText({ text: rawText, label: "dev-adviser.eval" });
|
|
290870
|
+
if (!result2.ok) return { parsed: null, rawText, issues: [`JSON parse failed: ${result2.error.slice(0, 200)}`] };
|
|
290871
|
+
const v = validateEvalRaw(result2.value, numWeeks);
|
|
290872
|
+
if (!v.ok) return { parsed: result2.value, rawText, issues: v.issues };
|
|
290873
|
+
return { parsed: result2.value, rawText, issues: [] };
|
|
290874
|
+
};
|
|
290875
|
+
const evalRawToWeekDimEvals = (raw) => {
|
|
290876
|
+
const result2 = [];
|
|
290877
|
+
for (let wi = 0; wi < numWeeks; wi++) {
|
|
290878
|
+
const wData = raw.weeks[wi];
|
|
290879
|
+
if (wData) {
|
|
290880
|
+
result2.push([
|
|
290881
|
+
{
|
|
290882
|
+
dimensionId: "architect-quality",
|
|
290883
|
+
subDimensions: sanitizeSubDimensions(wData.architectQuality?.subDimensions, ARCHITECT_QUALITY.subDimensions),
|
|
290884
|
+
overallScore: safeScore(wData.architectQuality?.overallScore ?? 3),
|
|
290885
|
+
summary: wData.architectQuality?.summary ?? ""
|
|
290886
|
+
},
|
|
290887
|
+
{
|
|
290888
|
+
dimensionId: "semantic-quality",
|
|
290889
|
+
subDimensions: sanitizeSubDimensions(wData.semanticQuality?.subDimensions, SEMANTIC_QUALITY.subDimensions),
|
|
290890
|
+
overallScore: safeScore(wData.semanticQuality?.overallScore ?? 3),
|
|
290891
|
+
summary: wData.semanticQuality?.summary ?? ""
|
|
290892
|
+
}
|
|
290893
|
+
]);
|
|
290894
|
+
} else {
|
|
290895
|
+
result2.push([
|
|
290896
|
+
neutralDimensionEval(ARCHITECT_QUALITY, "No data for this week"),
|
|
290897
|
+
neutralDimensionEval(SEMANTIC_QUALITY, "No data for this week")
|
|
290898
|
+
]);
|
|
290899
|
+
}
|
|
290900
|
+
}
|
|
290901
|
+
return result2;
|
|
290902
|
+
};
|
|
289308
290903
|
const evalResult = await withLiteSpinner(
|
|
289309
290904
|
`Evaluating Architect Quality & Semantic Quality (${selectedShas.size} commits examined)...`,
|
|
289310
290905
|
() => this.aiPromptStructured(ctx, {
|
|
@@ -289314,36 +290909,44 @@ Check the username and date range.`
|
|
|
289314
290909
|
signal: ctx.abortSignal
|
|
289315
290910
|
})
|
|
289316
290911
|
);
|
|
289317
|
-
|
|
290912
|
+
let weekDimensionEvals = [];
|
|
290913
|
+
let evalAccepted = false;
|
|
289318
290914
|
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
|
-
|
|
290915
|
+
const attempt0 = tryParseEval(evalResult.text || "");
|
|
290916
|
+
if (attempt0.issues.length === 0 && attempt0.parsed) {
|
|
290917
|
+
weekDimensionEvals = evalRawToWeekDimEvals(attempt0.parsed);
|
|
290918
|
+
evalAccepted = true;
|
|
290919
|
+
} else {
|
|
290920
|
+
let lastRawText = attempt0.rawText;
|
|
290921
|
+
let lastIssues = attempt0.issues;
|
|
290922
|
+
for (let ri = 1; ri <= MAX_EVAL_REPAIR_ATTEMPTS; ri++) {
|
|
290923
|
+
await emitLog("lite.dev-adviser.eval_repair", { attempt: ri, issues: lastIssues.slice(0, 5) });
|
|
290924
|
+
const repairPrompt = buildRepairPrompt(lastRawText, lastIssues, numWeeks);
|
|
290925
|
+
const repairResult = await withLiteSpinner(
|
|
290926
|
+
`Repairing evaluation format (attempt ${ri}/${MAX_EVAL_REPAIR_ATTEMPTS})...`,
|
|
290927
|
+
() => this.aiPromptStructured(ctx, {
|
|
290928
|
+
taskType: "dev-adviser.eval",
|
|
290929
|
+
systemPrompt: "You are a JSON repair assistant. Fix the broken JSON to match the required schema. Return valid JSON only.",
|
|
290930
|
+
prompt: repairPrompt,
|
|
290931
|
+
signal: ctx.abortSignal
|
|
290932
|
+
})
|
|
290933
|
+
);
|
|
290934
|
+
if (repairResult.status !== "ok") continue;
|
|
290935
|
+
const attemptN = tryParseEval(repairResult.text || "");
|
|
290936
|
+
if (attemptN.issues.length === 0 && attemptN.parsed) {
|
|
290937
|
+
weekDimensionEvals = evalRawToWeekDimEvals(attemptN.parsed);
|
|
290938
|
+
evalAccepted = true;
|
|
290939
|
+
await emitLog("lite.dev-adviser.eval_repair_ok", { attempt: ri });
|
|
290940
|
+
break;
|
|
289343
290941
|
}
|
|
290942
|
+
lastRawText = attemptN.rawText;
|
|
290943
|
+
lastIssues = attemptN.issues;
|
|
289344
290944
|
}
|
|
289345
290945
|
}
|
|
289346
290946
|
}
|
|
290947
|
+
if (!evalAccepted) {
|
|
290948
|
+
await emitLog("lite.dev-adviser.eval_fallback", { reason: "All eval attempts failed" });
|
|
290949
|
+
}
|
|
289347
290950
|
while (weekDimensionEvals.length < numWeeks) {
|
|
289348
290951
|
weekDimensionEvals.push([
|
|
289349
290952
|
neutralDimensionEval(ARCHITECT_QUALITY, "LLM evaluation unavailable"),
|
|
@@ -289382,7 +290985,7 @@ Check the username and date range.`
|
|
|
289382
290985
|
}
|
|
289383
290986
|
const stamp = nowStamp3();
|
|
289384
290987
|
const titleBase = sanitizeBasename(`dev-assessment-${username}`);
|
|
289385
|
-
const outDirRel = outputPath ||
|
|
290988
|
+
const outDirRel = outputPath || `.maria/desktop/dev-adviser/${stamp}_${sanitizeBasename(username)}`;
|
|
289386
290989
|
const outDirAbs = path87__namespace.resolve(ctx.cwd, outDirRel);
|
|
289387
290990
|
const prevDominants = await loadPreviousWeekDominants(ctx.cwd, username, outDirAbs);
|
|
289388
290991
|
const classificationChangelog = computeClassificationChangelog(weeks, prevDominants);
|
|
@@ -289534,6 +291137,69 @@ Check the username and date range.`
|
|
|
289534
291137
|
};
|
|
289535
291138
|
}
|
|
289536
291139
|
});
|
|
291140
|
+
async function fetchRepoTree(repo, cwd, signal) {
|
|
291141
|
+
const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/git/trees/HEAD?recursive=1`, "--jq", ".tree[] | [.path, .type, (.size // 0)] | @tsv"] });
|
|
291142
|
+
if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
|
|
291143
|
+
const entries = r2.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
291144
|
+
const [p, t2, s2] = line.split(" ");
|
|
291145
|
+
return { path: p ?? "", type: t2 ?? "blob", size: Number(s2 || 0) };
|
|
291146
|
+
});
|
|
291147
|
+
return { ok: true, entries };
|
|
291148
|
+
}
|
|
291149
|
+
function buildCompactTree(entries) {
|
|
291150
|
+
const SKIP = /^(node_modules|\.git|dist|build|\.next|__pycache__|\.cache|vendor)\//;
|
|
291151
|
+
const SKIP_FILES = /\.(lock|min\.js|min\.css|map|snap|svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$/;
|
|
291152
|
+
const filtered = entries.filter((e2) => !SKIP.test(e2.path) && !SKIP_FILES.test(e2.path));
|
|
291153
|
+
return filtered.map((e2) => `${e2.type === "tree" ? "D" : "F"} ${e2.path}`).join("\n");
|
|
291154
|
+
}
|
|
291155
|
+
async function fetchFileContents(repo, paths, cwd, signal) {
|
|
291156
|
+
const MAX_FILES = 20;
|
|
291157
|
+
const MAX_BYTES_PER_FILE = 15e3;
|
|
291158
|
+
const result = /* @__PURE__ */ new Map();
|
|
291159
|
+
const selected = paths.slice(0, MAX_FILES);
|
|
291160
|
+
for (const p of selected) {
|
|
291161
|
+
if (signal.aborted) break;
|
|
291162
|
+
const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/contents/${p}`, "--jq", ".content"] });
|
|
291163
|
+
if (r2.exitCode !== 0) continue;
|
|
291164
|
+
try {
|
|
291165
|
+
const decoded = Buffer.from(r2.stdout.trim(), "base64").toString("utf8");
|
|
291166
|
+
result.set(p, decoded.slice(0, MAX_BYTES_PER_FILE));
|
|
291167
|
+
} catch {
|
|
291168
|
+
}
|
|
291169
|
+
}
|
|
291170
|
+
return result;
|
|
291171
|
+
}
|
|
291172
|
+
async function publishTaskAsIssue(repo, task, projectTitle, cwd, signal) {
|
|
291173
|
+
const body = [
|
|
291174
|
+
`## ${task.id}: ${task.title}`,
|
|
291175
|
+
"",
|
|
291176
|
+
task.description,
|
|
291177
|
+
"",
|
|
291178
|
+
`**Assignee:** ${task.assignee}`,
|
|
291179
|
+
`**Domain:** ${task.domain}`,
|
|
291180
|
+
`**Effort:** Optimistic ${task.effort.optimistic.hours}h / Realistic ${task.effort.realistic.hours}h / Pessimistic ${task.effort.pessimistic.hours}h`,
|
|
291181
|
+
`**Deadline:** ${task.deadline}`,
|
|
291182
|
+
`**Slack:** ${task.slackDays >= 0 ? `${task.slackDays} days` : "unknown"}`,
|
|
291183
|
+
task.onCriticalPath ? "**On Critical Path:** YES" : "",
|
|
291184
|
+
task.dependencies.length > 0 ? `**Dependencies:** ${task.dependencies.join(", ")}` : "",
|
|
291185
|
+
task.relatedFiles.length > 0 ? `
|
|
291186
|
+
**Related Files:**
|
|
291187
|
+
${task.relatedFiles.map((f3) => `- \`${f3}\``).join("\n")}` : "",
|
|
291188
|
+
"",
|
|
291189
|
+
`**Rationale:** ${task.rationale}`,
|
|
291190
|
+
"",
|
|
291191
|
+
`---`,
|
|
291192
|
+
`*Generated by MARIA OS /task-distribution \u2014 ${projectTitle}*`
|
|
291193
|
+
].filter(Boolean).join("\n");
|
|
291194
|
+
const r2 = await runGhCapture({
|
|
291195
|
+
cwd,
|
|
291196
|
+
signal,
|
|
291197
|
+
args: ["issue", "create", "--repo", repo, "--title", `[${task.id}] ${task.title}`, "--body", body]
|
|
291198
|
+
});
|
|
291199
|
+
if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
|
|
291200
|
+
const url = r2.stdout.trim();
|
|
291201
|
+
return { ok: true, url };
|
|
291202
|
+
}
|
|
289537
291203
|
function renderTaskDistMarkdown(result) {
|
|
289538
291204
|
const lines = [];
|
|
289539
291205
|
lines.push(`# ${result.projectTitle}`);
|
|
@@ -289627,6 +291293,9 @@ function renderTaskDistMarkdown(result) {
|
|
|
289627
291293
|
lines.push(`- **Slack:** ${typeof t2.slackDays === "number" ? `${t2.slackDays} days` : "unknown"}`);
|
|
289628
291294
|
lines.push(`- **Deadline:** ${t2.deadline}`);
|
|
289629
291295
|
if (t2.dependencies.length > 0) lines.push(`- **Dependencies:** ${t2.dependencies.join(", ")}`);
|
|
291296
|
+
if (Array.isArray(t2.relatedFiles) && t2.relatedFiles.length > 0) {
|
|
291297
|
+
lines.push(`- **Related Files:** ${t2.relatedFiles.map((f3) => `\`${f3}\``).join(", ")}`);
|
|
291298
|
+
}
|
|
289630
291299
|
lines.push(`- **Rationale:** ${t2.rationale}`);
|
|
289631
291300
|
lines.push("");
|
|
289632
291301
|
}
|
|
@@ -289769,13 +291438,17 @@ function createTaskDistributionField() {
|
|
|
289769
291438
|
"Examples:",
|
|
289770
291439
|
' /task-distribution "Build MVP for customer portal" --members "Alice,Bob,Charlie" --apply',
|
|
289771
291440
|
' /task-distribution --in @project-brief.md --members "Dev1,Dev2,Designer,PM" --apply',
|
|
291441
|
+
' /task-distribution "Add auth module" --members "Dev1,Dev2" --repo "owner/repo" --apply',
|
|
291442
|
+
' /task-distribution "Refactor payments" --members "A,B" --repo "org/app" --publish-issues --apply',
|
|
289772
291443
|
"",
|
|
289773
291444
|
"Options:",
|
|
289774
291445
|
" --members <list>",
|
|
291446
|
+
" --repo <owner/repo>",
|
|
289775
291447
|
" --in <file>",
|
|
289776
291448
|
" --out <dir>",
|
|
289777
291449
|
" --docx",
|
|
289778
291450
|
" --pdf",
|
|
291451
|
+
" --publish-issues",
|
|
289779
291452
|
" --apply",
|
|
289780
291453
|
" --json"
|
|
289781
291454
|
].join("\n")
|
|
@@ -289789,8 +291462,10 @@ function createTaskDistributionField() {
|
|
|
289789
291462
|
const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
289790
291463
|
const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
|
|
289791
291464
|
const wantsJson = hasLiteFlag(ctx.parsed, "json");
|
|
291465
|
+
const wantsPublishIssues = hasLiteFlag(ctx.parsed, "publish-issues");
|
|
291466
|
+
const repoSlug = typeof ctx.parsed.options.repo === "string" ? String(ctx.parsed.options.repo).trim() : "";
|
|
289792
291467
|
const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
|
|
289793
|
-
const outBaseRel = (outBase || "task-distribution").replace(/\\/g, "/");
|
|
291468
|
+
const outBaseRel = (outBase || ".maria/desktop/task-distribution").replace(/\\/g, "/");
|
|
289794
291469
|
const membersRaw = typeof ctx.parsed.options.members === "string" ? String(ctx.parsed.options.members).trim() : "";
|
|
289795
291470
|
const members = membersRaw.split(",").map((m2) => m2.trim()).filter(Boolean);
|
|
289796
291471
|
if (members.length === 0) return { text: `Error: --members is required.
|
|
@@ -289812,17 +291487,77 @@ ${this.help.usage}` };
|
|
|
289812
291487
|
}
|
|
289813
291488
|
if (!goal) return { text: `Usage:
|
|
289814
291489
|
${this.help.usage}` };
|
|
291490
|
+
let repoContext = "";
|
|
291491
|
+
let repoTreeEntries = [];
|
|
291492
|
+
if (repoSlug) {
|
|
291493
|
+
const ghCheck = await ensureGhInstalled2();
|
|
291494
|
+
if (!ghCheck.ok) return { text: `Error: ${ghCheck.message}
|
|
291495
|
+
--repo requires GitHub CLI.` };
|
|
291496
|
+
const tree = await fetchRepoTree(repoSlug, ctx.cwd, ctx.abortSignal);
|
|
291497
|
+
if (!tree.ok) return { text: `Error fetching repo tree: ${tree.error}` };
|
|
291498
|
+
repoTreeEntries = tree.entries;
|
|
291499
|
+
const compactTree = buildCompactTree(repoTreeEntries);
|
|
291500
|
+
const triagePrompt = [
|
|
291501
|
+
"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.",
|
|
291502
|
+
"",
|
|
291503
|
+
`Goal: ${goal}`,
|
|
291504
|
+
additionalContext ? `
|
|
291505
|
+
Additional context:
|
|
291506
|
+
${additionalContext.slice(0, 1e4)}` : "",
|
|
291507
|
+
"",
|
|
291508
|
+
"## Repository File Tree",
|
|
291509
|
+
compactTree.slice(0, 8e4),
|
|
291510
|
+
"",
|
|
291511
|
+
"Select up to 20 files that are MOST relevant for understanding the codebase structure, architecture, and the areas this goal touches.",
|
|
291512
|
+
"Prefer: entry points, config files, core modules, type definitions, route definitions, schema files.",
|
|
291513
|
+
"Skip: tests, generated files, assets, lock files.",
|
|
291514
|
+
"",
|
|
291515
|
+
"Respond with JSON only:",
|
|
291516
|
+
"```json",
|
|
291517
|
+
'{ "selectedFiles": ["path/to/file1.ts", "path/to/file2.ts", ...],',
|
|
291518
|
+
' "rationale": "Brief explanation" }',
|
|
291519
|
+
"```"
|
|
291520
|
+
].join("\n");
|
|
291521
|
+
const triageRes = await this.aiPromptStructured(ctx, {
|
|
291522
|
+
taskType: "chat",
|
|
291523
|
+
prompt: triagePrompt,
|
|
291524
|
+
signal: ctx.abortSignal,
|
|
291525
|
+
spinnerTextOverride: "Analyzing repo structure"
|
|
291526
|
+
});
|
|
291527
|
+
const triageParsed = parseJsonFromModelText({
|
|
291528
|
+
text: String(triageRes.text || ""),
|
|
291529
|
+
label: "repo-triage"
|
|
291530
|
+
});
|
|
291531
|
+
const selectedPaths = triageParsed.ok ? triageParsed.value.selectedFiles || [] : [];
|
|
291532
|
+
const fileContents = selectedPaths.length > 0 ? await fetchFileContents(repoSlug, selectedPaths, ctx.cwd, ctx.abortSignal) : /* @__PURE__ */ new Map();
|
|
291533
|
+
const contextParts = [
|
|
291534
|
+
`
|
|
291535
|
+
## Repository: ${repoSlug}`,
|
|
291536
|
+
"",
|
|
291537
|
+
"### File Tree (filtered)",
|
|
291538
|
+
compactTree.slice(0, 4e4)
|
|
291539
|
+
];
|
|
291540
|
+
if (fileContents.size > 0) {
|
|
291541
|
+
contextParts.push("", "### Key File Contents");
|
|
291542
|
+
for (const [fp, content] of fileContents) {
|
|
291543
|
+
contextParts.push("", `#### ${fp}`, "```", content.slice(0, 1e4), "```");
|
|
291544
|
+
}
|
|
291545
|
+
}
|
|
291546
|
+
repoContext = contextParts.join("\n");
|
|
291547
|
+
}
|
|
289815
291548
|
if (!apply) {
|
|
289816
291549
|
return {
|
|
289817
291550
|
text: [
|
|
289818
291551
|
"Plan: /task-distribution",
|
|
289819
291552
|
`goal=${goal.slice(0, 120)}${goal.length > 120 ? "..." : ""}`,
|
|
289820
291553
|
`members=${members.join(", ")}`,
|
|
291554
|
+
repoSlug ? `repo=${repoSlug}` : "",
|
|
289821
291555
|
`outDir=${outBaseRel}`,
|
|
289822
291556
|
`docx=${wantsDocx ? "yes" : "no"} pdf=${wantsPdf ? "yes" : "no"}`,
|
|
291557
|
+
wantsPublishIssues ? "publish-issues=yes" : "",
|
|
289823
291558
|
"",
|
|
289824
291559
|
"Run with: --apply"
|
|
289825
|
-
].join("\n")
|
|
291560
|
+
].filter(Boolean).join("\n")
|
|
289826
291561
|
};
|
|
289827
291562
|
}
|
|
289828
291563
|
const stamp = nowStamp3();
|
|
@@ -289833,7 +291568,7 @@ ${this.help.usage}` };
|
|
|
289833
291568
|
schemaVersion: "maria_lite_task_dist_run_v1",
|
|
289834
291569
|
runId: ctx.runId,
|
|
289835
291570
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
289836
|
-
inputs: { goal: goal.slice(0, 500), members, docx: wantsDocx, pdf: wantsPdf }
|
|
291571
|
+
inputs: { goal: goal.slice(0, 500), members, repo: repoSlug || void 0, docx: wantsDocx, pdf: wantsPdf, publishIssues: wantsPublishIssues }
|
|
289837
291572
|
});
|
|
289838
291573
|
const logger = new LiteLogger({ cwd: ctx.cwd });
|
|
289839
291574
|
const emitLog = async (kind, payload) => {
|
|
@@ -289860,7 +291595,7 @@ ${this.help.usage}` };
|
|
|
289860
291595
|
`{"schemaVersion":"maria_lite_task_dist_v1","projectTitle":"...","goal":"...","members":["..."],`,
|
|
289861
291596
|
`"tasks":[{"id":"T-001","title":"...","description":"...","assignee":"member_name","domain":"...","dependencies":["T-xxx"],`,
|
|
289862
291597
|
`"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}],`,
|
|
291598
|
+
`"deadline":"YYYY-MM-DD","rationale":"...","slackDays":N,"onCriticalPath":bool,"relatedFiles":["path/to/file.ts"]}],`,
|
|
289864
291599
|
`"interfaces":[{"id":"IF-001","between":["member_A","member_B"],"contract":"...","dataFormat":"...","timing":"...","owner":"manager",`,
|
|
289865
291600
|
`"boundaryScenarios":["scenario that falls in the gap between both owners' responsibilities"]}],`,
|
|
289866
291601
|
`"criticalPath":{"path":["T-001","T-003","T-006"],"totalRealisticDays":N,"totalPessimisticDays":N,"bottleneckTaskId":"T-xxx","bottleneckReason":"..."},`,
|
|
@@ -289969,6 +291704,16 @@ ${this.help.usage}` };
|
|
|
289969
291704
|
" - vulnerabilities: at least 3 structural weaknesses with mitigations.",
|
|
289970
291705
|
" - idleGaps: per-member idle periods with gap-filling activity recommendations.",
|
|
289971
291706
|
"",
|
|
291707
|
+
repoSlug ? [
|
|
291708
|
+
"",
|
|
291709
|
+
"11. **Repository-Aware Task Distribution (when repo context is provided):**",
|
|
291710
|
+
" - Use the repository file tree and key file contents to understand the codebase architecture.",
|
|
291711
|
+
" - For each task, populate `relatedFiles` with specific file/directory paths from the repo that the task will touch or depend on.",
|
|
291712
|
+
" - Use actual file paths from the tree \u2014 do NOT invent paths that don't exist in the repo.",
|
|
291713
|
+
" - Task descriptions should reference specific modules, services, and components observed in the codebase.",
|
|
291714
|
+
" - Domain assignments should reflect the actual code organization (e.g., if the repo has `src/auth/`, `src/payments/`, use those as domains)."
|
|
291715
|
+
].join("\n") : "",
|
|
291716
|
+
"",
|
|
289972
291717
|
`Today's date: ${todayStr}`,
|
|
289973
291718
|
`Team members: ${members.join(", ")}`,
|
|
289974
291719
|
"",
|
|
@@ -289977,6 +291722,8 @@ ${this.help.usage}` };
|
|
|
289977
291722
|
additionalContext ? `
|
|
289978
291723
|
Additional context:
|
|
289979
291724
|
${additionalContext.slice(0, 6e4)}` : "",
|
|
291725
|
+
repoContext ? `
|
|
291726
|
+
${repoContext.slice(0, 8e4)}` : "",
|
|
289980
291727
|
fix ? `
|
|
289981
291728
|
|
|
289982
291729
|
${fix}
|
|
@@ -290007,6 +291754,7 @@ ${fix}
|
|
|
290007
291754
|
for (const t2 of v.tasks) {
|
|
290008
291755
|
if (typeof t2.slackDays !== "number" || !Number.isFinite(t2.slackDays)) t2.slackDays = -1;
|
|
290009
291756
|
if (typeof t2.onCriticalPath !== "boolean") t2.onCriticalPath = false;
|
|
291757
|
+
if (!Array.isArray(t2.relatedFiles)) t2.relatedFiles = [];
|
|
290010
291758
|
}
|
|
290011
291759
|
if (!Array.isArray(v.interfaces)) v.interfaces = [];
|
|
290012
291760
|
for (const iface of v.interfaces) {
|
|
@@ -290081,16 +291829,28 @@ outDir=${outDirRel}` };
|
|
|
290081
291829
|
failures.push(`PDF_EXPORT_FAILED: ${msg || "unknown"}`);
|
|
290082
291830
|
}
|
|
290083
291831
|
}
|
|
291832
|
+
const issueUrls = [];
|
|
291833
|
+
const issueFailures = [];
|
|
291834
|
+
if (wantsPublishIssues && repoSlug) {
|
|
291835
|
+
for (const task of result.tasks) {
|
|
291836
|
+
const ir = await publishTaskAsIssue(repoSlug, task, result.projectTitle, ctx.cwd, ctx.abortSignal);
|
|
291837
|
+
if (ir.ok && ir.url) issueUrls.push(`${task.id}: ${ir.url}`);
|
|
291838
|
+
else issueFailures.push(`${task.id}: ${ir.error || "unknown"}`);
|
|
291839
|
+
}
|
|
291840
|
+
await emitLog("lite.task_dist.issues", { published: issueUrls.length, failed: issueFailures.length });
|
|
291841
|
+
}
|
|
290084
291842
|
const ok = failures.length === 0 && ((wantsDocx ? Boolean(exports2.docxRel) : true) && (wantsPdf ? Boolean(exports2.pdfRel) : true));
|
|
290085
291843
|
await writeJson9(path87__namespace.join(outDirAbs, "summary.json"), {
|
|
290086
291844
|
schemaVersion: "maria_lite_task_dist_summary_v1",
|
|
290087
291845
|
ok,
|
|
290088
291846
|
outDir: outDirRel,
|
|
291847
|
+
repo: repoSlug || void 0,
|
|
290089
291848
|
taskCount: result.tasks.length,
|
|
290090
291849
|
interfaceCount: result.interfaces.length,
|
|
290091
291850
|
memberCount: result.members.length,
|
|
290092
291851
|
exports: exports2,
|
|
290093
|
-
failures
|
|
291852
|
+
failures,
|
|
291853
|
+
issues: issueUrls.length > 0 ? { published: issueUrls, failures: issueFailures } : void 0
|
|
290094
291854
|
});
|
|
290095
291855
|
const primaryArtifacts = [];
|
|
290096
291856
|
if (exports2.docxRel) primaryArtifacts.push({ path: exports2.docxRel, kind: "primary" });
|
|
@@ -290108,18 +291868,25 @@ outDir=${outDirRel}` };
|
|
|
290108
291868
|
idleGaps: result.idleGaps.length,
|
|
290109
291869
|
docx: Boolean(exports2.docxRel),
|
|
290110
291870
|
pdf: Boolean(exports2.pdfRel),
|
|
290111
|
-
failures: failures.length
|
|
291871
|
+
failures: failures.length,
|
|
291872
|
+
issuesPublished: issueUrls.length
|
|
290112
291873
|
});
|
|
290113
291874
|
const lines = [];
|
|
290114
291875
|
lines.push(ok ? "OK: /task-distribution" : "WARN: /task-distribution (partial)");
|
|
290115
291876
|
lines.push(`outDir=${outDirRel}`);
|
|
290116
291877
|
lines.push(`tasks=${result.tasks.length} members=${result.members.length} interfaces=${result.interfaces.length}`);
|
|
291878
|
+
if (repoSlug) lines.push(`repo=${repoSlug}`);
|
|
290117
291879
|
if (exports2.docxRel) lines.push(`docx=${exports2.docxRel}`);
|
|
290118
291880
|
if (exports2.pdfRel) lines.push(`pdf=${exports2.pdfRel}`);
|
|
291881
|
+
if (issueUrls.length > 0) {
|
|
291882
|
+
lines.push(`issues_published=${issueUrls.length}`);
|
|
291883
|
+
for (const u of issueUrls) lines.push(` ${u}`);
|
|
291884
|
+
}
|
|
291885
|
+
if (issueFailures.length > 0) lines.push(`issue_failures=${issueFailures.length}`);
|
|
290119
291886
|
if (!ok) lines.push(`failures=${failures.length}`);
|
|
290120
291887
|
return {
|
|
290121
291888
|
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") },
|
|
291889
|
+
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
291890
|
artifacts: [...primaryArtifacts, ...intermediateArtifacts]
|
|
290124
291891
|
};
|
|
290125
291892
|
}
|
|
@@ -290147,6 +291914,7 @@ var init_task_distribution_field = __esm({
|
|
|
290147
291914
|
init_soffice();
|
|
290148
291915
|
init_slides_helpers();
|
|
290149
291916
|
init_logger();
|
|
291917
|
+
init_gh_cli();
|
|
290150
291918
|
}
|
|
290151
291919
|
});
|
|
290152
291920
|
|
|
@@ -291460,6 +293228,168 @@ var init_phone_init_field = __esm({
|
|
|
291460
293228
|
}
|
|
291461
293229
|
});
|
|
291462
293230
|
|
|
293231
|
+
// commands/resource-manager.field.ts
|
|
293232
|
+
function formatBuildingsTable(items) {
|
|
293233
|
+
const lines = ["## Buildings", ""];
|
|
293234
|
+
lines.push("| ID | Name | Floors | Description |");
|
|
293235
|
+
lines.push("|---|---|---|---|");
|
|
293236
|
+
for (const b of items) {
|
|
293237
|
+
const floors = b.floorNames.join(", ");
|
|
293238
|
+
lines.push(`| ${b.buildingId} | ${b.buildingName} | ${floors} | ${b.description} |`);
|
|
293239
|
+
}
|
|
293240
|
+
lines.push("", `Total: ${items.length} building(s)`);
|
|
293241
|
+
return lines.join("\n");
|
|
293242
|
+
}
|
|
293243
|
+
function formatResourcesTable(items) {
|
|
293244
|
+
const lines = ["## Calendar Resources", ""];
|
|
293245
|
+
lines.push("| Name | Building | Capacity | Type | Floor | Email |");
|
|
293246
|
+
lines.push("|---|---|---|---|---|---|");
|
|
293247
|
+
for (const r2 of items) {
|
|
293248
|
+
lines.push(`| ${r2.resourceName} | ${r2.buildingId} | ${r2.capacity || ""} | ${r2.resourceType} | ${r2.floorName} | ${r2.resourceEmail} |`);
|
|
293249
|
+
}
|
|
293250
|
+
lines.push("", `Total: ${items.length} resource(s)`);
|
|
293251
|
+
return lines.join("\n");
|
|
293252
|
+
}
|
|
293253
|
+
function createResourceManagerField() {
|
|
293254
|
+
const worker = new ResourceManagerWorker();
|
|
293255
|
+
const checker = new ResourceManagerChecker();
|
|
293256
|
+
return { worker, checker };
|
|
293257
|
+
}
|
|
293258
|
+
var ResourceManagerWorker, ResourceManagerChecker;
|
|
293259
|
+
var init_resource_manager_field = __esm({
|
|
293260
|
+
"commands/resource-manager.field.ts"() {
|
|
293261
|
+
init_base2();
|
|
293262
|
+
init_google_oauth();
|
|
293263
|
+
init_admin_resources();
|
|
293264
|
+
ResourceManagerWorker = class extends LiteWorkerAgent {
|
|
293265
|
+
commandId = "resource-manager";
|
|
293266
|
+
help = {
|
|
293267
|
+
command: "/resource-manager",
|
|
293268
|
+
description: "Manage Google Workspace buildings and calendar resources.",
|
|
293269
|
+
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>]',
|
|
293270
|
+
runOnEmpty: true
|
|
293271
|
+
};
|
|
293272
|
+
steps = [
|
|
293273
|
+
{
|
|
293274
|
+
id: "resource-manager.exec",
|
|
293275
|
+
title: "Resource Manager",
|
|
293276
|
+
async run(ctx) {
|
|
293277
|
+
const mgr = new GoogleOAuthManager();
|
|
293278
|
+
const token = await mgr.getValidToken(ctx.abortSignal);
|
|
293279
|
+
if (!token) {
|
|
293280
|
+
return {
|
|
293281
|
+
text: "Resource Manager: Google account not connected. Run /google-connect first.",
|
|
293282
|
+
json: { ok: false, error: "not_connected" }
|
|
293283
|
+
};
|
|
293284
|
+
}
|
|
293285
|
+
const sub = (ctx.parsed.subcommand || ctx.parsed.args?.[0] || "buildings").toLowerCase();
|
|
293286
|
+
if (sub === "buildings" || sub === "list-buildings") {
|
|
293287
|
+
const result = await listBuildingsRaw(token, ctx.abortSignal);
|
|
293288
|
+
if (!result.ok) return { text: `Resource Manager: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293289
|
+
const items = result.data;
|
|
293290
|
+
if (items.length === 0) return { text: "No buildings found.", json: { ok: true, buildings: [] } };
|
|
293291
|
+
return { text: formatBuildingsTable(items), json: { ok: true, buildings: items } };
|
|
293292
|
+
}
|
|
293293
|
+
if (sub === "resources" || sub === "list-resources") {
|
|
293294
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
|
|
293295
|
+
const capacityRaw = String(ctx.parsed.options.capacity || "").trim();
|
|
293296
|
+
const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
|
|
293297
|
+
const result = await listResourcesRaw(token, { buildingId, capacity }, ctx.abortSignal);
|
|
293298
|
+
if (!result.ok) return { text: `Resource Manager: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293299
|
+
const items = result.data;
|
|
293300
|
+
if (items.length === 0) return { text: "No resources found.", json: { ok: true, resources: [] } };
|
|
293301
|
+
return { text: formatResourcesTable(items), json: { ok: true, resources: items } };
|
|
293302
|
+
}
|
|
293303
|
+
if (sub === "create-building") {
|
|
293304
|
+
const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
|
|
293305
|
+
if (!name) return { text: "resource-manager create-building: --name is required" };
|
|
293306
|
+
const floorsRaw = String(ctx.parsed.options.floors || "").trim();
|
|
293307
|
+
const description = String(ctx.parsed.options.description || "").trim() || void 0;
|
|
293308
|
+
const result = await createBuildingRaw(token, { name, floors: floorsRaw || void 0, description }, ctx.abortSignal);
|
|
293309
|
+
if (!result.ok) {
|
|
293310
|
+
if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293311
|
+
return { text: `Failed to create building: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293312
|
+
}
|
|
293313
|
+
const b = result.data;
|
|
293314
|
+
return { text: `Building created: ${b.buildingName} (${b.buildingId})`, json: { ok: true, building: b } };
|
|
293315
|
+
}
|
|
293316
|
+
if (sub === "create-resource") {
|
|
293317
|
+
const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
|
|
293318
|
+
if (!name) return { text: "resource-manager create-resource: --name is required" };
|
|
293319
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
|
|
293320
|
+
const capacity = Number(ctx.parsed.options.capacity || 0) || void 0;
|
|
293321
|
+
const resourceType = String(ctx.parsed.options.type || "CONFERENCE_ROOM").trim();
|
|
293322
|
+
const floorName = String(ctx.parsed.options.floor || "").trim() || void 0;
|
|
293323
|
+
const description = String(ctx.parsed.options.description || "").trim() || void 0;
|
|
293324
|
+
const resourceId = slugify2(name, "res");
|
|
293325
|
+
const result = await createResourceRaw(token, { name, resourceId, buildingId, capacity, resourceType, floorName, description }, ctx.abortSignal);
|
|
293326
|
+
if (!result.ok) {
|
|
293327
|
+
if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293328
|
+
return { text: `Failed to create resource: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293329
|
+
}
|
|
293330
|
+
const r2 = result.data;
|
|
293331
|
+
return { text: `Resource created: ${r2.resourceName} (${r2.resourceEmail})`, json: { ok: true, resource: r2 } };
|
|
293332
|
+
}
|
|
293333
|
+
if (sub === "update-building") {
|
|
293334
|
+
const id = String(ctx.parsed.options.id || "").trim();
|
|
293335
|
+
if (!id) return { text: "resource-manager update-building: --id is required" };
|
|
293336
|
+
const body = {};
|
|
293337
|
+
const name = String(ctx.parsed.options.name || "").trim();
|
|
293338
|
+
if (name) body.buildingName = name;
|
|
293339
|
+
const floorsRaw = String(ctx.parsed.options.floors || "").trim();
|
|
293340
|
+
if (floorsRaw) body.floorNames = parseFloorNames(floorsRaw);
|
|
293341
|
+
const description = String(ctx.parsed.options.description || "").trim();
|
|
293342
|
+
if (description) body.description = description;
|
|
293343
|
+
if (Object.keys(body).length === 0) return { text: "resource-manager update-building: at least one field to update is required (--name, --floors, --description)" };
|
|
293344
|
+
const result = await updateBuildingRaw(token, id, body, ctx.abortSignal);
|
|
293345
|
+
if (!result.ok) {
|
|
293346
|
+
if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293347
|
+
if (result.error === "not_found") return { text: `Building "${id}" not found.`, json: { ok: false, error: "not_found" } };
|
|
293348
|
+
return { text: `Failed to update building: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293349
|
+
}
|
|
293350
|
+
const b = result.data;
|
|
293351
|
+
return { text: `Building updated: ${b.buildingName} (${b.buildingId})`, json: { ok: true, building: b } };
|
|
293352
|
+
}
|
|
293353
|
+
if (sub === "update-resource") {
|
|
293354
|
+
const id = String(ctx.parsed.options.id || "").trim();
|
|
293355
|
+
if (!id) return { text: "resource-manager update-resource: --id is required" };
|
|
293356
|
+
const body = {};
|
|
293357
|
+
const name = String(ctx.parsed.options.name || "").trim();
|
|
293358
|
+
if (name) body.resourceName = name;
|
|
293359
|
+
const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim();
|
|
293360
|
+
if (buildingId) body.buildingId = buildingId;
|
|
293361
|
+
const capacity = Number(ctx.parsed.options.capacity || 0);
|
|
293362
|
+
if (capacity) body.capacity = capacity;
|
|
293363
|
+
const resourceType = String(ctx.parsed.options.type || "").trim();
|
|
293364
|
+
if (resourceType) body.resourceType = resourceType;
|
|
293365
|
+
const floorName = String(ctx.parsed.options.floor || "").trim();
|
|
293366
|
+
if (floorName) body.floorName = floorName;
|
|
293367
|
+
const description = String(ctx.parsed.options.description || "").trim();
|
|
293368
|
+
if (description) body.resourceDescription = description;
|
|
293369
|
+
if (Object.keys(body).length === 0) return { text: "resource-manager update-resource: at least one field to update is required" };
|
|
293370
|
+
const result = await updateResourceRaw(token, id, body, ctx.abortSignal);
|
|
293371
|
+
if (!result.ok) {
|
|
293372
|
+
if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
|
|
293373
|
+
if (result.error === "not_found") return { text: `Resource "${id}" not found.`, json: { ok: false, error: "not_found" } };
|
|
293374
|
+
return { text: `Failed to update resource: ${result.error}`, json: { ok: false, error: result.error } };
|
|
293375
|
+
}
|
|
293376
|
+
const r2 = result.data;
|
|
293377
|
+
return { text: `Resource updated: ${r2.resourceName} (${r2.resourceEmail})`, json: { ok: true, resource: r2 } };
|
|
293378
|
+
}
|
|
293379
|
+
return { text: `resource-manager: unknown subcommand "${sub}". Use buildings, resources, create-building, create-resource, update-building, or update-resource.` };
|
|
293380
|
+
}
|
|
293381
|
+
}
|
|
293382
|
+
];
|
|
293383
|
+
};
|
|
293384
|
+
ResourceManagerChecker = class extends LiteCheckerAgent {
|
|
293385
|
+
commandId = "resource-manager";
|
|
293386
|
+
async check(_ctx, _input) {
|
|
293387
|
+
return { outcome: "PASS", reasons: ["resource_manager_ok"] };
|
|
293388
|
+
}
|
|
293389
|
+
};
|
|
293390
|
+
}
|
|
293391
|
+
});
|
|
293392
|
+
|
|
291463
293393
|
// runtime/ext/ext-loader.ts
|
|
291464
293394
|
var ext_loader_exports = {};
|
|
291465
293395
|
__export(ext_loader_exports, {
|
|
@@ -291987,6 +293917,7 @@ function registerCoreLiteCommands(registry) {
|
|
|
291987
293917
|
safeRegister(registry, "gcal", createGcalField);
|
|
291988
293918
|
safeRegister(registry, "gdrive", createGdriveField);
|
|
291989
293919
|
safeRegister(registry, "gmeet", createGmeetField);
|
|
293920
|
+
safeRegister(registry, "resource-manager", createResourceManagerField);
|
|
291990
293921
|
}
|
|
291991
293922
|
function createLiteCommandProviders() {
|
|
291992
293923
|
const core2 = {
|
|
@@ -292116,6 +294047,7 @@ var init_command_providers = __esm({
|
|
|
292116
294047
|
init_phone_hp_import_field();
|
|
292117
294048
|
init_phone_deploy_field();
|
|
292118
294049
|
init_phone_init_field();
|
|
294050
|
+
init_resource_manager_field();
|
|
292119
294051
|
init_ext_loader();
|
|
292120
294052
|
init_beta_loader();
|
|
292121
294053
|
}
|