hanoman 0.1.38 → 0.1.40
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/dist/build-info.json +3 -3
- package/dist/cli.js +70 -0
- package/dist/server.js +626 -205
- package/package.json +1 -1
- package/prisma/migrations/20260815000000_terminal_workspace_user/migration.sql +4 -0
- package/prisma/migrations/20260815120000_sync_tombstone/migration.sql +14 -0
- package/prisma/schema.prisma +31 -0
- package/web/assets/{index-DeGA9N3Z.css → index-BeFLNV7l.css} +1 -1
- package/web/assets/{index-BGdNNyED.js → index-zZ7g98st.js} +1548 -1547
- package/web/index.html +2 -2
package/dist/server.js
CHANGED
|
@@ -6092,6 +6092,7 @@ var init_api = __esm({
|
|
|
6092
6092
|
// SPEC-233 · drop commit isolasi
|
|
6093
6093
|
fsBrowse: (path2) => `${API}/fs/browse${path2 ? `?path=${encodeURIComponent(path2)}` : ""}`,
|
|
6094
6094
|
terminalSessions: `${API}/terminal/sessions`,
|
|
6095
|
+
terminalWorkspace: `${API}/terminal/workspace`,
|
|
6095
6096
|
// SPEC-742 · ADR-0116 · pembersihan worktree yang masih tertunda sesudah sesi ditutup. Di bawah
|
|
6096
6097
|
// prefix /terminal supaya ikut capability `sessions` yang sudah ada (cermin sessionHistory).
|
|
6097
6098
|
terminalCleanups: `${API}/terminal/cleanups`,
|
|
@@ -6194,6 +6195,8 @@ var init_api = __esm({
|
|
|
6194
6195
|
syncNow: `${API}/sync/now`,
|
|
6195
6196
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
6196
6197
|
syncConflicts: `${API}/sync/conflicts`,
|
|
6198
|
+
// SPEC-799 · ADR-0119 · penghapusan yang menunggu jendela online (cookie-only, cermin syncNow).
|
|
6199
|
+
syncPending: `${API}/sync/pending`,
|
|
6197
6200
|
syncConflictResolve: (entity, recordId) => `${API}/sync/conflicts/${encodeURIComponent(entity)}/${encodeURIComponent(recordId)}/resolve`,
|
|
6198
6201
|
// SPEC-257 · ADR-0065 · agent token (kelola cookie-only) + katalog capability
|
|
6199
6202
|
agentTokens: `${API}/agent-tokens`,
|
|
@@ -7701,6 +7704,70 @@ var init_changelog = __esm({
|
|
|
7701
7704
|
}
|
|
7702
7705
|
});
|
|
7703
7706
|
|
|
7707
|
+
// ../shared/src/terminal-workspace.ts
|
|
7708
|
+
var zSessionId, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
|
|
7709
|
+
var init_terminal_workspace = __esm({
|
|
7710
|
+
"../shared/src/terminal-workspace.ts"() {
|
|
7711
|
+
"use strict";
|
|
7712
|
+
init_zod();
|
|
7713
|
+
zSessionId = external_exports.string().trim().min(1).max(256);
|
|
7714
|
+
zTerminalGroup = external_exports.object({
|
|
7715
|
+
id: external_exports.string().trim().min(1).max(128),
|
|
7716
|
+
name: external_exports.string().trim().min(1).max(80),
|
|
7717
|
+
layout: external_exports.object({
|
|
7718
|
+
rows: external_exports.number().int().min(1).max(12),
|
|
7719
|
+
cols: external_exports.number().int().min(1).max(12),
|
|
7720
|
+
cells: external_exports.array(zSessionId.nullable()).max(144)
|
|
7721
|
+
}).strict().superRefine((layout, ctx) => {
|
|
7722
|
+
if (layout.cells.length !== layout.rows * layout.cols) {
|
|
7723
|
+
ctx.addIssue({
|
|
7724
|
+
code: external_exports.ZodIssueCode.custom,
|
|
7725
|
+
path: ["cells"],
|
|
7726
|
+
message: "cells must match rows \xD7 cols"
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
})
|
|
7730
|
+
}).strict();
|
|
7731
|
+
zTerminalWorkspaceV1 = external_exports.object({
|
|
7732
|
+
version: external_exports.literal(1),
|
|
7733
|
+
groups: external_exports.array(zTerminalGroup).min(1).max(24)
|
|
7734
|
+
}).strict().superRefine((workspace, ctx) => {
|
|
7735
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
7736
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
7737
|
+
workspace.groups.forEach((group2, groupIndex) => {
|
|
7738
|
+
if (groupIds.has(group2.id)) {
|
|
7739
|
+
ctx.addIssue({
|
|
7740
|
+
code: external_exports.ZodIssueCode.custom,
|
|
7741
|
+
path: ["groups", groupIndex, "id"],
|
|
7742
|
+
message: "group id must be unique"
|
|
7743
|
+
});
|
|
7744
|
+
}
|
|
7745
|
+
groupIds.add(group2.id);
|
|
7746
|
+
group2.layout.cells.forEach((sessionId2, cellIndex) => {
|
|
7747
|
+
if (sessionId2 === null) return;
|
|
7748
|
+
if (sessionIds.has(sessionId2)) {
|
|
7749
|
+
ctx.addIssue({
|
|
7750
|
+
code: external_exports.ZodIssueCode.custom,
|
|
7751
|
+
path: ["groups", groupIndex, "layout", "cells", cellIndex],
|
|
7752
|
+
message: "sessionId must be unique across the workspace"
|
|
7753
|
+
});
|
|
7754
|
+
}
|
|
7755
|
+
sessionIds.add(sessionId2);
|
|
7756
|
+
});
|
|
7757
|
+
});
|
|
7758
|
+
});
|
|
7759
|
+
zTerminalWorkspaceWrite = external_exports.object({
|
|
7760
|
+
baseRevision: external_exports.number().int().min(0),
|
|
7761
|
+
workspace: zTerminalWorkspaceV1
|
|
7762
|
+
}).strict();
|
|
7763
|
+
zTerminalWorkspaceSnapshot = external_exports.object({
|
|
7764
|
+
workspace: zTerminalWorkspaceV1.nullable(),
|
|
7765
|
+
revision: external_exports.number().int().min(0),
|
|
7766
|
+
updatedAt: external_exports.string().datetime().nullable()
|
|
7767
|
+
}).strict();
|
|
7768
|
+
}
|
|
7769
|
+
});
|
|
7770
|
+
|
|
7704
7771
|
// ../shared/src/index.ts
|
|
7705
7772
|
var init_src = __esm({
|
|
7706
7773
|
"../shared/src/index.ts"() {
|
|
@@ -7732,6 +7799,7 @@ var init_src = __esm({
|
|
|
7732
7799
|
init_auto_merge();
|
|
7733
7800
|
init_changelog();
|
|
7734
7801
|
init_cron_expr();
|
|
7802
|
+
init_terminal_workspace();
|
|
7735
7803
|
}
|
|
7736
7804
|
});
|
|
7737
7805
|
|
|
@@ -10421,16 +10489,54 @@ var init_rename_project = __esm({
|
|
|
10421
10489
|
}
|
|
10422
10490
|
});
|
|
10423
10491
|
|
|
10492
|
+
// src/services/tombstone.ts
|
|
10493
|
+
async function findTombstone(entity, recordId) {
|
|
10494
|
+
const row = await prisma.syncTombstone.findUnique({ where: { entity_recordId: { entity, recordId } } });
|
|
10495
|
+
return row ? view(row) : null;
|
|
10496
|
+
}
|
|
10497
|
+
async function writeTombstone(entity, recordId, version2, data, deviceId) {
|
|
10498
|
+
const prev = await findTombstone(entity, recordId);
|
|
10499
|
+
if (prev && prev.version >= version2) return prev;
|
|
10500
|
+
const row = await prisma.syncTombstone.upsert({
|
|
10501
|
+
where: { entity_recordId: { entity, recordId } },
|
|
10502
|
+
create: { entity, recordId, version: version2, data, deviceId: deviceId ?? null },
|
|
10503
|
+
update: { version: version2, data, deviceId: deviceId ?? null, deletedAt: /* @__PURE__ */ new Date() }
|
|
10504
|
+
});
|
|
10505
|
+
return view(row);
|
|
10506
|
+
}
|
|
10507
|
+
async function clearTombstone(entity, recordId) {
|
|
10508
|
+
await prisma.syncTombstone.deleteMany({ where: { entity, recordId } });
|
|
10509
|
+
}
|
|
10510
|
+
var view;
|
|
10511
|
+
var init_tombstone = __esm({
|
|
10512
|
+
"src/services/tombstone.ts"() {
|
|
10513
|
+
"use strict";
|
|
10514
|
+
init_db();
|
|
10515
|
+
view = (r) => ({
|
|
10516
|
+
entity: r.entity,
|
|
10517
|
+
recordId: r.recordId,
|
|
10518
|
+
version: r.version,
|
|
10519
|
+
data: r.data ?? {},
|
|
10520
|
+
deletedAt: r.deletedAt,
|
|
10521
|
+
deviceId: r.deviceId
|
|
10522
|
+
});
|
|
10523
|
+
}
|
|
10524
|
+
});
|
|
10525
|
+
|
|
10424
10526
|
// src/services/sync.ts
|
|
10425
10527
|
var sync_exports = {};
|
|
10426
10528
|
__export(sync_exports, {
|
|
10529
|
+
PARENTS: () => PARENTS,
|
|
10427
10530
|
SYNCED: () => SYNCED,
|
|
10428
10531
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
10429
10532
|
__FIELDS: () => __FIELDS,
|
|
10430
10533
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
10431
10534
|
applyPush: () => applyPush,
|
|
10432
10535
|
backfillFeed: () => backfillFeed,
|
|
10536
|
+
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
10537
|
+
deleteRow: () => deleteRow,
|
|
10433
10538
|
isEntity: () => isEntity,
|
|
10539
|
+
publishDelete: () => publishDelete,
|
|
10434
10540
|
publishLocal: () => publishLocal,
|
|
10435
10541
|
pull: () => pull,
|
|
10436
10542
|
setAcceptedHook: () => setAcceptedHook,
|
|
@@ -10495,10 +10601,24 @@ async function snapshot(entity, id) {
|
|
|
10495
10601
|
for (const f of FIELDS[entity]) data[f] = jsonSafe(row[f]);
|
|
10496
10602
|
return { version: Number(row.version), data };
|
|
10497
10603
|
}
|
|
10498
|
-
async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
10604
|
+
async function applyPush(entity, id, baseVersion, data, deviceId, op = "upsert") {
|
|
10499
10605
|
validateSyncData(entity, data, { allowProjectRename: true });
|
|
10606
|
+
if (op === "delete") {
|
|
10607
|
+
const already = await findTombstone(entity, id);
|
|
10608
|
+
if (already) return { ok: true, version: already.version };
|
|
10609
|
+
const snap2 = await snapshot(entity, id);
|
|
10610
|
+
const version2 = (snap2?.version ?? baseVersion) + 1;
|
|
10611
|
+
if (snap2) await DELEGATE[entity].delete({ where: { id } });
|
|
10612
|
+
await writeTombstone(entity, id, version2, snap2?.data ?? data, deviceId);
|
|
10613
|
+
await publishDelete(entity, id);
|
|
10614
|
+
return { ok: true, version: version2 };
|
|
10615
|
+
}
|
|
10500
10616
|
if (entity === "project" && typeof data.renamedFrom === "string" && data.renamedFrom && data.renamedFrom !== id) {
|
|
10501
10617
|
const oldId = data.renamedFrom;
|
|
10618
|
+
const destTomb = await findTombstone("project", id);
|
|
10619
|
+
if (destTomb) {
|
|
10620
|
+
return { ok: false, conflict: true, deleted: true, deletedVersion: destTomb.version, server: null };
|
|
10621
|
+
}
|
|
10502
10622
|
const already = await DELEGATE.project.findUnique({ where: { id }, select: { version: true } });
|
|
10503
10623
|
if (already) return { ok: true, version: Number(already.version) };
|
|
10504
10624
|
const old = await DELEGATE.project.findUnique({ where: { id: oldId }, select: { version: true } });
|
|
@@ -10512,17 +10632,24 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10512
10632
|
const snap2 = await snapshot("project", id);
|
|
10513
10633
|
const logData = { ...snap2?.data ?? {}, renamedFrom: oldId };
|
|
10514
10634
|
const log3 = await prisma.syncLog.create({
|
|
10515
|
-
data: { entity: "project", recordId: id, version: newVersion2, data: logData, deviceId: deviceId ?? null }
|
|
10635
|
+
data: { entity: "project", recordId: id, version: newVersion2, op: "upsert", data: logData, deviceId: deviceId ?? null }
|
|
10516
10636
|
});
|
|
10517
|
-
onAccepted?.({ entity: "project", recordId: id, version: newVersion2, data: logData, seq: String(log3.seq) });
|
|
10637
|
+
onAccepted?.({ entity: "project", recordId: id, version: newVersion2, op: "upsert", data: logData, seq: String(log3.seq) });
|
|
10518
10638
|
return { ok: true, version: newVersion2 };
|
|
10519
10639
|
}
|
|
10520
10640
|
}
|
|
10641
|
+
const tomb = await findTombstone(entity, id);
|
|
10521
10642
|
const existing = await DELEGATE[entity].findUnique({ where: { id }, select: { version: true } });
|
|
10522
|
-
|
|
10523
|
-
|
|
10643
|
+
const currentVersion = existing ? Number(existing.version) : tomb ? tomb.version : null;
|
|
10644
|
+
if (currentVersion !== null && currentVersion !== baseVersion) {
|
|
10645
|
+
return {
|
|
10646
|
+
ok: false,
|
|
10647
|
+
conflict: true,
|
|
10648
|
+
server: await snapshot(entity, id),
|
|
10649
|
+
...tomb && !existing ? { deleted: true, deletedVersion: tomb.version } : {}
|
|
10650
|
+
};
|
|
10524
10651
|
}
|
|
10525
|
-
const newVersion =
|
|
10652
|
+
const newVersion = (currentVersion ?? 0) + 1;
|
|
10526
10653
|
const writeData = coerce2(entity, data);
|
|
10527
10654
|
const stamp2 = writeData.updatedAt ?? /* @__PURE__ */ new Date();
|
|
10528
10655
|
await DELEGATE[entity].upsert({
|
|
@@ -10530,11 +10657,12 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10530
10657
|
create: { id, ...writeData, version: newVersion, updatedAt: stamp2 },
|
|
10531
10658
|
update: { ...writeData, version: newVersion, updatedAt: stamp2 }
|
|
10532
10659
|
});
|
|
10660
|
+
if (tomb) await clearTombstone(entity, id);
|
|
10533
10661
|
const snap = await snapshot(entity, id);
|
|
10534
10662
|
const log2 = await prisma.syncLog.create({
|
|
10535
|
-
data: { entity, recordId: id, version: newVersion, data: snap?.data ?? {}, deviceId: deviceId ?? null }
|
|
10663
|
+
data: { entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, deviceId: deviceId ?? null }
|
|
10536
10664
|
});
|
|
10537
|
-
onAccepted?.({ entity, recordId: id, version: newVersion, data: snap?.data ?? {}, seq: String(log2.seq) });
|
|
10665
|
+
onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, seq: String(log2.seq) });
|
|
10538
10666
|
return { ok: true, version: newVersion };
|
|
10539
10667
|
}
|
|
10540
10668
|
async function pull(sinceCursor, limit = 500) {
|
|
@@ -10547,7 +10675,13 @@ async function pull(sinceCursor, limit = 500) {
|
|
|
10547
10675
|
const cursor = rows.length ? String(rows[rows.length - 1].seq) : sinceCursor || "0";
|
|
10548
10676
|
return {
|
|
10549
10677
|
cursor,
|
|
10550
|
-
records: rows.map((r) => ({
|
|
10678
|
+
records: rows.map((r) => ({
|
|
10679
|
+
entity: r.entity,
|
|
10680
|
+
recordId: r.recordId,
|
|
10681
|
+
version: r.version,
|
|
10682
|
+
op: r.op === "delete" ? "delete" : "upsert",
|
|
10683
|
+
data: r.data
|
|
10684
|
+
}))
|
|
10551
10685
|
};
|
|
10552
10686
|
}
|
|
10553
10687
|
async function publishLocal(entity, id) {
|
|
@@ -10556,9 +10690,38 @@ async function publishLocal(entity, id) {
|
|
|
10556
10690
|
const newVersion = snap.version + 1;
|
|
10557
10691
|
await DELEGATE[entity].update({ where: { id }, data: { version: newVersion } });
|
|
10558
10692
|
const log2 = await prisma.syncLog.create({
|
|
10559
|
-
data: { entity, recordId: id, version: newVersion, data: snap.data ?? {}, deviceId: null }
|
|
10693
|
+
data: { entity, recordId: id, version: newVersion, op: "upsert", data: snap.data ?? {}, deviceId: null }
|
|
10560
10694
|
});
|
|
10561
|
-
onAccepted?.({ entity, recordId: id, version: newVersion, data: snap.data ?? {}, seq: String(log2.seq) });
|
|
10695
|
+
onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap.data ?? {}, seq: String(log2.seq) });
|
|
10696
|
+
}
|
|
10697
|
+
async function deleteRow(entity, id) {
|
|
10698
|
+
await DELEGATE[entity].delete({ where: { id } });
|
|
10699
|
+
}
|
|
10700
|
+
async function publishDelete(entity, id) {
|
|
10701
|
+
const tomb = await findTombstone(entity, id);
|
|
10702
|
+
if (!tomb) return;
|
|
10703
|
+
const log2 = await prisma.syncLog.create({
|
|
10704
|
+
data: {
|
|
10705
|
+
entity,
|
|
10706
|
+
recordId: id,
|
|
10707
|
+
version: tomb.version,
|
|
10708
|
+
op: "delete",
|
|
10709
|
+
data: tomb.data,
|
|
10710
|
+
deviceId: tomb.deviceId
|
|
10711
|
+
}
|
|
10712
|
+
});
|
|
10713
|
+
onAccepted?.({ entity, recordId: id, version: tomb.version, op: "delete", data: tomb.data, seq: String(log2.seq) });
|
|
10714
|
+
}
|
|
10715
|
+
async function consumeTombstoneOnRecreate(entity, id) {
|
|
10716
|
+
const tomb = await findTombstone(entity, id);
|
|
10717
|
+
if (!tomb) return false;
|
|
10718
|
+
const row = await DELEGATE[entity].findUnique({ where: { id }, select: { version: true } });
|
|
10719
|
+
if (!row) return false;
|
|
10720
|
+
await clearTombstone(entity, id);
|
|
10721
|
+
if (Number(row.version) < tomb.version) {
|
|
10722
|
+
await DELEGATE[entity].update({ where: { id }, data: { version: tomb.version } });
|
|
10723
|
+
}
|
|
10724
|
+
return true;
|
|
10562
10725
|
}
|
|
10563
10726
|
async function backfillFeed() {
|
|
10564
10727
|
let published = 0;
|
|
@@ -10574,6 +10737,16 @@ async function backfillFeed() {
|
|
|
10574
10737
|
published++;
|
|
10575
10738
|
}
|
|
10576
10739
|
}
|
|
10740
|
+
for (const t of await prisma.syncTombstone.findMany({ select: { entity: true, recordId: true, version: true } })) {
|
|
10741
|
+
if (!isEntity(t.entity)) continue;
|
|
10742
|
+
const has = await prisma.syncLog.findFirst({
|
|
10743
|
+
where: { entity: t.entity, recordId: t.recordId, version: t.version, op: "delete" },
|
|
10744
|
+
select: { seq: true }
|
|
10745
|
+
});
|
|
10746
|
+
if (has) continue;
|
|
10747
|
+
await publishDelete(t.entity, t.recordId);
|
|
10748
|
+
published++;
|
|
10749
|
+
}
|
|
10577
10750
|
return published;
|
|
10578
10751
|
}
|
|
10579
10752
|
async function upsertLocal(entity, id, version2, data) {
|
|
@@ -10601,12 +10774,13 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
10601
10774
|
function setAcceptedHook(hook) {
|
|
10602
10775
|
onAccepted = hook;
|
|
10603
10776
|
}
|
|
10604
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10777
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10605
10778
|
var init_sync = __esm({
|
|
10606
10779
|
"src/services/sync.ts"() {
|
|
10607
10780
|
"use strict";
|
|
10608
10781
|
init_db();
|
|
10609
10782
|
init_rename_project();
|
|
10783
|
+
init_tombstone();
|
|
10610
10784
|
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
|
|
10611
10785
|
DELEGATE = {
|
|
10612
10786
|
project: prisma.project,
|
|
@@ -10680,6 +10854,13 @@ var init_sync = __esm({
|
|
|
10680
10854
|
customAgent: ["createdAt", "updatedAt"],
|
|
10681
10855
|
githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
|
|
10682
10856
|
};
|
|
10857
|
+
PARENTS = {
|
|
10858
|
+
spec: [{ field: "projectId", entity: "project" }],
|
|
10859
|
+
ticket: [{ field: "projectId", entity: "project" }],
|
|
10860
|
+
ticketAttachment: [{ field: "ticketId", entity: "ticket" }],
|
|
10861
|
+
customAgent: [{ field: "projectId", entity: "project" }],
|
|
10862
|
+
githubIssue: [{ field: "projectId", entity: "project" }]
|
|
10863
|
+
};
|
|
10683
10864
|
__FIELDS = FIELDS;
|
|
10684
10865
|
__DATE_FIELDS = DATE_FIELDS;
|
|
10685
10866
|
NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -10822,6 +11003,134 @@ var init_codex_trust = __esm({
|
|
|
10822
11003
|
}
|
|
10823
11004
|
});
|
|
10824
11005
|
|
|
11006
|
+
// src/services/notifications.ts
|
|
11007
|
+
async function recordDrift(vpsId, vpsName, drift, snapshotId) {
|
|
11008
|
+
if (drift.length === 0) return;
|
|
11009
|
+
const ids = drift.map((d) => d.itemId);
|
|
11010
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
|
|
11011
|
+
const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
|
|
11012
|
+
await prisma.notification.create({
|
|
11013
|
+
data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
|
|
11014
|
+
}).catch(() => {
|
|
11015
|
+
});
|
|
11016
|
+
}
|
|
11017
|
+
async function recordCompletion(specId, title, projectId) {
|
|
11018
|
+
await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
11019
|
+
});
|
|
11020
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11021
|
+
await prisma.notification.create({
|
|
11022
|
+
data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
11023
|
+
}).catch(() => {
|
|
11024
|
+
});
|
|
11025
|
+
}
|
|
11026
|
+
async function recordSyncDelete(entity, recordId, version2, title) {
|
|
11027
|
+
await prisma.notification.create({
|
|
11028
|
+
data: { type: "sync", key: `sync-delete:${entity}:${recordId}:${version2}`, title, projectId: null }
|
|
11029
|
+
}).catch(() => {
|
|
11030
|
+
});
|
|
11031
|
+
}
|
|
11032
|
+
async function recordFailure(specId, title, projectId, reason) {
|
|
11033
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11034
|
+
await prisma.notification.create({
|
|
11035
|
+
data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
|
|
11036
|
+
}).catch(() => {
|
|
11037
|
+
});
|
|
11038
|
+
}
|
|
11039
|
+
async function recordAutoMerge(specId, projectId, title) {
|
|
11040
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11041
|
+
await prisma.notification.create({
|
|
11042
|
+
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
11043
|
+
}).catch(() => {
|
|
11044
|
+
});
|
|
11045
|
+
}
|
|
11046
|
+
async function recordCleanupFailure(sessionId2, projectId, entry, reason) {
|
|
11047
|
+
await prisma.notification.create({
|
|
11048
|
+
data: {
|
|
11049
|
+
type: "cleanup",
|
|
11050
|
+
key: `cleanup:${entry}`,
|
|
11051
|
+
sessionId: sessionId2,
|
|
11052
|
+
projectId,
|
|
11053
|
+
title: `Worktree sesi ${sessionId2} gagal dibersihkan \u2014 ${reason}`
|
|
11054
|
+
}
|
|
11055
|
+
}).catch(() => {
|
|
11056
|
+
});
|
|
11057
|
+
}
|
|
11058
|
+
async function recordSourceChange(specId, projectId, title, from, to, seq) {
|
|
11059
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11060
|
+
await prisma.notification.create({
|
|
11061
|
+
data: {
|
|
11062
|
+
type: "spec-source",
|
|
11063
|
+
key: `source:${specId}:${seq}`,
|
|
11064
|
+
specId,
|
|
11065
|
+
sessionId: sessionId2,
|
|
11066
|
+
title: `${specId} \xB7 type ${from} \u2192 ${to} \u2014 ${title}`,
|
|
11067
|
+
projectId
|
|
11068
|
+
}
|
|
11069
|
+
}).catch(() => {
|
|
11070
|
+
});
|
|
11071
|
+
}
|
|
11072
|
+
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
11073
|
+
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
11074
|
+
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
11075
|
+
await prisma.notification.create({
|
|
11076
|
+
data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
|
|
11077
|
+
}).catch(() => {
|
|
11078
|
+
});
|
|
11079
|
+
}
|
|
11080
|
+
async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
|
|
11081
|
+
await prisma.notification.create({
|
|
11082
|
+
data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
|
|
11083
|
+
}).catch(() => {
|
|
11084
|
+
});
|
|
11085
|
+
}
|
|
11086
|
+
async function recordCronRun(cronId, cronName, projectId, dueAt, status, note) {
|
|
11087
|
+
const verb = status === "launched" ? "berjalan" : status === "skipped" ? "dilewati" : "gagal";
|
|
11088
|
+
const title = `Cron "${cronName}" ${verb}${note ? ` \u2014 ${note}` : ""}`;
|
|
11089
|
+
await prisma.notification.create({
|
|
11090
|
+
data: { type: "cron", key: `cron:${cronId}:${dueAt.toISOString()}`, title, projectId }
|
|
11091
|
+
}).catch(() => {
|
|
11092
|
+
});
|
|
11093
|
+
}
|
|
11094
|
+
async function scanDecisions(read2 = liveDecisions) {
|
|
11095
|
+
const next = /* @__PURE__ */ new Set();
|
|
11096
|
+
const fresh = [];
|
|
11097
|
+
for (const s2 of read2()) {
|
|
11098
|
+
if (!markerFilled(s2.decisionFile)) continue;
|
|
11099
|
+
next.add(s2.id);
|
|
11100
|
+
if (!awaiting.has(s2.id)) fresh.push(s2);
|
|
11101
|
+
}
|
|
11102
|
+
awaiting = next;
|
|
11103
|
+
for (const s2 of fresh) {
|
|
11104
|
+
const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
|
|
11105
|
+
await prisma.notification.create({
|
|
11106
|
+
data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
|
|
11107
|
+
});
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
async function notificationsFeed(p = {}) {
|
|
11111
|
+
await scanDecisions();
|
|
11112
|
+
const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
|
|
11113
|
+
const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
|
|
11114
|
+
const total = await prisma.notification.count();
|
|
11115
|
+
const items = await prisma.notification.findMany({
|
|
11116
|
+
orderBy: { createdAt: "desc" },
|
|
11117
|
+
skip: (page - 1) * pageSize,
|
|
11118
|
+
take: pageSize
|
|
11119
|
+
});
|
|
11120
|
+
const unread = await prisma.notification.count({ where: { readAt: null } });
|
|
11121
|
+
return { items, unread, total, page, pageSize };
|
|
11122
|
+
}
|
|
11123
|
+
var awaiting, DEFAULT_FEED_TAKE;
|
|
11124
|
+
var init_notifications = __esm({
|
|
11125
|
+
"src/services/notifications.ts"() {
|
|
11126
|
+
"use strict";
|
|
11127
|
+
init_db();
|
|
11128
|
+
init_pty();
|
|
11129
|
+
awaiting = /* @__PURE__ */ new Set();
|
|
11130
|
+
DEFAULT_FEED_TAKE = 50;
|
|
11131
|
+
}
|
|
11132
|
+
});
|
|
11133
|
+
|
|
10825
11134
|
// src/services/agent-token.ts
|
|
10826
11135
|
import { randomBytes as randomBytes2, createHash, timingSafeEqual } from "node:crypto";
|
|
10827
11136
|
function toAgentTokenView(t) {
|
|
@@ -16105,7 +16414,14 @@ function validateIncomingRecord(input) {
|
|
|
16105
16414
|
if (!row.data || typeof row.data !== "object" || Array.isArray(row.data)) throw new Error("sync data invalid");
|
|
16106
16415
|
if (Buffer.byteLength(JSON.stringify(input)) > MAX_SYNC_RECORD_BYTES) throw new Error("sync record terlalu besar");
|
|
16107
16416
|
validateSyncData(row.entity, row.data, { allowProjectRename: true });
|
|
16108
|
-
|
|
16417
|
+
const op = row.op === void 0 || row.op === null || row.op === "upsert" ? "upsert" : row.op === "delete" ? "delete" : null;
|
|
16418
|
+
return {
|
|
16419
|
+
entity: row.entity,
|
|
16420
|
+
recordId: row.recordId,
|
|
16421
|
+
version: Number(row.version),
|
|
16422
|
+
data: row.data,
|
|
16423
|
+
op
|
|
16424
|
+
};
|
|
16109
16425
|
}
|
|
16110
16426
|
async function getCursor() {
|
|
16111
16427
|
const s2 = await prisma.syncState.findUnique({ where: { id: 1 } });
|
|
@@ -16114,15 +16430,50 @@ async function getCursor() {
|
|
|
16114
16430
|
async function setCursor(cursor) {
|
|
16115
16431
|
await prisma.syncState.upsert({ where: { id: 1 }, create: { id: 1, cursor }, update: { cursor } });
|
|
16116
16432
|
}
|
|
16117
|
-
async function applyRemote(entity, recordId, version2, data) {
|
|
16118
|
-
if (!isEntity(entity)) return;
|
|
16433
|
+
async function applyRemote(entity, recordId, version2, data, op = "upsert") {
|
|
16434
|
+
if (!isEntity(entity)) return "dropped";
|
|
16435
|
+
if (op === "delete") {
|
|
16436
|
+
await applyRemoteDelete(entity, recordId, version2, data);
|
|
16437
|
+
return "applied";
|
|
16438
|
+
}
|
|
16439
|
+
const tomb = await findTombstone(entity, recordId);
|
|
16440
|
+
if (tomb) {
|
|
16441
|
+
if (version2 <= tomb.version) return "dropped";
|
|
16442
|
+
await clearTombstone(entity, recordId);
|
|
16443
|
+
}
|
|
16444
|
+
if (await parentTombstoned(entity, data)) return "dropped";
|
|
16119
16445
|
await upsertLocal(entity, recordId, version2, data);
|
|
16446
|
+
return "applied";
|
|
16447
|
+
}
|
|
16448
|
+
async function applyRemoteDelete(entity, recordId, version2, data) {
|
|
16449
|
+
const existing = await snapshot(entity, recordId);
|
|
16450
|
+
await writeTombstone(entity, recordId, version2, existing?.data ?? data);
|
|
16451
|
+
if (existing) await deleteRow(entity, recordId);
|
|
16452
|
+
const pending2 = await prisma.syncOutbox.findFirst({ where: { entity, recordId } });
|
|
16453
|
+
if (!pending2) return;
|
|
16454
|
+
await clearOutbox(entity, recordId);
|
|
16455
|
+
if (existing) {
|
|
16456
|
+
await recordSyncDelete(
|
|
16457
|
+
entity,
|
|
16458
|
+
recordId,
|
|
16459
|
+
version2,
|
|
16460
|
+
`Dihapus di peer: ${entity} ${recordId} \u2014 suntingan lokal yang belum tersinkron dibuang`
|
|
16461
|
+
);
|
|
16462
|
+
}
|
|
16463
|
+
}
|
|
16464
|
+
async function parentTombstoned(entity, data) {
|
|
16465
|
+
for (const p of PARENTS[entity] ?? []) {
|
|
16466
|
+
const v = data[p.field];
|
|
16467
|
+
if (typeof v !== "string" || !v) continue;
|
|
16468
|
+
if (await findTombstone(p.entity, v)) return true;
|
|
16469
|
+
}
|
|
16470
|
+
return false;
|
|
16120
16471
|
}
|
|
16121
16472
|
async function applyFeedFrame(msg) {
|
|
16122
16473
|
if (!msg.entity || !msg.recordId) return true;
|
|
16123
16474
|
try {
|
|
16124
16475
|
const record2 = validateIncomingRecord({ ...msg, version: Number(msg.version ?? 0), data: msg.data ?? {} });
|
|
16125
|
-
await applyRemote(record2.entity, record2.recordId, record2.version, record2.data);
|
|
16476
|
+
if (record2.op) await applyRemote(record2.entity, record2.recordId, record2.version, record2.data, record2.op);
|
|
16126
16477
|
} catch {
|
|
16127
16478
|
feedHole = true;
|
|
16128
16479
|
return false;
|
|
@@ -16131,7 +16482,7 @@ async function applyFeedFrame(msg) {
|
|
|
16131
16482
|
return true;
|
|
16132
16483
|
}
|
|
16133
16484
|
async function syncOnce(transport) {
|
|
16134
|
-
let pulled = 0, pushed = 0, conflicts = 0;
|
|
16485
|
+
let pulled = 0, pushed = 0, conflicts = 0, deleted = 0, dropped = 0;
|
|
16135
16486
|
const cursor = await getCursor();
|
|
16136
16487
|
const outbox = await listOutbox();
|
|
16137
16488
|
const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
|
|
@@ -16150,7 +16501,11 @@ async function syncOnce(transport) {
|
|
|
16150
16501
|
const deferred = [];
|
|
16151
16502
|
for (const rec of records) {
|
|
16152
16503
|
if (!isEntity(rec.entity)) continue;
|
|
16153
|
-
if (
|
|
16504
|
+
if (!rec.op) {
|
|
16505
|
+
dropped++;
|
|
16506
|
+
continue;
|
|
16507
|
+
}
|
|
16508
|
+
if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
|
|
16154
16509
|
const local = await snapshot(rec.entity, rec.recordId);
|
|
16155
16510
|
if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
|
|
16156
16511
|
await markConflict(
|
|
@@ -16163,8 +16518,10 @@ async function syncOnce(transport) {
|
|
|
16163
16518
|
continue;
|
|
16164
16519
|
}
|
|
16165
16520
|
try {
|
|
16166
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16167
|
-
|
|
16521
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
|
|
16522
|
+
if (r === "dropped") dropped++;
|
|
16523
|
+
else if (rec.op === "delete") deleted++;
|
|
16524
|
+
else pulled++;
|
|
16168
16525
|
} catch {
|
|
16169
16526
|
deferred.push(rec);
|
|
16170
16527
|
}
|
|
@@ -16174,15 +16531,18 @@ async function syncOnce(transport) {
|
|
|
16174
16531
|
const still = [];
|
|
16175
16532
|
for (const rec of rest) {
|
|
16176
16533
|
try {
|
|
16177
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16178
|
-
|
|
16534
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op ?? "upsert");
|
|
16535
|
+
if (r === "dropped") dropped++;
|
|
16536
|
+
else if (rec.op === "delete") deleted++;
|
|
16537
|
+
else pulled++;
|
|
16179
16538
|
} catch {
|
|
16180
16539
|
still.push(rec);
|
|
16181
16540
|
}
|
|
16182
16541
|
}
|
|
16183
16542
|
if (still.length === rest.length) {
|
|
16184
16543
|
for (const rec of still) {
|
|
16185
|
-
console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan
|
|
16544
|
+
console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
|
|
16545
|
+
dropped++;
|
|
16186
16546
|
}
|
|
16187
16547
|
break;
|
|
16188
16548
|
}
|
|
@@ -16216,7 +16576,24 @@ async function syncOnce(transport) {
|
|
|
16216
16576
|
}
|
|
16217
16577
|
const snap = await snapshot(item.entity, item.recordId);
|
|
16218
16578
|
if (!snap) {
|
|
16219
|
-
await
|
|
16579
|
+
const tomb = await findTombstone(item.entity, item.recordId);
|
|
16580
|
+
if (!tomb) {
|
|
16581
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16582
|
+
continue;
|
|
16583
|
+
}
|
|
16584
|
+
const res2 = await transport("POST", "/api/sync/push", {
|
|
16585
|
+
records: [{
|
|
16586
|
+
entity: item.entity,
|
|
16587
|
+
id: item.recordId,
|
|
16588
|
+
baseVersion: Math.max(tomb.version - 1, 0),
|
|
16589
|
+
op: "delete",
|
|
16590
|
+
data: tomb.data
|
|
16591
|
+
}]
|
|
16592
|
+
});
|
|
16593
|
+
if (res2.body?.results?.[0]?.ok) {
|
|
16594
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16595
|
+
pushed++;
|
|
16596
|
+
}
|
|
16220
16597
|
continue;
|
|
16221
16598
|
}
|
|
16222
16599
|
const res = await transport("POST", "/api/sync/push", {
|
|
@@ -16232,6 +16609,18 @@ async function syncOnce(transport) {
|
|
|
16232
16609
|
await clearOutbox(item.entity, item.recordId);
|
|
16233
16610
|
pushed++;
|
|
16234
16611
|
} else if (r?.conflict) {
|
|
16612
|
+
if (r.deleted) {
|
|
16613
|
+
await applyRemote(
|
|
16614
|
+
item.entity,
|
|
16615
|
+
item.recordId,
|
|
16616
|
+
Number(r.deletedVersion ?? snap.version + 1),
|
|
16617
|
+
snap.data,
|
|
16618
|
+
"delete"
|
|
16619
|
+
);
|
|
16620
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16621
|
+
deleted++;
|
|
16622
|
+
continue;
|
|
16623
|
+
}
|
|
16235
16624
|
const server = r.server;
|
|
16236
16625
|
if (server && JSON.stringify(server.data) !== JSON.stringify(snap.data)) {
|
|
16237
16626
|
await markConflict(
|
|
@@ -16246,7 +16635,7 @@ async function syncOnce(transport) {
|
|
|
16246
16635
|
}
|
|
16247
16636
|
}
|
|
16248
16637
|
}
|
|
16249
|
-
return { pulled, pushed, conflicts };
|
|
16638
|
+
return { pulled, pushed, conflicts, deleted, dropped };
|
|
16250
16639
|
}
|
|
16251
16640
|
function fetchTransport(base2, token) {
|
|
16252
16641
|
return async (method, path2, body) => {
|
|
@@ -16277,13 +16666,15 @@ async function syncNow(opts) {
|
|
|
16277
16666
|
const transport = fetchTransport(base2, token);
|
|
16278
16667
|
if (!opts?.full) return syncOnce(transport);
|
|
16279
16668
|
await setCursor("0");
|
|
16280
|
-
const total = { pulled: 0, pushed: 0, conflicts: 0 };
|
|
16669
|
+
const total = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
|
|
16281
16670
|
let seen = "0";
|
|
16282
16671
|
for (let page = 0; page < FULL_PULL_MAX_PAGES; page++) {
|
|
16283
16672
|
const s2 = await syncOnce(transport);
|
|
16284
16673
|
total.pulled += s2.pulled;
|
|
16285
16674
|
total.pushed += s2.pushed;
|
|
16286
16675
|
total.conflicts += s2.conflicts;
|
|
16676
|
+
total.deleted += s2.deleted;
|
|
16677
|
+
total.dropped += s2.dropped;
|
|
16287
16678
|
const now = await getCursor();
|
|
16288
16679
|
if (now === seen) break;
|
|
16289
16680
|
seen = now;
|
|
@@ -16366,6 +16757,8 @@ var init_sync_client = __esm({
|
|
|
16366
16757
|
"use strict";
|
|
16367
16758
|
init_db();
|
|
16368
16759
|
init_sync();
|
|
16760
|
+
init_tombstone();
|
|
16761
|
+
init_notifications();
|
|
16369
16762
|
init_conflicts();
|
|
16370
16763
|
init_outbox();
|
|
16371
16764
|
init_rename_project();
|
|
@@ -24738,11 +25131,42 @@ init_sync();
|
|
|
24738
25131
|
async function notifySynced(entity, id) {
|
|
24739
25132
|
try {
|
|
24740
25133
|
if (!isEntity(entity)) return;
|
|
25134
|
+
await consumeTombstoneOnRecreate(entity, id);
|
|
24741
25135
|
if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
|
|
24742
25136
|
else await publishLocal(entity, id);
|
|
24743
25137
|
} catch {
|
|
24744
25138
|
}
|
|
24745
25139
|
}
|
|
25140
|
+
async function notifyDeleted(entity, id) {
|
|
25141
|
+
try {
|
|
25142
|
+
if (!isEntity(entity)) return;
|
|
25143
|
+
if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
|
|
25144
|
+
else await publishDelete(entity, id);
|
|
25145
|
+
} catch {
|
|
25146
|
+
}
|
|
25147
|
+
}
|
|
25148
|
+
|
|
25149
|
+
// src/services/sync-delete.ts
|
|
25150
|
+
init_sync();
|
|
25151
|
+
init_tombstone();
|
|
25152
|
+
init_outbox();
|
|
25153
|
+
async function deleteSynced(entity, id, deviceId) {
|
|
25154
|
+
const snap = await snapshot(entity, id);
|
|
25155
|
+
if (!snap) return false;
|
|
25156
|
+
await deleteRow(entity, id);
|
|
25157
|
+
await writeTombstone(entity, id, snap.version + 1, snap.data, deviceId);
|
|
25158
|
+
await notifyDeleted(entity, id);
|
|
25159
|
+
return true;
|
|
25160
|
+
}
|
|
25161
|
+
async function listPendingDeletes() {
|
|
25162
|
+
const out3 = [];
|
|
25163
|
+
for (const item of await listOutbox()) {
|
|
25164
|
+
const tomb = await findTombstone(item.entity, item.recordId);
|
|
25165
|
+
if (!tomb) continue;
|
|
25166
|
+
out3.push({ entity: item.entity, recordId: item.recordId, deletedAt: tomb.deletedAt.toISOString() });
|
|
25167
|
+
}
|
|
25168
|
+
return out3;
|
|
25169
|
+
}
|
|
24746
25170
|
|
|
24747
25171
|
// src/services/branches.ts
|
|
24748
25172
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -24896,7 +25320,7 @@ async function projects_default(app2) {
|
|
|
24896
25320
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
24897
25321
|
const active = listSessions().filter((s2) => s2.projectId === id && !s2.exited).length;
|
|
24898
25322
|
if (active) return reply.code(409).send({ error: `project "${id}" masih punya ${active} sesi aktif` });
|
|
24899
|
-
await
|
|
25323
|
+
await deleteSynced("project", id);
|
|
24900
25324
|
return reply.code(204).send();
|
|
24901
25325
|
});
|
|
24902
25326
|
app2.get("/projects/:id/branches", async (req, reply) => {
|
|
@@ -25347,123 +25771,8 @@ function appendSourceHistory(current, entry) {
|
|
|
25347
25771
|
return [...Array.isArray(current) ? current : [], entry];
|
|
25348
25772
|
}
|
|
25349
25773
|
|
|
25350
|
-
// src/services/notifications.ts
|
|
25351
|
-
init_db();
|
|
25352
|
-
init_pty();
|
|
25353
|
-
async function recordDrift(vpsId, vpsName, drift, snapshotId) {
|
|
25354
|
-
if (drift.length === 0) return;
|
|
25355
|
-
const ids = drift.map((d) => d.itemId);
|
|
25356
|
-
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
|
|
25357
|
-
const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
|
|
25358
|
-
await prisma.notification.create({
|
|
25359
|
-
data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
|
|
25360
|
-
}).catch(() => {
|
|
25361
|
-
});
|
|
25362
|
-
}
|
|
25363
|
-
async function recordCompletion(specId, title, projectId) {
|
|
25364
|
-
await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
25365
|
-
});
|
|
25366
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25367
|
-
await prisma.notification.create({
|
|
25368
|
-
data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
25369
|
-
}).catch(() => {
|
|
25370
|
-
});
|
|
25371
|
-
}
|
|
25372
|
-
async function recordFailure(specId, title, projectId, reason) {
|
|
25373
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25374
|
-
await prisma.notification.create({
|
|
25375
|
-
data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
|
|
25376
|
-
}).catch(() => {
|
|
25377
|
-
});
|
|
25378
|
-
}
|
|
25379
|
-
async function recordAutoMerge(specId, projectId, title) {
|
|
25380
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25381
|
-
await prisma.notification.create({
|
|
25382
|
-
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
25383
|
-
}).catch(() => {
|
|
25384
|
-
});
|
|
25385
|
-
}
|
|
25386
|
-
async function recordCleanupFailure(sessionId2, projectId, entry, reason) {
|
|
25387
|
-
await prisma.notification.create({
|
|
25388
|
-
data: {
|
|
25389
|
-
type: "cleanup",
|
|
25390
|
-
key: `cleanup:${entry}`,
|
|
25391
|
-
sessionId: sessionId2,
|
|
25392
|
-
projectId,
|
|
25393
|
-
title: `Worktree sesi ${sessionId2} gagal dibersihkan \u2014 ${reason}`
|
|
25394
|
-
}
|
|
25395
|
-
}).catch(() => {
|
|
25396
|
-
});
|
|
25397
|
-
}
|
|
25398
|
-
async function recordSourceChange(specId, projectId, title, from, to, seq) {
|
|
25399
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25400
|
-
await prisma.notification.create({
|
|
25401
|
-
data: {
|
|
25402
|
-
type: "spec-source",
|
|
25403
|
-
key: `source:${specId}:${seq}`,
|
|
25404
|
-
specId,
|
|
25405
|
-
sessionId: sessionId2,
|
|
25406
|
-
title: `${specId} \xB7 type ${from} \u2192 ${to} \u2014 ${title}`,
|
|
25407
|
-
projectId
|
|
25408
|
-
}
|
|
25409
|
-
}).catch(() => {
|
|
25410
|
-
});
|
|
25411
|
-
}
|
|
25412
|
-
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
25413
|
-
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
25414
|
-
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
25415
|
-
await prisma.notification.create({
|
|
25416
|
-
data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
|
|
25417
|
-
}).catch(() => {
|
|
25418
|
-
});
|
|
25419
|
-
}
|
|
25420
|
-
async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
|
|
25421
|
-
await prisma.notification.create({
|
|
25422
|
-
data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
|
|
25423
|
-
}).catch(() => {
|
|
25424
|
-
});
|
|
25425
|
-
}
|
|
25426
|
-
async function recordCronRun(cronId, cronName, projectId, dueAt, status, note) {
|
|
25427
|
-
const verb = status === "launched" ? "berjalan" : status === "skipped" ? "dilewati" : "gagal";
|
|
25428
|
-
const title = `Cron "${cronName}" ${verb}${note ? ` \u2014 ${note}` : ""}`;
|
|
25429
|
-
await prisma.notification.create({
|
|
25430
|
-
data: { type: "cron", key: `cron:${cronId}:${dueAt.toISOString()}`, title, projectId }
|
|
25431
|
-
}).catch(() => {
|
|
25432
|
-
});
|
|
25433
|
-
}
|
|
25434
|
-
var awaiting = /* @__PURE__ */ new Set();
|
|
25435
|
-
async function scanDecisions(read2 = liveDecisions) {
|
|
25436
|
-
const next = /* @__PURE__ */ new Set();
|
|
25437
|
-
const fresh = [];
|
|
25438
|
-
for (const s2 of read2()) {
|
|
25439
|
-
if (!markerFilled(s2.decisionFile)) continue;
|
|
25440
|
-
next.add(s2.id);
|
|
25441
|
-
if (!awaiting.has(s2.id)) fresh.push(s2);
|
|
25442
|
-
}
|
|
25443
|
-
awaiting = next;
|
|
25444
|
-
for (const s2 of fresh) {
|
|
25445
|
-
const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
|
|
25446
|
-
await prisma.notification.create({
|
|
25447
|
-
data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
|
|
25448
|
-
});
|
|
25449
|
-
}
|
|
25450
|
-
}
|
|
25451
|
-
var DEFAULT_FEED_TAKE = 50;
|
|
25452
|
-
async function notificationsFeed(p = {}) {
|
|
25453
|
-
await scanDecisions();
|
|
25454
|
-
const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
|
|
25455
|
-
const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
|
|
25456
|
-
const total = await prisma.notification.count();
|
|
25457
|
-
const items = await prisma.notification.findMany({
|
|
25458
|
-
orderBy: { createdAt: "desc" },
|
|
25459
|
-
skip: (page - 1) * pageSize,
|
|
25460
|
-
take: pageSize
|
|
25461
|
-
});
|
|
25462
|
-
const unread = await prisma.notification.count({ where: { readAt: null } });
|
|
25463
|
-
return { items, unread, total, page, pageSize };
|
|
25464
|
-
}
|
|
25465
|
-
|
|
25466
25774
|
// src/routes/specs.ts
|
|
25775
|
+
init_notifications();
|
|
25467
25776
|
init_stage_machine();
|
|
25468
25777
|
|
|
25469
25778
|
// src/services/stage-artifacts.ts
|
|
@@ -28121,6 +28430,7 @@ init_db();
|
|
|
28121
28430
|
init_pty();
|
|
28122
28431
|
init_session_phases();
|
|
28123
28432
|
init_stage_machine();
|
|
28433
|
+
init_notifications();
|
|
28124
28434
|
|
|
28125
28435
|
// src/services/spec-head.ts
|
|
28126
28436
|
init_db();
|
|
@@ -28403,7 +28713,7 @@ ${item.context}` : item.context;
|
|
|
28403
28713
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
28404
28714
|
const { id } = req.params;
|
|
28405
28715
|
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
28406
|
-
await
|
|
28716
|
+
await deleteSynced("spec", id).catch(() => {
|
|
28407
28717
|
});
|
|
28408
28718
|
if (gone) {
|
|
28409
28719
|
const rows = await prisma.spec.findMany({
|
|
@@ -28489,6 +28799,7 @@ ${item.context}` : item.context;
|
|
|
28489
28799
|
|
|
28490
28800
|
// src/routes/notifications.ts
|
|
28491
28801
|
init_db();
|
|
28802
|
+
init_notifications();
|
|
28492
28803
|
async function notifications_default(app2) {
|
|
28493
28804
|
app2.get("/notifications", async (req) => notificationsFeed(req.query));
|
|
28494
28805
|
app2.post("/notifications/read", async (_req, reply) => {
|
|
@@ -30110,6 +30421,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
30110
30421
|
import { readdir as readdir2, rm as rm2 } from "node:fs/promises";
|
|
30111
30422
|
import { promisify as promisify9 } from "node:util";
|
|
30112
30423
|
import { join as join9, resolve as resolve14 } from "node:path";
|
|
30424
|
+
init_notifications();
|
|
30113
30425
|
var TICK_MS = 6e4;
|
|
30114
30426
|
var trashDirOf = (repoDir) => resolve14(repoDir, ".worktrees", ".trash");
|
|
30115
30427
|
var exec8 = promisify9(execFile8);
|
|
@@ -30235,6 +30547,7 @@ async function recordSessionResult(input) {
|
|
|
30235
30547
|
}
|
|
30236
30548
|
|
|
30237
30549
|
// src/routes/terminal.ts
|
|
30550
|
+
init_notifications();
|
|
30238
30551
|
init_stage_machine();
|
|
30239
30552
|
init_pty();
|
|
30240
30553
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
@@ -30618,6 +30931,93 @@ async function terminal_default(app2, opts) {
|
|
|
30618
30931
|
});
|
|
30619
30932
|
}
|
|
30620
30933
|
|
|
30934
|
+
// src/routes/terminal-workspace.ts
|
|
30935
|
+
init_src();
|
|
30936
|
+
|
|
30937
|
+
// src/services/terminal-workspace.ts
|
|
30938
|
+
init_src();
|
|
30939
|
+
init_db();
|
|
30940
|
+
import "@prisma/client";
|
|
30941
|
+
var InvalidStoredTerminalWorkspaceError = class extends Error {
|
|
30942
|
+
constructor() {
|
|
30943
|
+
super("stored terminal workspace is invalid");
|
|
30944
|
+
}
|
|
30945
|
+
};
|
|
30946
|
+
async function readTerminalWorkspace(userId) {
|
|
30947
|
+
const user = await prisma.user.findUniqueOrThrow({
|
|
30948
|
+
where: { id: userId },
|
|
30949
|
+
select: {
|
|
30950
|
+
terminalWorkspace: true,
|
|
30951
|
+
terminalWorkspaceRevision: true,
|
|
30952
|
+
terminalWorkspaceUpdatedAt: true
|
|
30953
|
+
}
|
|
30954
|
+
});
|
|
30955
|
+
if (user.terminalWorkspace === null) {
|
|
30956
|
+
return {
|
|
30957
|
+
workspace: null,
|
|
30958
|
+
revision: user.terminalWorkspaceRevision,
|
|
30959
|
+
updatedAt: user.terminalWorkspaceUpdatedAt?.toISOString() ?? null
|
|
30960
|
+
};
|
|
30961
|
+
}
|
|
30962
|
+
const parsed = zTerminalWorkspaceV1.safeParse(user.terminalWorkspace);
|
|
30963
|
+
if (!parsed.success) throw new InvalidStoredTerminalWorkspaceError();
|
|
30964
|
+
return {
|
|
30965
|
+
workspace: parsed.data,
|
|
30966
|
+
revision: user.terminalWorkspaceRevision,
|
|
30967
|
+
updatedAt: user.terminalWorkspaceUpdatedAt?.toISOString() ?? null
|
|
30968
|
+
};
|
|
30969
|
+
}
|
|
30970
|
+
async function writeTerminalWorkspace(userId, input) {
|
|
30971
|
+
await readTerminalWorkspace(userId);
|
|
30972
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
30973
|
+
const changed = await prisma.user.updateMany({
|
|
30974
|
+
where: { id: userId, terminalWorkspaceRevision: input.baseRevision },
|
|
30975
|
+
data: {
|
|
30976
|
+
terminalWorkspace: input.workspace,
|
|
30977
|
+
terminalWorkspaceRevision: { increment: 1 },
|
|
30978
|
+
terminalWorkspaceUpdatedAt: updatedAt
|
|
30979
|
+
}
|
|
30980
|
+
});
|
|
30981
|
+
if (changed.count === 1) {
|
|
30982
|
+
return {
|
|
30983
|
+
ok: true,
|
|
30984
|
+
current: {
|
|
30985
|
+
workspace: input.workspace,
|
|
30986
|
+
revision: input.baseRevision + 1,
|
|
30987
|
+
updatedAt: updatedAt.toISOString()
|
|
30988
|
+
}
|
|
30989
|
+
};
|
|
30990
|
+
}
|
|
30991
|
+
return { ok: false, current: await readTerminalWorkspace(userId) };
|
|
30992
|
+
}
|
|
30993
|
+
|
|
30994
|
+
// src/routes/terminal-workspace.ts
|
|
30995
|
+
var storedInvalid = (reply) => reply.code(422).send({ error: "stored terminal workspace is invalid" });
|
|
30996
|
+
async function terminalWorkspaceRoutes(app2) {
|
|
30997
|
+
app2.get("/terminal/workspace", async (request, reply) => {
|
|
30998
|
+
try {
|
|
30999
|
+
return await readTerminalWorkspace(request.user.id);
|
|
31000
|
+
} catch (error) {
|
|
31001
|
+
if (error instanceof InvalidStoredTerminalWorkspaceError) return storedInvalid(reply);
|
|
31002
|
+
throw error;
|
|
31003
|
+
}
|
|
31004
|
+
});
|
|
31005
|
+
app2.put("/terminal/workspace", async (request, reply) => {
|
|
31006
|
+
const parsed = zTerminalWorkspaceWrite.safeParse(request.body);
|
|
31007
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
31008
|
+
try {
|
|
31009
|
+
const result = await writeTerminalWorkspace(request.user.id, parsed.data);
|
|
31010
|
+
if (!result.ok) {
|
|
31011
|
+
return reply.code(409).send({ code: "revision-conflict", current: result.current });
|
|
31012
|
+
}
|
|
31013
|
+
return result.current;
|
|
31014
|
+
} catch (error) {
|
|
31015
|
+
if (error instanceof InvalidStoredTerminalWorkspaceError) return storedInvalid(reply);
|
|
31016
|
+
throw error;
|
|
31017
|
+
}
|
|
31018
|
+
});
|
|
31019
|
+
}
|
|
31020
|
+
|
|
30621
31021
|
// src/routes/vps.ts
|
|
30622
31022
|
init_db();
|
|
30623
31023
|
init_src();
|
|
@@ -30858,6 +31258,7 @@ function computeDrift(prev, curr) {
|
|
|
30858
31258
|
}
|
|
30859
31259
|
|
|
30860
31260
|
// src/services/vps-audit.ts
|
|
31261
|
+
init_notifications();
|
|
30861
31262
|
var CRITICAL = [
|
|
30862
31263
|
"sudo_ok",
|
|
30863
31264
|
"os_supported",
|
|
@@ -31121,12 +31522,9 @@ async function vps_default(app2) {
|
|
|
31121
31522
|
return updated;
|
|
31122
31523
|
});
|
|
31123
31524
|
app2.delete("/vps/:id", async (req, reply) => {
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31127
|
-
} catch {
|
|
31128
|
-
return reply.code(404).send({ error: "not found" });
|
|
31129
|
-
}
|
|
31525
|
+
const { id } = req.params;
|
|
31526
|
+
if (!await deleteSynced("vps", id)) return reply.code(404).send({ error: "not found" });
|
|
31527
|
+
return reply.code(204).send();
|
|
31130
31528
|
});
|
|
31131
31529
|
app2.post("/vps/:id/audit", async (req, reply) => {
|
|
31132
31530
|
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
@@ -31654,6 +32052,7 @@ async function update(app2) {
|
|
|
31654
32052
|
|
|
31655
32053
|
// src/services/events.ts
|
|
31656
32054
|
init_pty();
|
|
32055
|
+
init_notifications();
|
|
31657
32056
|
|
|
31658
32057
|
// src/services/lead/deciding.ts
|
|
31659
32058
|
var deciding = /* @__PURE__ */ new Set();
|
|
@@ -31822,7 +32221,7 @@ async function events_default(app2, opts) {
|
|
|
31822
32221
|
// src/routes/device-tokens.ts
|
|
31823
32222
|
init_src();
|
|
31824
32223
|
init_db();
|
|
31825
|
-
var
|
|
32224
|
+
var view2 = (t) => ({
|
|
31826
32225
|
id: t.id,
|
|
31827
32226
|
name: t.name,
|
|
31828
32227
|
createdAt: t.createdAt.toISOString(),
|
|
@@ -31830,7 +32229,7 @@ var view = (t) => ({
|
|
|
31830
32229
|
revokedAt: t.revokedAt?.toISOString() ?? null
|
|
31831
32230
|
});
|
|
31832
32231
|
async function device_tokens_default(app2) {
|
|
31833
|
-
app2.get("/device-tokens", async (req) => (await prisma.deviceToken.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" } })).map(
|
|
32232
|
+
app2.get("/device-tokens", async (req) => (await prisma.deviceToken.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" } })).map(view2));
|
|
31834
32233
|
app2.post("/device-tokens", async (req, reply) => {
|
|
31835
32234
|
const p = zIssueDeviceToken.safeParse(req.body);
|
|
31836
32235
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -31980,7 +32379,10 @@ var zPush = external_exports.object({
|
|
|
31980
32379
|
entity: external_exports.string(),
|
|
31981
32380
|
id: external_exports.string(),
|
|
31982
32381
|
baseVersion: external_exports.number().int().nonnegative(),
|
|
31983
|
-
data: external_exports.record(external_exports.unknown())
|
|
32382
|
+
data: external_exports.record(external_exports.unknown()),
|
|
32383
|
+
// SPEC-799 · ADR-0119 · absen = "upsert" (client versi lama). Hub versi lama membuang field ini
|
|
32384
|
+
// dan sekadar memperlakukan push delete sebagai update — status quo, bukan galat.
|
|
32385
|
+
op: external_exports.enum(["upsert", "delete"]).optional()
|
|
31984
32386
|
}))
|
|
31985
32387
|
});
|
|
31986
32388
|
var AUTHORED = ["spec", "sessionResult"];
|
|
@@ -32010,7 +32412,7 @@ async function sync_default(app2) {
|
|
|
32010
32412
|
}
|
|
32011
32413
|
const data = { ...rec.data };
|
|
32012
32414
|
if (AUTHORED.includes(rec.entity) && !data.author && user) data.author = user.email;
|
|
32013
|
-
const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id);
|
|
32415
|
+
const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id, rec.op ?? "upsert");
|
|
32014
32416
|
results.push({ id: rec.id, ...r });
|
|
32015
32417
|
}
|
|
32016
32418
|
return { results };
|
|
@@ -32022,6 +32424,10 @@ async function sync_default(app2) {
|
|
|
32022
32424
|
return { ok: true, full, ...stats2 };
|
|
32023
32425
|
});
|
|
32024
32426
|
app2.get("/sync/conflicts", async () => ({ conflicts: await listConflicts() }));
|
|
32427
|
+
app2.get("/sync/pending", async () => {
|
|
32428
|
+
const deletes = await listPendingDeletes();
|
|
32429
|
+
return { deletes, total: deletes.length };
|
|
32430
|
+
});
|
|
32025
32431
|
const zResolve = external_exports.object({ choice: external_exports.enum(["local", "server"]) });
|
|
32026
32432
|
app2.post("/sync/conflicts/:entity/:recordId/resolve", async (req, reply) => {
|
|
32027
32433
|
const { entity, recordId } = req.params;
|
|
@@ -32072,7 +32478,7 @@ async function sync_default(app2) {
|
|
|
32072
32478
|
|
|
32073
32479
|
// src/routes/session-results.ts
|
|
32074
32480
|
init_db();
|
|
32075
|
-
var
|
|
32481
|
+
var view3 = (r) => ({
|
|
32076
32482
|
id: r.id,
|
|
32077
32483
|
projectId: r.projectId,
|
|
32078
32484
|
specId: r.specId,
|
|
@@ -32095,7 +32501,7 @@ async function session_results_default(app2) {
|
|
|
32095
32501
|
orderBy: { createdAt: "desc" },
|
|
32096
32502
|
take: take2
|
|
32097
32503
|
});
|
|
32098
|
-
return rows.map(
|
|
32504
|
+
return rows.map(view3);
|
|
32099
32505
|
});
|
|
32100
32506
|
app2.delete("/session-results", async (req, reply) => {
|
|
32101
32507
|
const { projectId, before: before2 } = req.query;
|
|
@@ -32107,8 +32513,10 @@ async function session_results_default(app2) {
|
|
|
32107
32513
|
if (Number.isNaN(d.getTime())) return reply.code(400).send({ error: "before bukan tanggal valid" });
|
|
32108
32514
|
where.createdAt = { lt: d };
|
|
32109
32515
|
}
|
|
32110
|
-
const
|
|
32111
|
-
|
|
32516
|
+
const rows = await prisma.sessionResult.findMany({ where, select: { id: true } });
|
|
32517
|
+
let purged = 0;
|
|
32518
|
+
for (const r of rows) if (await deleteSynced("sessionResult", r.id)) purged++;
|
|
32519
|
+
return { purged };
|
|
32112
32520
|
});
|
|
32113
32521
|
}
|
|
32114
32522
|
|
|
@@ -32161,7 +32569,7 @@ async function deleteTranscript(key) {
|
|
|
32161
32569
|
}
|
|
32162
32570
|
|
|
32163
32571
|
// src/services/session-history.ts
|
|
32164
|
-
var
|
|
32572
|
+
var view4 = (r) => ({
|
|
32165
32573
|
id: r.id,
|
|
32166
32574
|
sessionId: r.sessionId,
|
|
32167
32575
|
projectId: r.projectId,
|
|
@@ -32245,11 +32653,11 @@ async function listHistory(q) {
|
|
|
32245
32653
|
skip: (page - 1) * pageSize,
|
|
32246
32654
|
take: pageSize
|
|
32247
32655
|
});
|
|
32248
|
-
return { items: rows.map(
|
|
32656
|
+
return { items: rows.map(view4), total, page, pageSize };
|
|
32249
32657
|
}
|
|
32250
32658
|
async function getHistory(id) {
|
|
32251
32659
|
const r = await prisma.sessionHistory.findUnique({ where: { id } });
|
|
32252
|
-
return r ? { ...
|
|
32660
|
+
return r ? { ...view4(r), hasTranscript: !!r.transcriptKey } : null;
|
|
32253
32661
|
}
|
|
32254
32662
|
async function transcriptOf(id) {
|
|
32255
32663
|
const r = await prisma.sessionHistory.findUnique({ where: { id }, select: { transcriptKey: true } });
|
|
@@ -32325,7 +32733,7 @@ init_config_apply();
|
|
|
32325
32733
|
init_sync_client();
|
|
32326
32734
|
var isSecret2 = (e) => e.kind === "secret";
|
|
32327
32735
|
var agentBlocked = (req, e) => Boolean(req.agent) && e.category === "credential";
|
|
32328
|
-
function
|
|
32736
|
+
function view5(e) {
|
|
32329
32737
|
const eff = effectiveStr(e.key);
|
|
32330
32738
|
const base2 = {
|
|
32331
32739
|
key: e.key,
|
|
@@ -32345,7 +32753,7 @@ function view4(e) {
|
|
|
32345
32753
|
}
|
|
32346
32754
|
async function config_default(app2) {
|
|
32347
32755
|
app2.get("/config", async () => ({
|
|
32348
|
-
entries: CONFIG_REGISTRY.map(
|
|
32756
|
+
entries: CONFIG_REGISTRY.map(view5),
|
|
32349
32757
|
sync: syncStatus()
|
|
32350
32758
|
}));
|
|
32351
32759
|
app2.put("/config", async (req, reply) => {
|
|
@@ -32357,7 +32765,7 @@ async function config_default(app2) {
|
|
|
32357
32765
|
const raw2 = b.value ?? "";
|
|
32358
32766
|
if (isSecret2(entry) && raw2.trim() === "") {
|
|
32359
32767
|
if (rawDbValue(entry.key) === void 0) return reply.code(400).send({ error: "tak boleh kosong" });
|
|
32360
|
-
return
|
|
32768
|
+
return view5(entry);
|
|
32361
32769
|
}
|
|
32362
32770
|
const parsed = parseConfigValue(entry, raw2);
|
|
32363
32771
|
if (!parsed.ok) return reply.code(400).send({ error: parsed.error });
|
|
@@ -32367,11 +32775,11 @@ async function config_default(app2) {
|
|
|
32367
32775
|
} catch (error) {
|
|
32368
32776
|
return reply.code(400).send({ error: error.message });
|
|
32369
32777
|
}
|
|
32370
|
-
return
|
|
32778
|
+
return view5(entry);
|
|
32371
32779
|
}
|
|
32372
32780
|
await setConfig(entry.key, parsed.value);
|
|
32373
32781
|
await applyConfigSideEffect(entry.key);
|
|
32374
|
-
return
|
|
32782
|
+
return view5(entry);
|
|
32375
32783
|
});
|
|
32376
32784
|
app2.delete("/config/:key", async (req, reply) => {
|
|
32377
32785
|
const { key } = req.params;
|
|
@@ -32437,6 +32845,7 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32437
32845
|
|
|
32438
32846
|
// src/services/ticket-intake.ts
|
|
32439
32847
|
init_db();
|
|
32848
|
+
init_notifications();
|
|
32440
32849
|
|
|
32441
32850
|
// src/services/upload-pipeline.ts
|
|
32442
32851
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -33670,25 +34079,25 @@ var cachedDecoders = {
|
|
|
33670
34079
|
};
|
|
33671
34080
|
var cachedEncoder = new globalThis.TextEncoder();
|
|
33672
34081
|
var byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
|
|
33673
|
-
function getUintBE(
|
|
33674
|
-
const { byteLength } =
|
|
34082
|
+
function getUintBE(view12) {
|
|
34083
|
+
const { byteLength } = view12;
|
|
33675
34084
|
if (byteLength === 6) {
|
|
33676
|
-
return
|
|
34085
|
+
return view12.getUint16(0) * 2 ** 32 + view12.getUint32(2);
|
|
33677
34086
|
}
|
|
33678
34087
|
if (byteLength === 5) {
|
|
33679
|
-
return
|
|
34088
|
+
return view12.getUint8(0) * 2 ** 32 + view12.getUint32(1);
|
|
33680
34089
|
}
|
|
33681
34090
|
if (byteLength === 4) {
|
|
33682
|
-
return
|
|
34091
|
+
return view12.getUint32(0);
|
|
33683
34092
|
}
|
|
33684
34093
|
if (byteLength === 3) {
|
|
33685
|
-
return
|
|
34094
|
+
return view12.getUint8(0) * 2 ** 16 + view12.getUint16(1);
|
|
33686
34095
|
}
|
|
33687
34096
|
if (byteLength === 2) {
|
|
33688
|
-
return
|
|
34097
|
+
return view12.getUint16(0);
|
|
33689
34098
|
}
|
|
33690
34099
|
if (byteLength === 1) {
|
|
33691
|
-
return
|
|
34100
|
+
return view12.getUint8(0);
|
|
33692
34101
|
}
|
|
33693
34102
|
}
|
|
33694
34103
|
|
|
@@ -40173,7 +40582,7 @@ async function acceptTicket(t, opts) {
|
|
|
40173
40582
|
|
|
40174
40583
|
// src/routes/tickets.ts
|
|
40175
40584
|
init_src();
|
|
40176
|
-
var
|
|
40585
|
+
var view6 = (t) => ({
|
|
40177
40586
|
id: t.id,
|
|
40178
40587
|
projectId: t.projectId,
|
|
40179
40588
|
number: t.number,
|
|
@@ -40201,7 +40610,7 @@ async function tickets_default(app2) {
|
|
|
40201
40610
|
rows = rows.filter((t) => `${t.title} ${t.reporterEmail}`.toLowerCase().includes(n2));
|
|
40202
40611
|
}
|
|
40203
40612
|
const unreviewed = rows.filter((t) => t.status === "new").length;
|
|
40204
|
-
return { ...paginate(rows.map(
|
|
40613
|
+
return { ...paginate(rows.map(view6), page, limit), unreviewed };
|
|
40205
40614
|
});
|
|
40206
40615
|
app2.get("/tickets/:id", async (req, reply) => {
|
|
40207
40616
|
const { id } = req.params;
|
|
@@ -40219,7 +40628,7 @@ async function tickets_default(app2) {
|
|
|
40219
40628
|
const base2 = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40220
40629
|
const publicStatusUrl = `${base2}/help/${encodeURIComponent(t.projectId)}/status/${shareToken}`;
|
|
40221
40630
|
return {
|
|
40222
|
-
...
|
|
40631
|
+
...view6(t),
|
|
40223
40632
|
detail: t.detail,
|
|
40224
40633
|
attachments: t.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40225
40634
|
spec,
|
|
@@ -40279,7 +40688,7 @@ async function tickets_default(app2) {
|
|
|
40279
40688
|
});
|
|
40280
40689
|
const spec = updated.specId ? await prisma.spec.findUnique({ where: { id: updated.specId } }) : null;
|
|
40281
40690
|
return {
|
|
40282
|
-
...
|
|
40691
|
+
...view6(updated),
|
|
40283
40692
|
detail: updated.detail,
|
|
40284
40693
|
attachments: updated.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40285
40694
|
spec
|
|
@@ -40290,7 +40699,7 @@ async function tickets_default(app2) {
|
|
|
40290
40699
|
const t = await prisma.ticket.findUnique({ where: { id }, include: { attachments: true } });
|
|
40291
40700
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40292
40701
|
for (const a of t.attachments) await deleteUpload(a.storageKey);
|
|
40293
|
-
await
|
|
40702
|
+
await deleteSynced("ticket", id);
|
|
40294
40703
|
return { ok: true };
|
|
40295
40704
|
});
|
|
40296
40705
|
}
|
|
@@ -40397,6 +40806,7 @@ async function isQueued(id) {
|
|
|
40397
40806
|
// src/services/scheduler/cron.ts
|
|
40398
40807
|
init_db();
|
|
40399
40808
|
init_src();
|
|
40809
|
+
init_notifications();
|
|
40400
40810
|
var GRACE_MS = 30 * 6e4;
|
|
40401
40811
|
function computeNextRun(expr, after) {
|
|
40402
40812
|
const spec = parseCron(expr);
|
|
@@ -40747,6 +41157,7 @@ async function leadProjects() {
|
|
|
40747
41157
|
init_src();
|
|
40748
41158
|
init_db();
|
|
40749
41159
|
init_pty();
|
|
41160
|
+
init_notifications();
|
|
40750
41161
|
|
|
40751
41162
|
// src/services/lead/prompt.ts
|
|
40752
41163
|
init_src();
|
|
@@ -41518,6 +41929,7 @@ init_src();
|
|
|
41518
41929
|
init_db();
|
|
41519
41930
|
init_pty();
|
|
41520
41931
|
init_session_phases();
|
|
41932
|
+
init_notifications();
|
|
41521
41933
|
|
|
41522
41934
|
// src/services/webhooks/actor.ts
|
|
41523
41935
|
init_src();
|
|
@@ -41670,6 +42082,7 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
41670
42082
|
init_src();
|
|
41671
42083
|
init_pty();
|
|
41672
42084
|
init_tui_dialog();
|
|
42085
|
+
init_notifications();
|
|
41673
42086
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
41674
42087
|
|
|
41675
42088
|
// src/services/lead/pane.ts
|
|
@@ -41990,6 +42403,7 @@ function sweep(liveIds) {
|
|
|
41990
42403
|
init_db();
|
|
41991
42404
|
init_pty();
|
|
41992
42405
|
init_session_phases();
|
|
42406
|
+
init_notifications();
|
|
41993
42407
|
var moduleOf = (p) => p.split("/").slice(0, 2).join("/");
|
|
41994
42408
|
function findCollisions(areas) {
|
|
41995
42409
|
const out3 = [];
|
|
@@ -42257,6 +42671,7 @@ async function orderProject(projectId, deps) {
|
|
|
42257
42671
|
}
|
|
42258
42672
|
|
|
42259
42673
|
// src/services/lead/engine.ts
|
|
42674
|
+
init_notifications();
|
|
42260
42675
|
var TICK_MS2 = 5e3;
|
|
42261
42676
|
var busyDetect = false;
|
|
42262
42677
|
var busyPulse = false;
|
|
@@ -42771,7 +43186,7 @@ async function generateChangelog(projectId, req, deps = {}) {
|
|
|
42771
43186
|
}
|
|
42772
43187
|
|
|
42773
43188
|
// src/routes/changelog.ts
|
|
42774
|
-
var
|
|
43189
|
+
var view7 = (c) => ({
|
|
42775
43190
|
id: c.id,
|
|
42776
43191
|
projectId: c.projectId,
|
|
42777
43192
|
mode: c.mode,
|
|
@@ -42811,7 +43226,7 @@ async function changelog_default(app2) {
|
|
|
42811
43226
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
42812
43227
|
const { page, limit, q } = req.query;
|
|
42813
43228
|
const rows = await prisma.changelog.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
|
|
42814
|
-
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(
|
|
43229
|
+
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(view7), page, limit);
|
|
42815
43230
|
});
|
|
42816
43231
|
app2.get("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42817
43232
|
const { id, cid } = req.params;
|
|
@@ -42825,7 +43240,7 @@ async function changelog_default(app2) {
|
|
|
42825
43240
|
eyebrow: `hanoman \xB7 ${id} \xB7 changelog`,
|
|
42826
43241
|
path: row.title
|
|
42827
43242
|
});
|
|
42828
|
-
return
|
|
43243
|
+
return view7(row);
|
|
42829
43244
|
});
|
|
42830
43245
|
app2.post("/projects/:id/changelog", async (req, reply) => {
|
|
42831
43246
|
const { id } = req.params;
|
|
@@ -42834,7 +43249,7 @@ async function changelog_default(app2) {
|
|
|
42834
43249
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
42835
43250
|
const r = await generateChangelog(id, parsed.data);
|
|
42836
43251
|
if (!r.ok) return reply.code(422).send({ error: r.reason });
|
|
42837
|
-
return reply.code(201).send(
|
|
43252
|
+
return reply.code(201).send(view7(r.row));
|
|
42838
43253
|
});
|
|
42839
43254
|
app2.delete("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42840
43255
|
const { id, cid } = req.params;
|
|
@@ -42973,7 +43388,7 @@ async function installCustomAgents() {
|
|
|
42973
43388
|
|
|
42974
43389
|
// src/routes/custom-agents.ts
|
|
42975
43390
|
var rowsOf = async () => await prisma.customAgent.findMany();
|
|
42976
|
-
var
|
|
43391
|
+
var view8 = (r, projectId) => ({
|
|
42977
43392
|
id: r.id,
|
|
42978
43393
|
projectId: r.projectId,
|
|
42979
43394
|
name: r.name,
|
|
@@ -43018,7 +43433,7 @@ async function custom_agents_default(app2) {
|
|
|
43018
43433
|
const byName = /* @__PURE__ */ new Map();
|
|
43019
43434
|
for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
|
|
43020
43435
|
for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
|
|
43021
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) =>
|
|
43436
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId));
|
|
43022
43437
|
});
|
|
43023
43438
|
app2.post("/custom-agents", async (req, reply) => {
|
|
43024
43439
|
const parsed = zCreateCustomAgent.safeParse(req.body);
|
|
@@ -43065,7 +43480,7 @@ async function custom_agents_default(app2) {
|
|
|
43065
43480
|
} });
|
|
43066
43481
|
await loadCustomAgents();
|
|
43067
43482
|
await notifySynced("customAgent", id);
|
|
43068
|
-
return reply.code(201).send(
|
|
43483
|
+
return reply.code(201).send(view8(row));
|
|
43069
43484
|
});
|
|
43070
43485
|
app2.patch("/custom-agents/:id", async (req, reply) => {
|
|
43071
43486
|
const { id } = req.params;
|
|
@@ -43112,13 +43527,13 @@ async function custom_agents_default(app2) {
|
|
|
43112
43527
|
} });
|
|
43113
43528
|
await loadCustomAgents();
|
|
43114
43529
|
await notifySynced("customAgent", id);
|
|
43115
|
-
return
|
|
43530
|
+
return view8(row);
|
|
43116
43531
|
});
|
|
43117
43532
|
app2.delete("/custom-agents/:id", async (req, reply) => {
|
|
43118
43533
|
const { id } = req.params;
|
|
43119
43534
|
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
43120
43535
|
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
43121
|
-
await
|
|
43536
|
+
await deleteSynced("customAgent", id);
|
|
43122
43537
|
const name2 = existing.name;
|
|
43123
43538
|
for (const r of await rowsOf()) {
|
|
43124
43539
|
const m = mentionsOf(r.mentions);
|
|
@@ -43453,7 +43868,7 @@ var STATUS = {
|
|
|
43453
43868
|
unauthorized: 401,
|
|
43454
43869
|
other: 502
|
|
43455
43870
|
};
|
|
43456
|
-
var
|
|
43871
|
+
var view9 = (i) => ({
|
|
43457
43872
|
id: i.id,
|
|
43458
43873
|
projectId: i.projectId,
|
|
43459
43874
|
repoSlug: i.repoSlug,
|
|
@@ -43488,7 +43903,7 @@ async function githubIssues(app2) {
|
|
|
43488
43903
|
where: { projectId: id, ...status ? { status } : {} },
|
|
43489
43904
|
orderBy: [{ number: "desc" }]
|
|
43490
43905
|
});
|
|
43491
|
-
return reply.send(paginate(items.map(
|
|
43906
|
+
return reply.send(paginate(items.map(view9), page, limit));
|
|
43492
43907
|
});
|
|
43493
43908
|
app2.post("/github-issues/accept", async (req, reply) => {
|
|
43494
43909
|
const parsed = zAcceptMany.safeParse(req.body ?? {});
|
|
@@ -44248,7 +44663,7 @@ async function portal_default(app2) {
|
|
|
44248
44663
|
// src/routes/client-accounts.ts
|
|
44249
44664
|
init_src();
|
|
44250
44665
|
init_db();
|
|
44251
|
-
var
|
|
44666
|
+
var view10 = (u) => ({
|
|
44252
44667
|
id: u.id,
|
|
44253
44668
|
email: u.email,
|
|
44254
44669
|
disabled: u.disabled,
|
|
@@ -44279,7 +44694,7 @@ async function client_accounts_default(app2) {
|
|
|
44279
44694
|
orderBy: { createdAt: "asc" },
|
|
44280
44695
|
include: { projectAccess: { select: { projectId: true } } }
|
|
44281
44696
|
});
|
|
44282
|
-
return { items: rows.map(
|
|
44697
|
+
return { items: rows.map(view10) };
|
|
44283
44698
|
});
|
|
44284
44699
|
app2.post("/client-accounts", async (req, reply) => {
|
|
44285
44700
|
const p = zCreateClientAccount.safeParse(req.body);
|
|
@@ -44293,7 +44708,7 @@ async function client_accounts_default(app2) {
|
|
|
44293
44708
|
await prisma.user.delete({ where: { id: user.id } });
|
|
44294
44709
|
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
44295
44710
|
}
|
|
44296
|
-
return reply.code(201).send(
|
|
44711
|
+
return reply.code(201).send(view10(await load(user.id)));
|
|
44297
44712
|
});
|
|
44298
44713
|
app2.patch("/client-accounts/:id", async (req, reply) => {
|
|
44299
44714
|
const { id } = req.params;
|
|
@@ -44307,7 +44722,7 @@ async function client_accounts_default(app2) {
|
|
|
44307
44722
|
if (p.data.password)
|
|
44308
44723
|
await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(p.data.password) } });
|
|
44309
44724
|
if (p.data.disabled || p.data.password) await deleteUserSessions(id);
|
|
44310
|
-
return
|
|
44725
|
+
return view10(await load(id));
|
|
44311
44726
|
});
|
|
44312
44727
|
app2.delete("/client-accounts/:id", async (req, reply) => {
|
|
44313
44728
|
const { id } = req.params;
|
|
@@ -44403,7 +44818,7 @@ async function consumeSetupToken(home3) {
|
|
|
44403
44818
|
|
|
44404
44819
|
// src/routes/auth.ts
|
|
44405
44820
|
var BOOTSTRAP_USER_ID = "bootstrap-admin";
|
|
44406
|
-
var
|
|
44821
|
+
var view11 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
44407
44822
|
async function issue(reply, userId) {
|
|
44408
44823
|
const token = await createSession2(userId);
|
|
44409
44824
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -44443,7 +44858,7 @@ async function auth_default(app2, opts) {
|
|
|
44443
44858
|
if (opts.bootstrapRequired) await consumeSetupToken(opts.home);
|
|
44444
44859
|
setupAttempts.clear(req.ip);
|
|
44445
44860
|
await issue(reply, user.id);
|
|
44446
|
-
return { user:
|
|
44861
|
+
return { user: view11(user) };
|
|
44447
44862
|
});
|
|
44448
44863
|
app2.post("/auth/login", async (req, reply) => {
|
|
44449
44864
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -44456,7 +44871,7 @@ async function auth_default(app2, opts) {
|
|
|
44456
44871
|
}
|
|
44457
44872
|
clearLoginFails(req.ip);
|
|
44458
44873
|
await issue(reply, user.id);
|
|
44459
|
-
return { user:
|
|
44874
|
+
return { user: view11(user) };
|
|
44460
44875
|
});
|
|
44461
44876
|
app2.post("/auth/logout", async (req, reply) => {
|
|
44462
44877
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -44464,7 +44879,7 @@ async function auth_default(app2, opts) {
|
|
|
44464
44879
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
44465
44880
|
return reply.code(204).send();
|
|
44466
44881
|
});
|
|
44467
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
44882
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view11));
|
|
44468
44883
|
app2.post("/auth/users", async (req, reply) => {
|
|
44469
44884
|
const p = zSignup.safeParse(req.body);
|
|
44470
44885
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -44475,7 +44890,7 @@ async function auth_default(app2, opts) {
|
|
|
44475
44890
|
// (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
|
|
44476
44891
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "admin" }
|
|
44477
44892
|
});
|
|
44478
|
-
return
|
|
44893
|
+
return view11(user);
|
|
44479
44894
|
});
|
|
44480
44895
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
44481
44896
|
const target2 = await prisma.user.findUnique({ where: { id: req.params.id } });
|
|
@@ -44498,7 +44913,7 @@ async function auth_default(app2, opts) {
|
|
|
44498
44913
|
});
|
|
44499
44914
|
await deleteUserSessions(user.id);
|
|
44500
44915
|
await issue(reply, user.id);
|
|
44501
|
-
return { user:
|
|
44916
|
+
return { user: view11(user) };
|
|
44502
44917
|
});
|
|
44503
44918
|
}
|
|
44504
44919
|
|
|
@@ -44511,15 +44926,15 @@ async function agent_tokens_default(app2) {
|
|
|
44511
44926
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
44512
44927
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
44513
44928
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44514
|
-
const { view:
|
|
44515
|
-
return reply.code(201).send({ ...
|
|
44929
|
+
const { view: view12, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
44930
|
+
return reply.code(201).send({ ...view12, token });
|
|
44516
44931
|
});
|
|
44517
44932
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
44518
44933
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
44519
44934
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44520
44935
|
const { id } = req.params;
|
|
44521
|
-
const
|
|
44522
|
-
return
|
|
44936
|
+
const view12 = await patchAgentToken(id, p.data);
|
|
44937
|
+
return view12 ? reply.send(view12) : reply.code(404).send({ error: "not found" });
|
|
44523
44938
|
});
|
|
44524
44939
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
44525
44940
|
const { id } = req.params;
|
|
@@ -44586,6 +45001,7 @@ function capabilityForRoute(method, path2) {
|
|
|
44586
45001
|
if (top === "vps") return rw("vps");
|
|
44587
45002
|
if (top === "prds") return rw("docs");
|
|
44588
45003
|
if (top === "terminal") {
|
|
45004
|
+
if (seg[1] === "workspace") return "COOKIE_ONLY";
|
|
44589
45005
|
if (seg[seg.length - 1] === "ws") return "sessions:write";
|
|
44590
45006
|
return rw("sessions");
|
|
44591
45007
|
}
|
|
@@ -44817,7 +45233,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
44817
45233
|
if (PUBLIC.has(`${req.method} ${path2}`)) return;
|
|
44818
45234
|
if (user?.role === "client" && !clientRouteAllowed(req.method, path2))
|
|
44819
45235
|
return reply.code(403).send({ error: "portal klien: baca-saja" });
|
|
44820
|
-
if (path2.startsWith("/api/sync") && path2 !== "/api/sync/now" && !path2.startsWith("/api/sync/conflicts")) return;
|
|
45236
|
+
if (path2.startsWith("/api/sync") && path2 !== "/api/sync/now" && path2 !== "/api/sync/pending" && !path2.startsWith("/api/sync/conflicts")) return;
|
|
44821
45237
|
if (path2.startsWith("/api/help")) return;
|
|
44822
45238
|
if (user) return;
|
|
44823
45239
|
const agentTok = agentTokenFromReq(req);
|
|
@@ -44852,6 +45268,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
44852
45268
|
await api.register(fs_default);
|
|
44853
45269
|
const wsOptions = { allowedOrigins: wsControlOrigins(env) };
|
|
44854
45270
|
await api.register(terminal_default, wsOptions);
|
|
45271
|
+
await api.register(terminalWorkspaceRoutes);
|
|
44855
45272
|
await api.register(vps_default);
|
|
44856
45273
|
await api.register(limits);
|
|
44857
45274
|
await api.register(update);
|
|
@@ -44930,6 +45347,7 @@ init_src();
|
|
|
44930
45347
|
|
|
44931
45348
|
// src/services/scheduler/governor.ts
|
|
44932
45349
|
init_db();
|
|
45350
|
+
init_notifications();
|
|
44933
45351
|
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
44934
45352
|
var canceledRaceNote = (sessionId2) => `dibatalkan saat sesi ${sessionId2} sudah terlanjur lahir \u2014 sesi dibiarkan hidup, tutup dari Terminal bila tak diperlukan`;
|
|
44935
45353
|
var CAP_FULL_NOTE = "cap penuh \u2014 tak ada slot sesi";
|
|
@@ -45064,6 +45482,7 @@ async function startCronSession(cron) {
|
|
|
45064
45482
|
// src/services/scheduler/reconcile.ts
|
|
45065
45483
|
init_db();
|
|
45066
45484
|
init_src2();
|
|
45485
|
+
init_notifications();
|
|
45067
45486
|
init_stage_machine();
|
|
45068
45487
|
init_pty();
|
|
45069
45488
|
init_session_phases();
|
|
@@ -45127,6 +45546,7 @@ var reconcileProdDeps = {
|
|
|
45127
45546
|
};
|
|
45128
45547
|
|
|
45129
45548
|
// src/services/scheduler/engine.ts
|
|
45549
|
+
init_notifications();
|
|
45130
45550
|
init_pty();
|
|
45131
45551
|
var prodEnd = {
|
|
45132
45552
|
reconcile: () => reconcile(reconcileProdDeps),
|
|
@@ -45501,6 +45921,7 @@ init_src();
|
|
|
45501
45921
|
init_db();
|
|
45502
45922
|
import { execFile as execFile13 } from "node:child_process";
|
|
45503
45923
|
import { promisify as promisify12 } from "node:util";
|
|
45924
|
+
init_notifications();
|
|
45504
45925
|
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
45505
45926
|
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
45506
45927
|
var TICK_MS5 = 6e4;
|