hanoman 0.1.39 → 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 +4 -0
- package/dist/server.js +471 -205
- package/package.json +1 -1
- package/prisma/migrations/20260815120000_sync_tombstone/migration.sql +14 -0
- package/prisma/schema.prisma +26 -0
- package/web/assets/{index-DeGA9N3Z.css → index-BeFLNV7l.css} +1 -1
- package/web/assets/{index-BzBNb9qp.js → index-zZ7g98st.js} +1547 -1546
- package/web/index.html +2 -2
package/dist/server.js
CHANGED
|
@@ -6195,6 +6195,8 @@ var init_api = __esm({
|
|
|
6195
6195
|
syncNow: `${API}/sync/now`,
|
|
6196
6196
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
6197
6197
|
syncConflicts: `${API}/sync/conflicts`,
|
|
6198
|
+
// SPEC-799 · ADR-0119 · penghapusan yang menunggu jendela online (cookie-only, cermin syncNow).
|
|
6199
|
+
syncPending: `${API}/sync/pending`,
|
|
6198
6200
|
syncConflictResolve: (entity, recordId) => `${API}/sync/conflicts/${encodeURIComponent(entity)}/${encodeURIComponent(recordId)}/resolve`,
|
|
6199
6201
|
// SPEC-257 · ADR-0065 · agent token (kelola cookie-only) + katalog capability
|
|
6200
6202
|
agentTokens: `${API}/agent-tokens`,
|
|
@@ -10487,16 +10489,54 @@ var init_rename_project = __esm({
|
|
|
10487
10489
|
}
|
|
10488
10490
|
});
|
|
10489
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
|
+
|
|
10490
10526
|
// src/services/sync.ts
|
|
10491
10527
|
var sync_exports = {};
|
|
10492
10528
|
__export(sync_exports, {
|
|
10529
|
+
PARENTS: () => PARENTS,
|
|
10493
10530
|
SYNCED: () => SYNCED,
|
|
10494
10531
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
10495
10532
|
__FIELDS: () => __FIELDS,
|
|
10496
10533
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
10497
10534
|
applyPush: () => applyPush,
|
|
10498
10535
|
backfillFeed: () => backfillFeed,
|
|
10536
|
+
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
10537
|
+
deleteRow: () => deleteRow,
|
|
10499
10538
|
isEntity: () => isEntity,
|
|
10539
|
+
publishDelete: () => publishDelete,
|
|
10500
10540
|
publishLocal: () => publishLocal,
|
|
10501
10541
|
pull: () => pull,
|
|
10502
10542
|
setAcceptedHook: () => setAcceptedHook,
|
|
@@ -10561,10 +10601,24 @@ async function snapshot(entity, id) {
|
|
|
10561
10601
|
for (const f of FIELDS[entity]) data[f] = jsonSafe(row[f]);
|
|
10562
10602
|
return { version: Number(row.version), data };
|
|
10563
10603
|
}
|
|
10564
|
-
async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
10604
|
+
async function applyPush(entity, id, baseVersion, data, deviceId, op = "upsert") {
|
|
10565
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
|
+
}
|
|
10566
10616
|
if (entity === "project" && typeof data.renamedFrom === "string" && data.renamedFrom && data.renamedFrom !== id) {
|
|
10567
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
|
+
}
|
|
10568
10622
|
const already = await DELEGATE.project.findUnique({ where: { id }, select: { version: true } });
|
|
10569
10623
|
if (already) return { ok: true, version: Number(already.version) };
|
|
10570
10624
|
const old = await DELEGATE.project.findUnique({ where: { id: oldId }, select: { version: true } });
|
|
@@ -10578,17 +10632,24 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10578
10632
|
const snap2 = await snapshot("project", id);
|
|
10579
10633
|
const logData = { ...snap2?.data ?? {}, renamedFrom: oldId };
|
|
10580
10634
|
const log3 = await prisma.syncLog.create({
|
|
10581
|
-
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 }
|
|
10582
10636
|
});
|
|
10583
|
-
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) });
|
|
10584
10638
|
return { ok: true, version: newVersion2 };
|
|
10585
10639
|
}
|
|
10586
10640
|
}
|
|
10641
|
+
const tomb = await findTombstone(entity, id);
|
|
10587
10642
|
const existing = await DELEGATE[entity].findUnique({ where: { id }, select: { version: true } });
|
|
10588
|
-
|
|
10589
|
-
|
|
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
|
+
};
|
|
10590
10651
|
}
|
|
10591
|
-
const newVersion =
|
|
10652
|
+
const newVersion = (currentVersion ?? 0) + 1;
|
|
10592
10653
|
const writeData = coerce2(entity, data);
|
|
10593
10654
|
const stamp2 = writeData.updatedAt ?? /* @__PURE__ */ new Date();
|
|
10594
10655
|
await DELEGATE[entity].upsert({
|
|
@@ -10596,11 +10657,12 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10596
10657
|
create: { id, ...writeData, version: newVersion, updatedAt: stamp2 },
|
|
10597
10658
|
update: { ...writeData, version: newVersion, updatedAt: stamp2 }
|
|
10598
10659
|
});
|
|
10660
|
+
if (tomb) await clearTombstone(entity, id);
|
|
10599
10661
|
const snap = await snapshot(entity, id);
|
|
10600
10662
|
const log2 = await prisma.syncLog.create({
|
|
10601
|
-
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 }
|
|
10602
10664
|
});
|
|
10603
|
-
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) });
|
|
10604
10666
|
return { ok: true, version: newVersion };
|
|
10605
10667
|
}
|
|
10606
10668
|
async function pull(sinceCursor, limit = 500) {
|
|
@@ -10613,7 +10675,13 @@ async function pull(sinceCursor, limit = 500) {
|
|
|
10613
10675
|
const cursor = rows.length ? String(rows[rows.length - 1].seq) : sinceCursor || "0";
|
|
10614
10676
|
return {
|
|
10615
10677
|
cursor,
|
|
10616
|
-
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
|
+
}))
|
|
10617
10685
|
};
|
|
10618
10686
|
}
|
|
10619
10687
|
async function publishLocal(entity, id) {
|
|
@@ -10622,9 +10690,38 @@ async function publishLocal(entity, id) {
|
|
|
10622
10690
|
const newVersion = snap.version + 1;
|
|
10623
10691
|
await DELEGATE[entity].update({ where: { id }, data: { version: newVersion } });
|
|
10624
10692
|
const log2 = await prisma.syncLog.create({
|
|
10625
|
-
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 }
|
|
10694
|
+
});
|
|
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
|
+
}
|
|
10626
10712
|
});
|
|
10627
|
-
onAccepted?.({ entity, recordId: id, version:
|
|
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;
|
|
10628
10725
|
}
|
|
10629
10726
|
async function backfillFeed() {
|
|
10630
10727
|
let published = 0;
|
|
@@ -10640,6 +10737,16 @@ async function backfillFeed() {
|
|
|
10640
10737
|
published++;
|
|
10641
10738
|
}
|
|
10642
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
|
+
}
|
|
10643
10750
|
return published;
|
|
10644
10751
|
}
|
|
10645
10752
|
async function upsertLocal(entity, id, version2, data) {
|
|
@@ -10667,12 +10774,13 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
10667
10774
|
function setAcceptedHook(hook) {
|
|
10668
10775
|
onAccepted = hook;
|
|
10669
10776
|
}
|
|
10670
|
-
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;
|
|
10671
10778
|
var init_sync = __esm({
|
|
10672
10779
|
"src/services/sync.ts"() {
|
|
10673
10780
|
"use strict";
|
|
10674
10781
|
init_db();
|
|
10675
10782
|
init_rename_project();
|
|
10783
|
+
init_tombstone();
|
|
10676
10784
|
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
|
|
10677
10785
|
DELEGATE = {
|
|
10678
10786
|
project: prisma.project,
|
|
@@ -10746,6 +10854,13 @@ var init_sync = __esm({
|
|
|
10746
10854
|
customAgent: ["createdAt", "updatedAt"],
|
|
10747
10855
|
githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
|
|
10748
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
|
+
};
|
|
10749
10864
|
__FIELDS = FIELDS;
|
|
10750
10865
|
__DATE_FIELDS = DATE_FIELDS;
|
|
10751
10866
|
NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -10888,6 +11003,134 @@ var init_codex_trust = __esm({
|
|
|
10888
11003
|
}
|
|
10889
11004
|
});
|
|
10890
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
|
+
|
|
10891
11134
|
// src/services/agent-token.ts
|
|
10892
11135
|
import { randomBytes as randomBytes2, createHash, timingSafeEqual } from "node:crypto";
|
|
10893
11136
|
function toAgentTokenView(t) {
|
|
@@ -16171,7 +16414,14 @@ function validateIncomingRecord(input) {
|
|
|
16171
16414
|
if (!row.data || typeof row.data !== "object" || Array.isArray(row.data)) throw new Error("sync data invalid");
|
|
16172
16415
|
if (Buffer.byteLength(JSON.stringify(input)) > MAX_SYNC_RECORD_BYTES) throw new Error("sync record terlalu besar");
|
|
16173
16416
|
validateSyncData(row.entity, row.data, { allowProjectRename: true });
|
|
16174
|
-
|
|
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
|
+
};
|
|
16175
16425
|
}
|
|
16176
16426
|
async function getCursor() {
|
|
16177
16427
|
const s2 = await prisma.syncState.findUnique({ where: { id: 1 } });
|
|
@@ -16180,15 +16430,50 @@ async function getCursor() {
|
|
|
16180
16430
|
async function setCursor(cursor) {
|
|
16181
16431
|
await prisma.syncState.upsert({ where: { id: 1 }, create: { id: 1, cursor }, update: { cursor } });
|
|
16182
16432
|
}
|
|
16183
|
-
async function applyRemote(entity, recordId, version2, data) {
|
|
16184
|
-
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";
|
|
16185
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;
|
|
16186
16471
|
}
|
|
16187
16472
|
async function applyFeedFrame(msg) {
|
|
16188
16473
|
if (!msg.entity || !msg.recordId) return true;
|
|
16189
16474
|
try {
|
|
16190
16475
|
const record2 = validateIncomingRecord({ ...msg, version: Number(msg.version ?? 0), data: msg.data ?? {} });
|
|
16191
|
-
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);
|
|
16192
16477
|
} catch {
|
|
16193
16478
|
feedHole = true;
|
|
16194
16479
|
return false;
|
|
@@ -16197,7 +16482,7 @@ async function applyFeedFrame(msg) {
|
|
|
16197
16482
|
return true;
|
|
16198
16483
|
}
|
|
16199
16484
|
async function syncOnce(transport) {
|
|
16200
|
-
let pulled = 0, pushed = 0, conflicts = 0;
|
|
16485
|
+
let pulled = 0, pushed = 0, conflicts = 0, deleted = 0, dropped = 0;
|
|
16201
16486
|
const cursor = await getCursor();
|
|
16202
16487
|
const outbox = await listOutbox();
|
|
16203
16488
|
const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
|
|
@@ -16216,7 +16501,11 @@ async function syncOnce(transport) {
|
|
|
16216
16501
|
const deferred = [];
|
|
16217
16502
|
for (const rec of records) {
|
|
16218
16503
|
if (!isEntity(rec.entity)) continue;
|
|
16219
|
-
if (
|
|
16504
|
+
if (!rec.op) {
|
|
16505
|
+
dropped++;
|
|
16506
|
+
continue;
|
|
16507
|
+
}
|
|
16508
|
+
if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
|
|
16220
16509
|
const local = await snapshot(rec.entity, rec.recordId);
|
|
16221
16510
|
if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
|
|
16222
16511
|
await markConflict(
|
|
@@ -16229,8 +16518,10 @@ async function syncOnce(transport) {
|
|
|
16229
16518
|
continue;
|
|
16230
16519
|
}
|
|
16231
16520
|
try {
|
|
16232
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16233
|
-
|
|
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++;
|
|
16234
16525
|
} catch {
|
|
16235
16526
|
deferred.push(rec);
|
|
16236
16527
|
}
|
|
@@ -16240,15 +16531,18 @@ async function syncOnce(transport) {
|
|
|
16240
16531
|
const still = [];
|
|
16241
16532
|
for (const rec of rest) {
|
|
16242
16533
|
try {
|
|
16243
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16244
|
-
|
|
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++;
|
|
16245
16538
|
} catch {
|
|
16246
16539
|
still.push(rec);
|
|
16247
16540
|
}
|
|
16248
16541
|
}
|
|
16249
16542
|
if (still.length === rest.length) {
|
|
16250
16543
|
for (const rec of still) {
|
|
16251
|
-
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++;
|
|
16252
16546
|
}
|
|
16253
16547
|
break;
|
|
16254
16548
|
}
|
|
@@ -16282,7 +16576,24 @@ async function syncOnce(transport) {
|
|
|
16282
16576
|
}
|
|
16283
16577
|
const snap = await snapshot(item.entity, item.recordId);
|
|
16284
16578
|
if (!snap) {
|
|
16285
|
-
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
|
+
}
|
|
16286
16597
|
continue;
|
|
16287
16598
|
}
|
|
16288
16599
|
const res = await transport("POST", "/api/sync/push", {
|
|
@@ -16298,6 +16609,18 @@ async function syncOnce(transport) {
|
|
|
16298
16609
|
await clearOutbox(item.entity, item.recordId);
|
|
16299
16610
|
pushed++;
|
|
16300
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
|
+
}
|
|
16301
16624
|
const server = r.server;
|
|
16302
16625
|
if (server && JSON.stringify(server.data) !== JSON.stringify(snap.data)) {
|
|
16303
16626
|
await markConflict(
|
|
@@ -16312,7 +16635,7 @@ async function syncOnce(transport) {
|
|
|
16312
16635
|
}
|
|
16313
16636
|
}
|
|
16314
16637
|
}
|
|
16315
|
-
return { pulled, pushed, conflicts };
|
|
16638
|
+
return { pulled, pushed, conflicts, deleted, dropped };
|
|
16316
16639
|
}
|
|
16317
16640
|
function fetchTransport(base2, token) {
|
|
16318
16641
|
return async (method, path2, body) => {
|
|
@@ -16343,13 +16666,15 @@ async function syncNow(opts) {
|
|
|
16343
16666
|
const transport = fetchTransport(base2, token);
|
|
16344
16667
|
if (!opts?.full) return syncOnce(transport);
|
|
16345
16668
|
await setCursor("0");
|
|
16346
|
-
const total = { pulled: 0, pushed: 0, conflicts: 0 };
|
|
16669
|
+
const total = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
|
|
16347
16670
|
let seen = "0";
|
|
16348
16671
|
for (let page = 0; page < FULL_PULL_MAX_PAGES; page++) {
|
|
16349
16672
|
const s2 = await syncOnce(transport);
|
|
16350
16673
|
total.pulled += s2.pulled;
|
|
16351
16674
|
total.pushed += s2.pushed;
|
|
16352
16675
|
total.conflicts += s2.conflicts;
|
|
16676
|
+
total.deleted += s2.deleted;
|
|
16677
|
+
total.dropped += s2.dropped;
|
|
16353
16678
|
const now = await getCursor();
|
|
16354
16679
|
if (now === seen) break;
|
|
16355
16680
|
seen = now;
|
|
@@ -16432,6 +16757,8 @@ var init_sync_client = __esm({
|
|
|
16432
16757
|
"use strict";
|
|
16433
16758
|
init_db();
|
|
16434
16759
|
init_sync();
|
|
16760
|
+
init_tombstone();
|
|
16761
|
+
init_notifications();
|
|
16435
16762
|
init_conflicts();
|
|
16436
16763
|
init_outbox();
|
|
16437
16764
|
init_rename_project();
|
|
@@ -24804,11 +25131,42 @@ init_sync();
|
|
|
24804
25131
|
async function notifySynced(entity, id) {
|
|
24805
25132
|
try {
|
|
24806
25133
|
if (!isEntity(entity)) return;
|
|
25134
|
+
await consumeTombstoneOnRecreate(entity, id);
|
|
24807
25135
|
if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
|
|
24808
25136
|
else await publishLocal(entity, id);
|
|
24809
25137
|
} catch {
|
|
24810
25138
|
}
|
|
24811
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
|
+
}
|
|
24812
25170
|
|
|
24813
25171
|
// src/services/branches.ts
|
|
24814
25172
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -24962,7 +25320,7 @@ async function projects_default(app2) {
|
|
|
24962
25320
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
24963
25321
|
const active = listSessions().filter((s2) => s2.projectId === id && !s2.exited).length;
|
|
24964
25322
|
if (active) return reply.code(409).send({ error: `project "${id}" masih punya ${active} sesi aktif` });
|
|
24965
|
-
await
|
|
25323
|
+
await deleteSynced("project", id);
|
|
24966
25324
|
return reply.code(204).send();
|
|
24967
25325
|
});
|
|
24968
25326
|
app2.get("/projects/:id/branches", async (req, reply) => {
|
|
@@ -25413,123 +25771,8 @@ function appendSourceHistory(current, entry) {
|
|
|
25413
25771
|
return [...Array.isArray(current) ? current : [], entry];
|
|
25414
25772
|
}
|
|
25415
25773
|
|
|
25416
|
-
// src/services/notifications.ts
|
|
25417
|
-
init_db();
|
|
25418
|
-
init_pty();
|
|
25419
|
-
async function recordDrift(vpsId, vpsName, drift, snapshotId) {
|
|
25420
|
-
if (drift.length === 0) return;
|
|
25421
|
-
const ids = drift.map((d) => d.itemId);
|
|
25422
|
-
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
|
|
25423
|
-
const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
|
|
25424
|
-
await prisma.notification.create({
|
|
25425
|
-
data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
|
|
25426
|
-
}).catch(() => {
|
|
25427
|
-
});
|
|
25428
|
-
}
|
|
25429
|
-
async function recordCompletion(specId, title, projectId) {
|
|
25430
|
-
await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
25431
|
-
});
|
|
25432
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25433
|
-
await prisma.notification.create({
|
|
25434
|
-
data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
25435
|
-
}).catch(() => {
|
|
25436
|
-
});
|
|
25437
|
-
}
|
|
25438
|
-
async function recordFailure(specId, title, projectId, reason) {
|
|
25439
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25440
|
-
await prisma.notification.create({
|
|
25441
|
-
data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
|
|
25442
|
-
}).catch(() => {
|
|
25443
|
-
});
|
|
25444
|
-
}
|
|
25445
|
-
async function recordAutoMerge(specId, projectId, title) {
|
|
25446
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25447
|
-
await prisma.notification.create({
|
|
25448
|
-
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
25449
|
-
}).catch(() => {
|
|
25450
|
-
});
|
|
25451
|
-
}
|
|
25452
|
-
async function recordCleanupFailure(sessionId2, projectId, entry, reason) {
|
|
25453
|
-
await prisma.notification.create({
|
|
25454
|
-
data: {
|
|
25455
|
-
type: "cleanup",
|
|
25456
|
-
key: `cleanup:${entry}`,
|
|
25457
|
-
sessionId: sessionId2,
|
|
25458
|
-
projectId,
|
|
25459
|
-
title: `Worktree sesi ${sessionId2} gagal dibersihkan \u2014 ${reason}`
|
|
25460
|
-
}
|
|
25461
|
-
}).catch(() => {
|
|
25462
|
-
});
|
|
25463
|
-
}
|
|
25464
|
-
async function recordSourceChange(specId, projectId, title, from, to, seq) {
|
|
25465
|
-
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25466
|
-
await prisma.notification.create({
|
|
25467
|
-
data: {
|
|
25468
|
-
type: "spec-source",
|
|
25469
|
-
key: `source:${specId}:${seq}`,
|
|
25470
|
-
specId,
|
|
25471
|
-
sessionId: sessionId2,
|
|
25472
|
-
title: `${specId} \xB7 type ${from} \u2192 ${to} \u2014 ${title}`,
|
|
25473
|
-
projectId
|
|
25474
|
-
}
|
|
25475
|
-
}).catch(() => {
|
|
25476
|
-
});
|
|
25477
|
-
}
|
|
25478
|
-
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
25479
|
-
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
25480
|
-
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
25481
|
-
await prisma.notification.create({
|
|
25482
|
-
data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
|
|
25483
|
-
}).catch(() => {
|
|
25484
|
-
});
|
|
25485
|
-
}
|
|
25486
|
-
async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
|
|
25487
|
-
await prisma.notification.create({
|
|
25488
|
-
data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
|
|
25489
|
-
}).catch(() => {
|
|
25490
|
-
});
|
|
25491
|
-
}
|
|
25492
|
-
async function recordCronRun(cronId, cronName, projectId, dueAt, status, note) {
|
|
25493
|
-
const verb = status === "launched" ? "berjalan" : status === "skipped" ? "dilewati" : "gagal";
|
|
25494
|
-
const title = `Cron "${cronName}" ${verb}${note ? ` \u2014 ${note}` : ""}`;
|
|
25495
|
-
await prisma.notification.create({
|
|
25496
|
-
data: { type: "cron", key: `cron:${cronId}:${dueAt.toISOString()}`, title, projectId }
|
|
25497
|
-
}).catch(() => {
|
|
25498
|
-
});
|
|
25499
|
-
}
|
|
25500
|
-
var awaiting = /* @__PURE__ */ new Set();
|
|
25501
|
-
async function scanDecisions(read2 = liveDecisions) {
|
|
25502
|
-
const next = /* @__PURE__ */ new Set();
|
|
25503
|
-
const fresh = [];
|
|
25504
|
-
for (const s2 of read2()) {
|
|
25505
|
-
if (!markerFilled(s2.decisionFile)) continue;
|
|
25506
|
-
next.add(s2.id);
|
|
25507
|
-
if (!awaiting.has(s2.id)) fresh.push(s2);
|
|
25508
|
-
}
|
|
25509
|
-
awaiting = next;
|
|
25510
|
-
for (const s2 of fresh) {
|
|
25511
|
-
const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
|
|
25512
|
-
await prisma.notification.create({
|
|
25513
|
-
data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
|
|
25514
|
-
});
|
|
25515
|
-
}
|
|
25516
|
-
}
|
|
25517
|
-
var DEFAULT_FEED_TAKE = 50;
|
|
25518
|
-
async function notificationsFeed(p = {}) {
|
|
25519
|
-
await scanDecisions();
|
|
25520
|
-
const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
|
|
25521
|
-
const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
|
|
25522
|
-
const total = await prisma.notification.count();
|
|
25523
|
-
const items = await prisma.notification.findMany({
|
|
25524
|
-
orderBy: { createdAt: "desc" },
|
|
25525
|
-
skip: (page - 1) * pageSize,
|
|
25526
|
-
take: pageSize
|
|
25527
|
-
});
|
|
25528
|
-
const unread = await prisma.notification.count({ where: { readAt: null } });
|
|
25529
|
-
return { items, unread, total, page, pageSize };
|
|
25530
|
-
}
|
|
25531
|
-
|
|
25532
25774
|
// src/routes/specs.ts
|
|
25775
|
+
init_notifications();
|
|
25533
25776
|
init_stage_machine();
|
|
25534
25777
|
|
|
25535
25778
|
// src/services/stage-artifacts.ts
|
|
@@ -28187,6 +28430,7 @@ init_db();
|
|
|
28187
28430
|
init_pty();
|
|
28188
28431
|
init_session_phases();
|
|
28189
28432
|
init_stage_machine();
|
|
28433
|
+
init_notifications();
|
|
28190
28434
|
|
|
28191
28435
|
// src/services/spec-head.ts
|
|
28192
28436
|
init_db();
|
|
@@ -28469,7 +28713,7 @@ ${item.context}` : item.context;
|
|
|
28469
28713
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
28470
28714
|
const { id } = req.params;
|
|
28471
28715
|
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
28472
|
-
await
|
|
28716
|
+
await deleteSynced("spec", id).catch(() => {
|
|
28473
28717
|
});
|
|
28474
28718
|
if (gone) {
|
|
28475
28719
|
const rows = await prisma.spec.findMany({
|
|
@@ -28555,6 +28799,7 @@ ${item.context}` : item.context;
|
|
|
28555
28799
|
|
|
28556
28800
|
// src/routes/notifications.ts
|
|
28557
28801
|
init_db();
|
|
28802
|
+
init_notifications();
|
|
28558
28803
|
async function notifications_default(app2) {
|
|
28559
28804
|
app2.get("/notifications", async (req) => notificationsFeed(req.query));
|
|
28560
28805
|
app2.post("/notifications/read", async (_req, reply) => {
|
|
@@ -30176,6 +30421,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
30176
30421
|
import { readdir as readdir2, rm as rm2 } from "node:fs/promises";
|
|
30177
30422
|
import { promisify as promisify9 } from "node:util";
|
|
30178
30423
|
import { join as join9, resolve as resolve14 } from "node:path";
|
|
30424
|
+
init_notifications();
|
|
30179
30425
|
var TICK_MS = 6e4;
|
|
30180
30426
|
var trashDirOf = (repoDir) => resolve14(repoDir, ".worktrees", ".trash");
|
|
30181
30427
|
var exec8 = promisify9(execFile8);
|
|
@@ -30301,6 +30547,7 @@ async function recordSessionResult(input) {
|
|
|
30301
30547
|
}
|
|
30302
30548
|
|
|
30303
30549
|
// src/routes/terminal.ts
|
|
30550
|
+
init_notifications();
|
|
30304
30551
|
init_stage_machine();
|
|
30305
30552
|
init_pty();
|
|
30306
30553
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
@@ -31011,6 +31258,7 @@ function computeDrift(prev, curr) {
|
|
|
31011
31258
|
}
|
|
31012
31259
|
|
|
31013
31260
|
// src/services/vps-audit.ts
|
|
31261
|
+
init_notifications();
|
|
31014
31262
|
var CRITICAL = [
|
|
31015
31263
|
"sudo_ok",
|
|
31016
31264
|
"os_supported",
|
|
@@ -31274,12 +31522,9 @@ async function vps_default(app2) {
|
|
|
31274
31522
|
return updated;
|
|
31275
31523
|
});
|
|
31276
31524
|
app2.delete("/vps/:id", async (req, reply) => {
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
} catch {
|
|
31281
|
-
return reply.code(404).send({ error: "not found" });
|
|
31282
|
-
}
|
|
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();
|
|
31283
31528
|
});
|
|
31284
31529
|
app2.post("/vps/:id/audit", async (req, reply) => {
|
|
31285
31530
|
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
@@ -31807,6 +32052,7 @@ async function update(app2) {
|
|
|
31807
32052
|
|
|
31808
32053
|
// src/services/events.ts
|
|
31809
32054
|
init_pty();
|
|
32055
|
+
init_notifications();
|
|
31810
32056
|
|
|
31811
32057
|
// src/services/lead/deciding.ts
|
|
31812
32058
|
var deciding = /* @__PURE__ */ new Set();
|
|
@@ -31975,7 +32221,7 @@ async function events_default(app2, opts) {
|
|
|
31975
32221
|
// src/routes/device-tokens.ts
|
|
31976
32222
|
init_src();
|
|
31977
32223
|
init_db();
|
|
31978
|
-
var
|
|
32224
|
+
var view2 = (t) => ({
|
|
31979
32225
|
id: t.id,
|
|
31980
32226
|
name: t.name,
|
|
31981
32227
|
createdAt: t.createdAt.toISOString(),
|
|
@@ -31983,7 +32229,7 @@ var view = (t) => ({
|
|
|
31983
32229
|
revokedAt: t.revokedAt?.toISOString() ?? null
|
|
31984
32230
|
});
|
|
31985
32231
|
async function device_tokens_default(app2) {
|
|
31986
|
-
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));
|
|
31987
32233
|
app2.post("/device-tokens", async (req, reply) => {
|
|
31988
32234
|
const p = zIssueDeviceToken.safeParse(req.body);
|
|
31989
32235
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -32133,7 +32379,10 @@ var zPush = external_exports.object({
|
|
|
32133
32379
|
entity: external_exports.string(),
|
|
32134
32380
|
id: external_exports.string(),
|
|
32135
32381
|
baseVersion: external_exports.number().int().nonnegative(),
|
|
32136
|
-
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()
|
|
32137
32386
|
}))
|
|
32138
32387
|
});
|
|
32139
32388
|
var AUTHORED = ["spec", "sessionResult"];
|
|
@@ -32163,7 +32412,7 @@ async function sync_default(app2) {
|
|
|
32163
32412
|
}
|
|
32164
32413
|
const data = { ...rec.data };
|
|
32165
32414
|
if (AUTHORED.includes(rec.entity) && !data.author && user) data.author = user.email;
|
|
32166
|
-
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");
|
|
32167
32416
|
results.push({ id: rec.id, ...r });
|
|
32168
32417
|
}
|
|
32169
32418
|
return { results };
|
|
@@ -32175,6 +32424,10 @@ async function sync_default(app2) {
|
|
|
32175
32424
|
return { ok: true, full, ...stats2 };
|
|
32176
32425
|
});
|
|
32177
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
|
+
});
|
|
32178
32431
|
const zResolve = external_exports.object({ choice: external_exports.enum(["local", "server"]) });
|
|
32179
32432
|
app2.post("/sync/conflicts/:entity/:recordId/resolve", async (req, reply) => {
|
|
32180
32433
|
const { entity, recordId } = req.params;
|
|
@@ -32225,7 +32478,7 @@ async function sync_default(app2) {
|
|
|
32225
32478
|
|
|
32226
32479
|
// src/routes/session-results.ts
|
|
32227
32480
|
init_db();
|
|
32228
|
-
var
|
|
32481
|
+
var view3 = (r) => ({
|
|
32229
32482
|
id: r.id,
|
|
32230
32483
|
projectId: r.projectId,
|
|
32231
32484
|
specId: r.specId,
|
|
@@ -32248,7 +32501,7 @@ async function session_results_default(app2) {
|
|
|
32248
32501
|
orderBy: { createdAt: "desc" },
|
|
32249
32502
|
take: take2
|
|
32250
32503
|
});
|
|
32251
|
-
return rows.map(
|
|
32504
|
+
return rows.map(view3);
|
|
32252
32505
|
});
|
|
32253
32506
|
app2.delete("/session-results", async (req, reply) => {
|
|
32254
32507
|
const { projectId, before: before2 } = req.query;
|
|
@@ -32260,8 +32513,10 @@ async function session_results_default(app2) {
|
|
|
32260
32513
|
if (Number.isNaN(d.getTime())) return reply.code(400).send({ error: "before bukan tanggal valid" });
|
|
32261
32514
|
where.createdAt = { lt: d };
|
|
32262
32515
|
}
|
|
32263
|
-
const
|
|
32264
|
-
|
|
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 };
|
|
32265
32520
|
});
|
|
32266
32521
|
}
|
|
32267
32522
|
|
|
@@ -32314,7 +32569,7 @@ async function deleteTranscript(key) {
|
|
|
32314
32569
|
}
|
|
32315
32570
|
|
|
32316
32571
|
// src/services/session-history.ts
|
|
32317
|
-
var
|
|
32572
|
+
var view4 = (r) => ({
|
|
32318
32573
|
id: r.id,
|
|
32319
32574
|
sessionId: r.sessionId,
|
|
32320
32575
|
projectId: r.projectId,
|
|
@@ -32398,11 +32653,11 @@ async function listHistory(q) {
|
|
|
32398
32653
|
skip: (page - 1) * pageSize,
|
|
32399
32654
|
take: pageSize
|
|
32400
32655
|
});
|
|
32401
|
-
return { items: rows.map(
|
|
32656
|
+
return { items: rows.map(view4), total, page, pageSize };
|
|
32402
32657
|
}
|
|
32403
32658
|
async function getHistory(id) {
|
|
32404
32659
|
const r = await prisma.sessionHistory.findUnique({ where: { id } });
|
|
32405
|
-
return r ? { ...
|
|
32660
|
+
return r ? { ...view4(r), hasTranscript: !!r.transcriptKey } : null;
|
|
32406
32661
|
}
|
|
32407
32662
|
async function transcriptOf(id) {
|
|
32408
32663
|
const r = await prisma.sessionHistory.findUnique({ where: { id }, select: { transcriptKey: true } });
|
|
@@ -32478,7 +32733,7 @@ init_config_apply();
|
|
|
32478
32733
|
init_sync_client();
|
|
32479
32734
|
var isSecret2 = (e) => e.kind === "secret";
|
|
32480
32735
|
var agentBlocked = (req, e) => Boolean(req.agent) && e.category === "credential";
|
|
32481
|
-
function
|
|
32736
|
+
function view5(e) {
|
|
32482
32737
|
const eff = effectiveStr(e.key);
|
|
32483
32738
|
const base2 = {
|
|
32484
32739
|
key: e.key,
|
|
@@ -32498,7 +32753,7 @@ function view4(e) {
|
|
|
32498
32753
|
}
|
|
32499
32754
|
async function config_default(app2) {
|
|
32500
32755
|
app2.get("/config", async () => ({
|
|
32501
|
-
entries: CONFIG_REGISTRY.map(
|
|
32756
|
+
entries: CONFIG_REGISTRY.map(view5),
|
|
32502
32757
|
sync: syncStatus()
|
|
32503
32758
|
}));
|
|
32504
32759
|
app2.put("/config", async (req, reply) => {
|
|
@@ -32510,7 +32765,7 @@ async function config_default(app2) {
|
|
|
32510
32765
|
const raw2 = b.value ?? "";
|
|
32511
32766
|
if (isSecret2(entry) && raw2.trim() === "") {
|
|
32512
32767
|
if (rawDbValue(entry.key) === void 0) return reply.code(400).send({ error: "tak boleh kosong" });
|
|
32513
|
-
return
|
|
32768
|
+
return view5(entry);
|
|
32514
32769
|
}
|
|
32515
32770
|
const parsed = parseConfigValue(entry, raw2);
|
|
32516
32771
|
if (!parsed.ok) return reply.code(400).send({ error: parsed.error });
|
|
@@ -32520,11 +32775,11 @@ async function config_default(app2) {
|
|
|
32520
32775
|
} catch (error) {
|
|
32521
32776
|
return reply.code(400).send({ error: error.message });
|
|
32522
32777
|
}
|
|
32523
|
-
return
|
|
32778
|
+
return view5(entry);
|
|
32524
32779
|
}
|
|
32525
32780
|
await setConfig(entry.key, parsed.value);
|
|
32526
32781
|
await applyConfigSideEffect(entry.key);
|
|
32527
|
-
return
|
|
32782
|
+
return view5(entry);
|
|
32528
32783
|
});
|
|
32529
32784
|
app2.delete("/config/:key", async (req, reply) => {
|
|
32530
32785
|
const { key } = req.params;
|
|
@@ -32590,6 +32845,7 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32590
32845
|
|
|
32591
32846
|
// src/services/ticket-intake.ts
|
|
32592
32847
|
init_db();
|
|
32848
|
+
init_notifications();
|
|
32593
32849
|
|
|
32594
32850
|
// src/services/upload-pipeline.ts
|
|
32595
32851
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -33823,25 +34079,25 @@ var cachedDecoders = {
|
|
|
33823
34079
|
};
|
|
33824
34080
|
var cachedEncoder = new globalThis.TextEncoder();
|
|
33825
34081
|
var byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
|
|
33826
|
-
function getUintBE(
|
|
33827
|
-
const { byteLength } =
|
|
34082
|
+
function getUintBE(view12) {
|
|
34083
|
+
const { byteLength } = view12;
|
|
33828
34084
|
if (byteLength === 6) {
|
|
33829
|
-
return
|
|
34085
|
+
return view12.getUint16(0) * 2 ** 32 + view12.getUint32(2);
|
|
33830
34086
|
}
|
|
33831
34087
|
if (byteLength === 5) {
|
|
33832
|
-
return
|
|
34088
|
+
return view12.getUint8(0) * 2 ** 32 + view12.getUint32(1);
|
|
33833
34089
|
}
|
|
33834
34090
|
if (byteLength === 4) {
|
|
33835
|
-
return
|
|
34091
|
+
return view12.getUint32(0);
|
|
33836
34092
|
}
|
|
33837
34093
|
if (byteLength === 3) {
|
|
33838
|
-
return
|
|
34094
|
+
return view12.getUint8(0) * 2 ** 16 + view12.getUint16(1);
|
|
33839
34095
|
}
|
|
33840
34096
|
if (byteLength === 2) {
|
|
33841
|
-
return
|
|
34097
|
+
return view12.getUint16(0);
|
|
33842
34098
|
}
|
|
33843
34099
|
if (byteLength === 1) {
|
|
33844
|
-
return
|
|
34100
|
+
return view12.getUint8(0);
|
|
33845
34101
|
}
|
|
33846
34102
|
}
|
|
33847
34103
|
|
|
@@ -40326,7 +40582,7 @@ async function acceptTicket(t, opts) {
|
|
|
40326
40582
|
|
|
40327
40583
|
// src/routes/tickets.ts
|
|
40328
40584
|
init_src();
|
|
40329
|
-
var
|
|
40585
|
+
var view6 = (t) => ({
|
|
40330
40586
|
id: t.id,
|
|
40331
40587
|
projectId: t.projectId,
|
|
40332
40588
|
number: t.number,
|
|
@@ -40354,7 +40610,7 @@ async function tickets_default(app2) {
|
|
|
40354
40610
|
rows = rows.filter((t) => `${t.title} ${t.reporterEmail}`.toLowerCase().includes(n2));
|
|
40355
40611
|
}
|
|
40356
40612
|
const unreviewed = rows.filter((t) => t.status === "new").length;
|
|
40357
|
-
return { ...paginate(rows.map(
|
|
40613
|
+
return { ...paginate(rows.map(view6), page, limit), unreviewed };
|
|
40358
40614
|
});
|
|
40359
40615
|
app2.get("/tickets/:id", async (req, reply) => {
|
|
40360
40616
|
const { id } = req.params;
|
|
@@ -40372,7 +40628,7 @@ async function tickets_default(app2) {
|
|
|
40372
40628
|
const base2 = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40373
40629
|
const publicStatusUrl = `${base2}/help/${encodeURIComponent(t.projectId)}/status/${shareToken}`;
|
|
40374
40630
|
return {
|
|
40375
|
-
...
|
|
40631
|
+
...view6(t),
|
|
40376
40632
|
detail: t.detail,
|
|
40377
40633
|
attachments: t.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40378
40634
|
spec,
|
|
@@ -40432,7 +40688,7 @@ async function tickets_default(app2) {
|
|
|
40432
40688
|
});
|
|
40433
40689
|
const spec = updated.specId ? await prisma.spec.findUnique({ where: { id: updated.specId } }) : null;
|
|
40434
40690
|
return {
|
|
40435
|
-
...
|
|
40691
|
+
...view6(updated),
|
|
40436
40692
|
detail: updated.detail,
|
|
40437
40693
|
attachments: updated.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40438
40694
|
spec
|
|
@@ -40443,7 +40699,7 @@ async function tickets_default(app2) {
|
|
|
40443
40699
|
const t = await prisma.ticket.findUnique({ where: { id }, include: { attachments: true } });
|
|
40444
40700
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40445
40701
|
for (const a of t.attachments) await deleteUpload(a.storageKey);
|
|
40446
|
-
await
|
|
40702
|
+
await deleteSynced("ticket", id);
|
|
40447
40703
|
return { ok: true };
|
|
40448
40704
|
});
|
|
40449
40705
|
}
|
|
@@ -40550,6 +40806,7 @@ async function isQueued(id) {
|
|
|
40550
40806
|
// src/services/scheduler/cron.ts
|
|
40551
40807
|
init_db();
|
|
40552
40808
|
init_src();
|
|
40809
|
+
init_notifications();
|
|
40553
40810
|
var GRACE_MS = 30 * 6e4;
|
|
40554
40811
|
function computeNextRun(expr, after) {
|
|
40555
40812
|
const spec = parseCron(expr);
|
|
@@ -40900,6 +41157,7 @@ async function leadProjects() {
|
|
|
40900
41157
|
init_src();
|
|
40901
41158
|
init_db();
|
|
40902
41159
|
init_pty();
|
|
41160
|
+
init_notifications();
|
|
40903
41161
|
|
|
40904
41162
|
// src/services/lead/prompt.ts
|
|
40905
41163
|
init_src();
|
|
@@ -41671,6 +41929,7 @@ init_src();
|
|
|
41671
41929
|
init_db();
|
|
41672
41930
|
init_pty();
|
|
41673
41931
|
init_session_phases();
|
|
41932
|
+
init_notifications();
|
|
41674
41933
|
|
|
41675
41934
|
// src/services/webhooks/actor.ts
|
|
41676
41935
|
init_src();
|
|
@@ -41823,6 +42082,7 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
41823
42082
|
init_src();
|
|
41824
42083
|
init_pty();
|
|
41825
42084
|
init_tui_dialog();
|
|
42085
|
+
init_notifications();
|
|
41826
42086
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
41827
42087
|
|
|
41828
42088
|
// src/services/lead/pane.ts
|
|
@@ -42143,6 +42403,7 @@ function sweep(liveIds) {
|
|
|
42143
42403
|
init_db();
|
|
42144
42404
|
init_pty();
|
|
42145
42405
|
init_session_phases();
|
|
42406
|
+
init_notifications();
|
|
42146
42407
|
var moduleOf = (p) => p.split("/").slice(0, 2).join("/");
|
|
42147
42408
|
function findCollisions(areas) {
|
|
42148
42409
|
const out3 = [];
|
|
@@ -42410,6 +42671,7 @@ async function orderProject(projectId, deps) {
|
|
|
42410
42671
|
}
|
|
42411
42672
|
|
|
42412
42673
|
// src/services/lead/engine.ts
|
|
42674
|
+
init_notifications();
|
|
42413
42675
|
var TICK_MS2 = 5e3;
|
|
42414
42676
|
var busyDetect = false;
|
|
42415
42677
|
var busyPulse = false;
|
|
@@ -42924,7 +43186,7 @@ async function generateChangelog(projectId, req, deps = {}) {
|
|
|
42924
43186
|
}
|
|
42925
43187
|
|
|
42926
43188
|
// src/routes/changelog.ts
|
|
42927
|
-
var
|
|
43189
|
+
var view7 = (c) => ({
|
|
42928
43190
|
id: c.id,
|
|
42929
43191
|
projectId: c.projectId,
|
|
42930
43192
|
mode: c.mode,
|
|
@@ -42964,7 +43226,7 @@ async function changelog_default(app2) {
|
|
|
42964
43226
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
42965
43227
|
const { page, limit, q } = req.query;
|
|
42966
43228
|
const rows = await prisma.changelog.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
|
|
42967
|
-
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(
|
|
43229
|
+
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(view7), page, limit);
|
|
42968
43230
|
});
|
|
42969
43231
|
app2.get("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42970
43232
|
const { id, cid } = req.params;
|
|
@@ -42978,7 +43240,7 @@ async function changelog_default(app2) {
|
|
|
42978
43240
|
eyebrow: `hanoman \xB7 ${id} \xB7 changelog`,
|
|
42979
43241
|
path: row.title
|
|
42980
43242
|
});
|
|
42981
|
-
return
|
|
43243
|
+
return view7(row);
|
|
42982
43244
|
});
|
|
42983
43245
|
app2.post("/projects/:id/changelog", async (req, reply) => {
|
|
42984
43246
|
const { id } = req.params;
|
|
@@ -42987,7 +43249,7 @@ async function changelog_default(app2) {
|
|
|
42987
43249
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
42988
43250
|
const r = await generateChangelog(id, parsed.data);
|
|
42989
43251
|
if (!r.ok) return reply.code(422).send({ error: r.reason });
|
|
42990
|
-
return reply.code(201).send(
|
|
43252
|
+
return reply.code(201).send(view7(r.row));
|
|
42991
43253
|
});
|
|
42992
43254
|
app2.delete("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42993
43255
|
const { id, cid } = req.params;
|
|
@@ -43126,7 +43388,7 @@ async function installCustomAgents() {
|
|
|
43126
43388
|
|
|
43127
43389
|
// src/routes/custom-agents.ts
|
|
43128
43390
|
var rowsOf = async () => await prisma.customAgent.findMany();
|
|
43129
|
-
var
|
|
43391
|
+
var view8 = (r, projectId) => ({
|
|
43130
43392
|
id: r.id,
|
|
43131
43393
|
projectId: r.projectId,
|
|
43132
43394
|
name: r.name,
|
|
@@ -43171,7 +43433,7 @@ async function custom_agents_default(app2) {
|
|
|
43171
43433
|
const byName = /* @__PURE__ */ new Map();
|
|
43172
43434
|
for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
|
|
43173
43435
|
for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
|
|
43174
|
-
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));
|
|
43175
43437
|
});
|
|
43176
43438
|
app2.post("/custom-agents", async (req, reply) => {
|
|
43177
43439
|
const parsed = zCreateCustomAgent.safeParse(req.body);
|
|
@@ -43218,7 +43480,7 @@ async function custom_agents_default(app2) {
|
|
|
43218
43480
|
} });
|
|
43219
43481
|
await loadCustomAgents();
|
|
43220
43482
|
await notifySynced("customAgent", id);
|
|
43221
|
-
return reply.code(201).send(
|
|
43483
|
+
return reply.code(201).send(view8(row));
|
|
43222
43484
|
});
|
|
43223
43485
|
app2.patch("/custom-agents/:id", async (req, reply) => {
|
|
43224
43486
|
const { id } = req.params;
|
|
@@ -43265,13 +43527,13 @@ async function custom_agents_default(app2) {
|
|
|
43265
43527
|
} });
|
|
43266
43528
|
await loadCustomAgents();
|
|
43267
43529
|
await notifySynced("customAgent", id);
|
|
43268
|
-
return
|
|
43530
|
+
return view8(row);
|
|
43269
43531
|
});
|
|
43270
43532
|
app2.delete("/custom-agents/:id", async (req, reply) => {
|
|
43271
43533
|
const { id } = req.params;
|
|
43272
43534
|
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
43273
43535
|
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
43274
|
-
await
|
|
43536
|
+
await deleteSynced("customAgent", id);
|
|
43275
43537
|
const name2 = existing.name;
|
|
43276
43538
|
for (const r of await rowsOf()) {
|
|
43277
43539
|
const m = mentionsOf(r.mentions);
|
|
@@ -43606,7 +43868,7 @@ var STATUS = {
|
|
|
43606
43868
|
unauthorized: 401,
|
|
43607
43869
|
other: 502
|
|
43608
43870
|
};
|
|
43609
|
-
var
|
|
43871
|
+
var view9 = (i) => ({
|
|
43610
43872
|
id: i.id,
|
|
43611
43873
|
projectId: i.projectId,
|
|
43612
43874
|
repoSlug: i.repoSlug,
|
|
@@ -43641,7 +43903,7 @@ async function githubIssues(app2) {
|
|
|
43641
43903
|
where: { projectId: id, ...status ? { status } : {} },
|
|
43642
43904
|
orderBy: [{ number: "desc" }]
|
|
43643
43905
|
});
|
|
43644
|
-
return reply.send(paginate(items.map(
|
|
43906
|
+
return reply.send(paginate(items.map(view9), page, limit));
|
|
43645
43907
|
});
|
|
43646
43908
|
app2.post("/github-issues/accept", async (req, reply) => {
|
|
43647
43909
|
const parsed = zAcceptMany.safeParse(req.body ?? {});
|
|
@@ -44401,7 +44663,7 @@ async function portal_default(app2) {
|
|
|
44401
44663
|
// src/routes/client-accounts.ts
|
|
44402
44664
|
init_src();
|
|
44403
44665
|
init_db();
|
|
44404
|
-
var
|
|
44666
|
+
var view10 = (u) => ({
|
|
44405
44667
|
id: u.id,
|
|
44406
44668
|
email: u.email,
|
|
44407
44669
|
disabled: u.disabled,
|
|
@@ -44432,7 +44694,7 @@ async function client_accounts_default(app2) {
|
|
|
44432
44694
|
orderBy: { createdAt: "asc" },
|
|
44433
44695
|
include: { projectAccess: { select: { projectId: true } } }
|
|
44434
44696
|
});
|
|
44435
|
-
return { items: rows.map(
|
|
44697
|
+
return { items: rows.map(view10) };
|
|
44436
44698
|
});
|
|
44437
44699
|
app2.post("/client-accounts", async (req, reply) => {
|
|
44438
44700
|
const p = zCreateClientAccount.safeParse(req.body);
|
|
@@ -44446,7 +44708,7 @@ async function client_accounts_default(app2) {
|
|
|
44446
44708
|
await prisma.user.delete({ where: { id: user.id } });
|
|
44447
44709
|
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
44448
44710
|
}
|
|
44449
|
-
return reply.code(201).send(
|
|
44711
|
+
return reply.code(201).send(view10(await load(user.id)));
|
|
44450
44712
|
});
|
|
44451
44713
|
app2.patch("/client-accounts/:id", async (req, reply) => {
|
|
44452
44714
|
const { id } = req.params;
|
|
@@ -44460,7 +44722,7 @@ async function client_accounts_default(app2) {
|
|
|
44460
44722
|
if (p.data.password)
|
|
44461
44723
|
await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(p.data.password) } });
|
|
44462
44724
|
if (p.data.disabled || p.data.password) await deleteUserSessions(id);
|
|
44463
|
-
return
|
|
44725
|
+
return view10(await load(id));
|
|
44464
44726
|
});
|
|
44465
44727
|
app2.delete("/client-accounts/:id", async (req, reply) => {
|
|
44466
44728
|
const { id } = req.params;
|
|
@@ -44556,7 +44818,7 @@ async function consumeSetupToken(home3) {
|
|
|
44556
44818
|
|
|
44557
44819
|
// src/routes/auth.ts
|
|
44558
44820
|
var BOOTSTRAP_USER_ID = "bootstrap-admin";
|
|
44559
|
-
var
|
|
44821
|
+
var view11 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
44560
44822
|
async function issue(reply, userId) {
|
|
44561
44823
|
const token = await createSession2(userId);
|
|
44562
44824
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -44596,7 +44858,7 @@ async function auth_default(app2, opts) {
|
|
|
44596
44858
|
if (opts.bootstrapRequired) await consumeSetupToken(opts.home);
|
|
44597
44859
|
setupAttempts.clear(req.ip);
|
|
44598
44860
|
await issue(reply, user.id);
|
|
44599
|
-
return { user:
|
|
44861
|
+
return { user: view11(user) };
|
|
44600
44862
|
});
|
|
44601
44863
|
app2.post("/auth/login", async (req, reply) => {
|
|
44602
44864
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -44609,7 +44871,7 @@ async function auth_default(app2, opts) {
|
|
|
44609
44871
|
}
|
|
44610
44872
|
clearLoginFails(req.ip);
|
|
44611
44873
|
await issue(reply, user.id);
|
|
44612
|
-
return { user:
|
|
44874
|
+
return { user: view11(user) };
|
|
44613
44875
|
});
|
|
44614
44876
|
app2.post("/auth/logout", async (req, reply) => {
|
|
44615
44877
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -44617,7 +44879,7 @@ async function auth_default(app2, opts) {
|
|
|
44617
44879
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
44618
44880
|
return reply.code(204).send();
|
|
44619
44881
|
});
|
|
44620
|
-
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));
|
|
44621
44883
|
app2.post("/auth/users", async (req, reply) => {
|
|
44622
44884
|
const p = zSignup.safeParse(req.body);
|
|
44623
44885
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -44628,7 +44890,7 @@ async function auth_default(app2, opts) {
|
|
|
44628
44890
|
// (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
|
|
44629
44891
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "admin" }
|
|
44630
44892
|
});
|
|
44631
|
-
return
|
|
44893
|
+
return view11(user);
|
|
44632
44894
|
});
|
|
44633
44895
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
44634
44896
|
const target2 = await prisma.user.findUnique({ where: { id: req.params.id } });
|
|
@@ -44651,7 +44913,7 @@ async function auth_default(app2, opts) {
|
|
|
44651
44913
|
});
|
|
44652
44914
|
await deleteUserSessions(user.id);
|
|
44653
44915
|
await issue(reply, user.id);
|
|
44654
|
-
return { user:
|
|
44916
|
+
return { user: view11(user) };
|
|
44655
44917
|
});
|
|
44656
44918
|
}
|
|
44657
44919
|
|
|
@@ -44664,15 +44926,15 @@ async function agent_tokens_default(app2) {
|
|
|
44664
44926
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
44665
44927
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
44666
44928
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44667
|
-
const { view:
|
|
44668
|
-
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 });
|
|
44669
44931
|
});
|
|
44670
44932
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
44671
44933
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
44672
44934
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44673
44935
|
const { id } = req.params;
|
|
44674
|
-
const
|
|
44675
|
-
return
|
|
44936
|
+
const view12 = await patchAgentToken(id, p.data);
|
|
44937
|
+
return view12 ? reply.send(view12) : reply.code(404).send({ error: "not found" });
|
|
44676
44938
|
});
|
|
44677
44939
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
44678
44940
|
const { id } = req.params;
|
|
@@ -44971,7 +45233,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
44971
45233
|
if (PUBLIC.has(`${req.method} ${path2}`)) return;
|
|
44972
45234
|
if (user?.role === "client" && !clientRouteAllowed(req.method, path2))
|
|
44973
45235
|
return reply.code(403).send({ error: "portal klien: baca-saja" });
|
|
44974
|
-
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;
|
|
44975
45237
|
if (path2.startsWith("/api/help")) return;
|
|
44976
45238
|
if (user) return;
|
|
44977
45239
|
const agentTok = agentTokenFromReq(req);
|
|
@@ -45085,6 +45347,7 @@ init_src();
|
|
|
45085
45347
|
|
|
45086
45348
|
// src/services/scheduler/governor.ts
|
|
45087
45349
|
init_db();
|
|
45350
|
+
init_notifications();
|
|
45088
45351
|
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
45089
45352
|
var canceledRaceNote = (sessionId2) => `dibatalkan saat sesi ${sessionId2} sudah terlanjur lahir \u2014 sesi dibiarkan hidup, tutup dari Terminal bila tak diperlukan`;
|
|
45090
45353
|
var CAP_FULL_NOTE = "cap penuh \u2014 tak ada slot sesi";
|
|
@@ -45219,6 +45482,7 @@ async function startCronSession(cron) {
|
|
|
45219
45482
|
// src/services/scheduler/reconcile.ts
|
|
45220
45483
|
init_db();
|
|
45221
45484
|
init_src2();
|
|
45485
|
+
init_notifications();
|
|
45222
45486
|
init_stage_machine();
|
|
45223
45487
|
init_pty();
|
|
45224
45488
|
init_session_phases();
|
|
@@ -45282,6 +45546,7 @@ var reconcileProdDeps = {
|
|
|
45282
45546
|
};
|
|
45283
45547
|
|
|
45284
45548
|
// src/services/scheduler/engine.ts
|
|
45549
|
+
init_notifications();
|
|
45285
45550
|
init_pty();
|
|
45286
45551
|
var prodEnd = {
|
|
45287
45552
|
reconcile: () => reconcile(reconcileProdDeps),
|
|
@@ -45656,6 +45921,7 @@ init_src();
|
|
|
45656
45921
|
init_db();
|
|
45657
45922
|
import { execFile as execFile13 } from "node:child_process";
|
|
45658
45923
|
import { promisify as promisify12 } from "node:util";
|
|
45924
|
+
init_notifications();
|
|
45659
45925
|
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
45660
45926
|
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
45661
45927
|
var TICK_MS5 = 6e4;
|