hanoman 0.1.39 → 0.1.41
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 +25 -3
- package/dist/server.js +594 -241
- package/docs/agent-integration.md +5 -1
- package/package.json +1 -1
- package/prisma/migrations/20260815120000_sync_tombstone/migration.sql +14 -0
- package/prisma/migrations/20260815130000_spec_manual_done/migration.sql +9 -0
- package/prisma/schema.prisma +32 -0
- package/web/assets/{index-BzBNb9qp.js → index-BLt4wnhB.js} +1548 -1547
- package/web/assets/{index-DeGA9N3Z.css → index-BeFLNV7l.css} +1 -1
- package/web/index.html +2 -2
package/dist/server.js
CHANGED
|
@@ -4557,7 +4557,7 @@ function cmpVersion(a, b) {
|
|
|
4557
4557
|
}
|
|
4558
4558
|
return 0;
|
|
4559
4559
|
}
|
|
4560
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4560
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4561
4561
|
var init_entities = __esm({
|
|
4562
4562
|
"../shared/src/entities.ts"() {
|
|
4563
4563
|
"use strict";
|
|
@@ -4615,6 +4615,11 @@ var init_entities = __esm({
|
|
|
4615
4615
|
by: external_exports.string(),
|
|
4616
4616
|
payload: external_exports.unknown().optional()
|
|
4617
4617
|
});
|
|
4618
|
+
zManualDone = external_exports.object({
|
|
4619
|
+
at: external_exports.string(),
|
|
4620
|
+
by: external_exports.string(),
|
|
4621
|
+
reason: external_exports.string().optional()
|
|
4622
|
+
});
|
|
4618
4623
|
zSpec = external_exports.object({
|
|
4619
4624
|
id: external_exports.string(),
|
|
4620
4625
|
projectId: external_exports.string(),
|
|
@@ -4646,7 +4651,10 @@ var init_entities = __esm({
|
|
|
4646
4651
|
// SPEC-546 · ADR-0109 · jejak konversi type. `.default([])` menjaga respons/klien versi lama
|
|
4647
4652
|
// tetap parse; kolom DB-nya `Json?` sehingga baris yang belum pernah dikonversi mengirim
|
|
4648
4653
|
// `null` — pemakai UI menulis `spec.sourceHistory ?? []`, cermin `blockedBy`.
|
|
4649
|
-
sourceHistory: external_exports.array(zSourceChange).default([])
|
|
4654
|
+
sourceHistory: external_exports.array(zSourceChange).default([]),
|
|
4655
|
+
// SPEC-804 · ADR-0120 · terisi hanya bila item ditandai selesai manual; null = selesai lewat
|
|
4656
|
+
// sesi atau belum selesai. `.nullable().default(null)` menjaga respons/klien versi lama parse.
|
|
4657
|
+
manualDone: zManualDone.nullable().default(null)
|
|
4650
4658
|
});
|
|
4651
4659
|
NOTIFY_SOUNDS = [
|
|
4652
4660
|
"off",
|
|
@@ -5450,7 +5458,7 @@ var init_cron_expr = __esm({
|
|
|
5450
5458
|
function flowForSource(source) {
|
|
5451
5459
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
5452
5460
|
}
|
|
5453
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5461
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5454
5462
|
var init_dto = __esm({
|
|
5455
5463
|
"../shared/src/dto.ts"() {
|
|
5456
5464
|
"use strict";
|
|
@@ -5557,6 +5565,10 @@ var init_dto = __esm({
|
|
|
5557
5565
|
if (o.payload !== void 0 && !payloadMatchesSource(o.source, o.payload))
|
|
5558
5566
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
|
|
5559
5567
|
});
|
|
5568
|
+
zMarkSpecDone = external_exports.object({
|
|
5569
|
+
reason: external_exports.string().trim().max(280).optional(),
|
|
5570
|
+
confirm: external_exports.boolean().optional()
|
|
5571
|
+
});
|
|
5560
5572
|
zIntegrate = external_exports.object({
|
|
5561
5573
|
op: external_exports.enum(["merge", "rebase"]),
|
|
5562
5574
|
target: external_exports.string().regex(/^(local|origin):.+/)
|
|
@@ -6028,6 +6040,8 @@ var init_api = __esm({
|
|
|
6028
6040
|
specIntegrate: (id) => `${API}/specs/${id}/integrate`,
|
|
6029
6041
|
// SPEC-546 · ADR-0109 · ubah type/source item in-place (operasi khusus, bukan field PATCH).
|
|
6030
6042
|
specSource: (id) => `${API}/specs/${id}/source`,
|
|
6043
|
+
// SPEC-804 · ADR-0120 · tandai item selesai manual (operasi khusus, bukan field PATCH).
|
|
6044
|
+
specDone: (id) => `${API}/specs/${id}/done`,
|
|
6031
6045
|
specReview: (id) => `${API}/specs/${id}/review`,
|
|
6032
6046
|
specReviewFile: (id, path2) => `${API}/specs/${id}/review/${path2}`,
|
|
6033
6047
|
settings: `${API}/settings`,
|
|
@@ -6195,6 +6209,8 @@ var init_api = __esm({
|
|
|
6195
6209
|
syncNow: `${API}/sync/now`,
|
|
6196
6210
|
// SPEC-270 · ADR-0067 · antrean konflik rekonsil (cookie-authed)
|
|
6197
6211
|
syncConflicts: `${API}/sync/conflicts`,
|
|
6212
|
+
// SPEC-799 · ADR-0119 · penghapusan yang menunggu jendela online (cookie-only, cermin syncNow).
|
|
6213
|
+
syncPending: `${API}/sync/pending`,
|
|
6198
6214
|
syncConflictResolve: (entity, recordId) => `${API}/sync/conflicts/${encodeURIComponent(entity)}/${encodeURIComponent(recordId)}/resolve`,
|
|
6199
6215
|
// SPEC-257 · ADR-0065 · agent token (kelola cookie-only) + katalog capability
|
|
6200
6216
|
agentTokens: `${API}/agent-tokens`,
|
|
@@ -6747,6 +6763,9 @@ var init_webhook = __esm({
|
|
|
6747
6763
|
"autoMerge",
|
|
6748
6764
|
"createdAt",
|
|
6749
6765
|
"startedAt",
|
|
6766
|
+
// SPEC-804 · ADR-0120 · penerima harus bisa membedakan "selesai lewat sesi" dari "ditandai
|
|
6767
|
+
// manusia" tanpa mendiff dua amplop. Peristiwanya tetap `spec.stage_changed`.
|
|
6768
|
+
"manualDone",
|
|
6750
6769
|
"updatedAt"
|
|
6751
6770
|
],
|
|
6752
6771
|
projectIdField: "projectId",
|
|
@@ -6785,6 +6804,7 @@ var init_webhook = __esm({
|
|
|
6785
6804
|
autoMerge: null,
|
|
6786
6805
|
createdAt: "2026-08-01T02:10:00.000Z",
|
|
6787
6806
|
startedAt: "2026-08-01T02:12:31.000Z",
|
|
6807
|
+
manualDone: null,
|
|
6788
6808
|
updatedAt: "2026-08-01T09:41:22.000Z"
|
|
6789
6809
|
}
|
|
6790
6810
|
},
|
|
@@ -9736,8 +9756,8 @@ function assertRuntimeBoundary(env, runtime) {
|
|
|
9736
9756
|
if (runtime.uid === 0) throw new Error("production Hanoman harus berjalan sebagai user non-root");
|
|
9737
9757
|
if (env.HANOMAN_SESSION_SANDBOX !== "podman")
|
|
9738
9758
|
throw new Error("HANOMAN_SESSION_SANDBOX=podman wajib di production");
|
|
9739
|
-
if (!env.
|
|
9740
|
-
throw new Error("public/control origin wajib dipisahkan di production");
|
|
9759
|
+
if (!env.HANOMAN_CONTROL_ORIGINS || !env.HANOMAN_PUBLIC_ORIGINS && !env.HANOMAN_SINGLE_ORIGIN)
|
|
9760
|
+
throw new Error("public/control origin wajib dipisahkan di production (HANOMAN_SINGLE_ORIGIN=1 untuk sadar memilih satu origin)");
|
|
9741
9761
|
if (!env.HANOMAN_TRUST_PROXY) throw new Error("trusted proxy hop/CIDR wajib di production");
|
|
9742
9762
|
if (runtime.host !== "127.0.0.1" && runtime.host !== "::1" && runtime.host !== "localhost")
|
|
9743
9763
|
throw new Error("origin Hanoman harus bind loopback; buka hanya reverse proxy");
|
|
@@ -10487,16 +10507,55 @@ var init_rename_project = __esm({
|
|
|
10487
10507
|
}
|
|
10488
10508
|
});
|
|
10489
10509
|
|
|
10510
|
+
// src/services/tombstone.ts
|
|
10511
|
+
async function findTombstone(entity, recordId) {
|
|
10512
|
+
const row = await prisma.syncTombstone.findUnique({ where: { entity_recordId: { entity, recordId } } });
|
|
10513
|
+
return row ? view(row) : null;
|
|
10514
|
+
}
|
|
10515
|
+
async function writeTombstone(entity, recordId, version2, data, deviceId) {
|
|
10516
|
+
const prev = await findTombstone(entity, recordId);
|
|
10517
|
+
if (prev && prev.version >= version2) return prev;
|
|
10518
|
+
const row = await prisma.syncTombstone.upsert({
|
|
10519
|
+
where: { entity_recordId: { entity, recordId } },
|
|
10520
|
+
create: { entity, recordId, version: version2, data, deviceId: deviceId ?? null },
|
|
10521
|
+
update: { version: version2, data, deviceId: deviceId ?? null, deletedAt: /* @__PURE__ */ new Date() }
|
|
10522
|
+
});
|
|
10523
|
+
return view(row);
|
|
10524
|
+
}
|
|
10525
|
+
async function clearTombstone(entity, recordId) {
|
|
10526
|
+
await prisma.syncTombstone.deleteMany({ where: { entity, recordId } });
|
|
10527
|
+
}
|
|
10528
|
+
var view;
|
|
10529
|
+
var init_tombstone = __esm({
|
|
10530
|
+
"src/services/tombstone.ts"() {
|
|
10531
|
+
"use strict";
|
|
10532
|
+
init_db();
|
|
10533
|
+
view = (r) => ({
|
|
10534
|
+
entity: r.entity,
|
|
10535
|
+
recordId: r.recordId,
|
|
10536
|
+
version: r.version,
|
|
10537
|
+
data: r.data ?? {},
|
|
10538
|
+
deletedAt: r.deletedAt,
|
|
10539
|
+
deviceId: r.deviceId
|
|
10540
|
+
});
|
|
10541
|
+
}
|
|
10542
|
+
});
|
|
10543
|
+
|
|
10490
10544
|
// src/services/sync.ts
|
|
10491
10545
|
var sync_exports = {};
|
|
10492
10546
|
__export(sync_exports, {
|
|
10547
|
+
PARENTS: () => PARENTS,
|
|
10493
10548
|
SYNCED: () => SYNCED,
|
|
10494
10549
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
10495
10550
|
__FIELDS: () => __FIELDS,
|
|
10496
10551
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
10552
|
+
__JSON_FIELDS: () => __JSON_FIELDS,
|
|
10497
10553
|
applyPush: () => applyPush,
|
|
10498
10554
|
backfillFeed: () => backfillFeed,
|
|
10555
|
+
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
10556
|
+
deleteRow: () => deleteRow,
|
|
10499
10557
|
isEntity: () => isEntity,
|
|
10558
|
+
publishDelete: () => publishDelete,
|
|
10500
10559
|
publishLocal: () => publishLocal,
|
|
10501
10560
|
pull: () => pull,
|
|
10502
10561
|
setAcceptedHook: () => setAcceptedHook,
|
|
@@ -10561,10 +10620,24 @@ async function snapshot(entity, id) {
|
|
|
10561
10620
|
for (const f of FIELDS[entity]) data[f] = jsonSafe(row[f]);
|
|
10562
10621
|
return { version: Number(row.version), data };
|
|
10563
10622
|
}
|
|
10564
|
-
async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
10623
|
+
async function applyPush(entity, id, baseVersion, data, deviceId, op = "upsert") {
|
|
10565
10624
|
validateSyncData(entity, data, { allowProjectRename: true });
|
|
10625
|
+
if (op === "delete") {
|
|
10626
|
+
const already = await findTombstone(entity, id);
|
|
10627
|
+
if (already) return { ok: true, version: already.version };
|
|
10628
|
+
const snap2 = await snapshot(entity, id);
|
|
10629
|
+
const version2 = (snap2?.version ?? baseVersion) + 1;
|
|
10630
|
+
if (snap2) await DELEGATE[entity].delete({ where: { id } });
|
|
10631
|
+
await writeTombstone(entity, id, version2, snap2?.data ?? data, deviceId);
|
|
10632
|
+
await publishDelete(entity, id);
|
|
10633
|
+
return { ok: true, version: version2 };
|
|
10634
|
+
}
|
|
10566
10635
|
if (entity === "project" && typeof data.renamedFrom === "string" && data.renamedFrom && data.renamedFrom !== id) {
|
|
10567
10636
|
const oldId = data.renamedFrom;
|
|
10637
|
+
const destTomb = await findTombstone("project", id);
|
|
10638
|
+
if (destTomb) {
|
|
10639
|
+
return { ok: false, conflict: true, deleted: true, deletedVersion: destTomb.version, server: null };
|
|
10640
|
+
}
|
|
10568
10641
|
const already = await DELEGATE.project.findUnique({ where: { id }, select: { version: true } });
|
|
10569
10642
|
if (already) return { ok: true, version: Number(already.version) };
|
|
10570
10643
|
const old = await DELEGATE.project.findUnique({ where: { id: oldId }, select: { version: true } });
|
|
@@ -10578,17 +10651,24 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10578
10651
|
const snap2 = await snapshot("project", id);
|
|
10579
10652
|
const logData = { ...snap2?.data ?? {}, renamedFrom: oldId };
|
|
10580
10653
|
const log3 = await prisma.syncLog.create({
|
|
10581
|
-
data: { entity: "project", recordId: id, version: newVersion2, data: logData, deviceId: deviceId ?? null }
|
|
10654
|
+
data: { entity: "project", recordId: id, version: newVersion2, op: "upsert", data: logData, deviceId: deviceId ?? null }
|
|
10582
10655
|
});
|
|
10583
|
-
onAccepted?.({ entity: "project", recordId: id, version: newVersion2, data: logData, seq: String(log3.seq) });
|
|
10656
|
+
onAccepted?.({ entity: "project", recordId: id, version: newVersion2, op: "upsert", data: logData, seq: String(log3.seq) });
|
|
10584
10657
|
return { ok: true, version: newVersion2 };
|
|
10585
10658
|
}
|
|
10586
10659
|
}
|
|
10660
|
+
const tomb = await findTombstone(entity, id);
|
|
10587
10661
|
const existing = await DELEGATE[entity].findUnique({ where: { id }, select: { version: true } });
|
|
10588
|
-
|
|
10589
|
-
|
|
10662
|
+
const currentVersion = existing ? Number(existing.version) : tomb ? tomb.version : null;
|
|
10663
|
+
if (currentVersion !== null && currentVersion !== baseVersion) {
|
|
10664
|
+
return {
|
|
10665
|
+
ok: false,
|
|
10666
|
+
conflict: true,
|
|
10667
|
+
server: await snapshot(entity, id),
|
|
10668
|
+
...tomb && !existing ? { deleted: true, deletedVersion: tomb.version } : {}
|
|
10669
|
+
};
|
|
10590
10670
|
}
|
|
10591
|
-
const newVersion =
|
|
10671
|
+
const newVersion = (currentVersion ?? 0) + 1;
|
|
10592
10672
|
const writeData = coerce2(entity, data);
|
|
10593
10673
|
const stamp2 = writeData.updatedAt ?? /* @__PURE__ */ new Date();
|
|
10594
10674
|
await DELEGATE[entity].upsert({
|
|
@@ -10596,11 +10676,12 @@ async function applyPush(entity, id, baseVersion, data, deviceId) {
|
|
|
10596
10676
|
create: { id, ...writeData, version: newVersion, updatedAt: stamp2 },
|
|
10597
10677
|
update: { ...writeData, version: newVersion, updatedAt: stamp2 }
|
|
10598
10678
|
});
|
|
10679
|
+
if (tomb) await clearTombstone(entity, id);
|
|
10599
10680
|
const snap = await snapshot(entity, id);
|
|
10600
10681
|
const log2 = await prisma.syncLog.create({
|
|
10601
|
-
data: { entity, recordId: id, version: newVersion, data: snap?.data ?? {}, deviceId: deviceId ?? null }
|
|
10682
|
+
data: { entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, deviceId: deviceId ?? null }
|
|
10602
10683
|
});
|
|
10603
|
-
onAccepted?.({ entity, recordId: id, version: newVersion, data: snap?.data ?? {}, seq: String(log2.seq) });
|
|
10684
|
+
onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, seq: String(log2.seq) });
|
|
10604
10685
|
return { ok: true, version: newVersion };
|
|
10605
10686
|
}
|
|
10606
10687
|
async function pull(sinceCursor, limit = 500) {
|
|
@@ -10613,7 +10694,13 @@ async function pull(sinceCursor, limit = 500) {
|
|
|
10613
10694
|
const cursor = rows.length ? String(rows[rows.length - 1].seq) : sinceCursor || "0";
|
|
10614
10695
|
return {
|
|
10615
10696
|
cursor,
|
|
10616
|
-
records: rows.map((r) => ({
|
|
10697
|
+
records: rows.map((r) => ({
|
|
10698
|
+
entity: r.entity,
|
|
10699
|
+
recordId: r.recordId,
|
|
10700
|
+
version: r.version,
|
|
10701
|
+
op: r.op === "delete" ? "delete" : "upsert",
|
|
10702
|
+
data: r.data
|
|
10703
|
+
}))
|
|
10617
10704
|
};
|
|
10618
10705
|
}
|
|
10619
10706
|
async function publishLocal(entity, id) {
|
|
@@ -10622,9 +10709,38 @@ async function publishLocal(entity, id) {
|
|
|
10622
10709
|
const newVersion = snap.version + 1;
|
|
10623
10710
|
await DELEGATE[entity].update({ where: { id }, data: { version: newVersion } });
|
|
10624
10711
|
const log2 = await prisma.syncLog.create({
|
|
10625
|
-
data: { entity, recordId: id, version: newVersion, data: snap.data ?? {}, deviceId: null }
|
|
10712
|
+
data: { entity, recordId: id, version: newVersion, op: "upsert", data: snap.data ?? {}, deviceId: null }
|
|
10626
10713
|
});
|
|
10627
|
-
onAccepted?.({ entity, recordId: id, version: newVersion, data: snap.data ?? {}, seq: String(log2.seq) });
|
|
10714
|
+
onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap.data ?? {}, seq: String(log2.seq) });
|
|
10715
|
+
}
|
|
10716
|
+
async function deleteRow(entity, id) {
|
|
10717
|
+
await DELEGATE[entity].delete({ where: { id } });
|
|
10718
|
+
}
|
|
10719
|
+
async function publishDelete(entity, id) {
|
|
10720
|
+
const tomb = await findTombstone(entity, id);
|
|
10721
|
+
if (!tomb) return;
|
|
10722
|
+
const log2 = await prisma.syncLog.create({
|
|
10723
|
+
data: {
|
|
10724
|
+
entity,
|
|
10725
|
+
recordId: id,
|
|
10726
|
+
version: tomb.version,
|
|
10727
|
+
op: "delete",
|
|
10728
|
+
data: tomb.data,
|
|
10729
|
+
deviceId: tomb.deviceId
|
|
10730
|
+
}
|
|
10731
|
+
});
|
|
10732
|
+
onAccepted?.({ entity, recordId: id, version: tomb.version, op: "delete", data: tomb.data, seq: String(log2.seq) });
|
|
10733
|
+
}
|
|
10734
|
+
async function consumeTombstoneOnRecreate(entity, id) {
|
|
10735
|
+
const tomb = await findTombstone(entity, id);
|
|
10736
|
+
if (!tomb) return false;
|
|
10737
|
+
const row = await DELEGATE[entity].findUnique({ where: { id }, select: { version: true } });
|
|
10738
|
+
if (!row) return false;
|
|
10739
|
+
await clearTombstone(entity, id);
|
|
10740
|
+
if (Number(row.version) < tomb.version) {
|
|
10741
|
+
await DELEGATE[entity].update({ where: { id }, data: { version: tomb.version } });
|
|
10742
|
+
}
|
|
10743
|
+
return true;
|
|
10628
10744
|
}
|
|
10629
10745
|
async function backfillFeed() {
|
|
10630
10746
|
let published = 0;
|
|
@@ -10640,6 +10756,16 @@ async function backfillFeed() {
|
|
|
10640
10756
|
published++;
|
|
10641
10757
|
}
|
|
10642
10758
|
}
|
|
10759
|
+
for (const t of await prisma.syncTombstone.findMany({ select: { entity: true, recordId: true, version: true } })) {
|
|
10760
|
+
if (!isEntity(t.entity)) continue;
|
|
10761
|
+
const has = await prisma.syncLog.findFirst({
|
|
10762
|
+
where: { entity: t.entity, recordId: t.recordId, version: t.version, op: "delete" },
|
|
10763
|
+
select: { seq: true }
|
|
10764
|
+
});
|
|
10765
|
+
if (has) continue;
|
|
10766
|
+
await publishDelete(t.entity, t.recordId);
|
|
10767
|
+
published++;
|
|
10768
|
+
}
|
|
10643
10769
|
return published;
|
|
10644
10770
|
}
|
|
10645
10771
|
async function upsertLocal(entity, id, version2, data) {
|
|
@@ -10667,12 +10793,13 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
10667
10793
|
function setAcceptedHook(hook) {
|
|
10668
10794
|
onAccepted = hook;
|
|
10669
10795
|
}
|
|
10670
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10796
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10671
10797
|
var init_sync = __esm({
|
|
10672
10798
|
"src/services/sync.ts"() {
|
|
10673
10799
|
"use strict";
|
|
10674
10800
|
init_db();
|
|
10675
10801
|
init_rename_project();
|
|
10802
|
+
init_tombstone();
|
|
10676
10803
|
SYNCED = ["project", "spec", "vps", "sessionResult", "ticket", "ticketAttachment", "customAgent", "githubIssue"];
|
|
10677
10804
|
DELEGATE = {
|
|
10678
10805
|
project: prisma.project,
|
|
@@ -10698,7 +10825,10 @@ var init_sync = __esm({
|
|
|
10698
10825
|
// TETAP berhasil, jadi kolom yang terlewat di sini mendarat sebagai null palsu di tiap client
|
|
10699
10826
|
// tanpa satu pun error (kelas gagal-senyap ADR-0090/0093/0094/0105). BUKAN DATE_FIELDS —
|
|
10700
10827
|
// `at` hidup di dalam JSON-nya, kolomnya sendiri bukan DateTime.
|
|
10701
|
-
|
|
10828
|
+
// SPEC-804 · ADR-0120 · manualDone ikut menyeberang: "item ini ditandai selesai manusia" adalah
|
|
10829
|
+
// bagian keadaan yang harus dilihat sama oleh semua mesin — di antaranya gerbang auto-merge.
|
|
10830
|
+
// BUKAN DATE_FIELDS, alasan yang sama dengan sourceHistory.
|
|
10831
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "sourceHistory", "manualDone", "createdAt", "startedAt", "doneAt", "updatedAt"],
|
|
10702
10832
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
10703
10833
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
10704
10834
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
@@ -10746,6 +10876,13 @@ var init_sync = __esm({
|
|
|
10746
10876
|
customAgent: ["createdAt", "updatedAt"],
|
|
10747
10877
|
githubIssue: ["issueCreatedAt", "issueUpdatedAt", "pulledAt", "createdAt", "updatedAt"]
|
|
10748
10878
|
};
|
|
10879
|
+
PARENTS = {
|
|
10880
|
+
spec: [{ field: "projectId", entity: "project" }],
|
|
10881
|
+
ticket: [{ field: "projectId", entity: "project" }],
|
|
10882
|
+
ticketAttachment: [{ field: "ticketId", entity: "ticket" }],
|
|
10883
|
+
customAgent: [{ field: "projectId", entity: "project" }],
|
|
10884
|
+
githubIssue: [{ field: "projectId", entity: "project" }]
|
|
10885
|
+
};
|
|
10749
10886
|
__FIELDS = FIELDS;
|
|
10750
10887
|
__DATE_FIELDS = DATE_FIELDS;
|
|
10751
10888
|
NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -10759,12 +10896,14 @@ var init_sync = __esm({
|
|
|
10759
10896
|
"spec:payload",
|
|
10760
10897
|
"spec:dependsOn",
|
|
10761
10898
|
"spec:sourceHistory",
|
|
10899
|
+
"spec:manualDone",
|
|
10762
10900
|
"vps:health",
|
|
10763
10901
|
"vps:audit",
|
|
10764
10902
|
"customAgent:tools",
|
|
10765
10903
|
"customAgent:mentions",
|
|
10766
10904
|
"githubIssue:labels"
|
|
10767
10905
|
]);
|
|
10906
|
+
__JSON_FIELDS = JSON_FIELDS;
|
|
10768
10907
|
__FIELDS_FOR_TEST = FIELDS;
|
|
10769
10908
|
}
|
|
10770
10909
|
});
|
|
@@ -10888,6 +11027,134 @@ var init_codex_trust = __esm({
|
|
|
10888
11027
|
}
|
|
10889
11028
|
});
|
|
10890
11029
|
|
|
11030
|
+
// src/services/notifications.ts
|
|
11031
|
+
async function recordDrift(vpsId, vpsName, drift, snapshotId) {
|
|
11032
|
+
if (drift.length === 0) return;
|
|
11033
|
+
const ids = drift.map((d) => d.itemId);
|
|
11034
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
|
|
11035
|
+
const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
|
|
11036
|
+
await prisma.notification.create({
|
|
11037
|
+
data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
|
|
11038
|
+
}).catch(() => {
|
|
11039
|
+
});
|
|
11040
|
+
}
|
|
11041
|
+
async function recordCompletion(specId, title, projectId) {
|
|
11042
|
+
await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
|
|
11043
|
+
});
|
|
11044
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11045
|
+
await prisma.notification.create({
|
|
11046
|
+
data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
11047
|
+
}).catch(() => {
|
|
11048
|
+
});
|
|
11049
|
+
}
|
|
11050
|
+
async function recordSyncDelete(entity, recordId, version2, title) {
|
|
11051
|
+
await prisma.notification.create({
|
|
11052
|
+
data: { type: "sync", key: `sync-delete:${entity}:${recordId}:${version2}`, title, projectId: null }
|
|
11053
|
+
}).catch(() => {
|
|
11054
|
+
});
|
|
11055
|
+
}
|
|
11056
|
+
async function recordFailure(specId, title, projectId, reason) {
|
|
11057
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11058
|
+
await prisma.notification.create({
|
|
11059
|
+
data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
|
|
11060
|
+
}).catch(() => {
|
|
11061
|
+
});
|
|
11062
|
+
}
|
|
11063
|
+
async function recordAutoMerge(specId, projectId, title) {
|
|
11064
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11065
|
+
await prisma.notification.create({
|
|
11066
|
+
data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
|
|
11067
|
+
}).catch(() => {
|
|
11068
|
+
});
|
|
11069
|
+
}
|
|
11070
|
+
async function recordCleanupFailure(sessionId2, projectId, entry, reason) {
|
|
11071
|
+
await prisma.notification.create({
|
|
11072
|
+
data: {
|
|
11073
|
+
type: "cleanup",
|
|
11074
|
+
key: `cleanup:${entry}`,
|
|
11075
|
+
sessionId: sessionId2,
|
|
11076
|
+
projectId,
|
|
11077
|
+
title: `Worktree sesi ${sessionId2} gagal dibersihkan \u2014 ${reason}`
|
|
11078
|
+
}
|
|
11079
|
+
}).catch(() => {
|
|
11080
|
+
});
|
|
11081
|
+
}
|
|
11082
|
+
async function recordSourceChange(specId, projectId, title, from, to, seq) {
|
|
11083
|
+
const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
11084
|
+
await prisma.notification.create({
|
|
11085
|
+
data: {
|
|
11086
|
+
type: "spec-source",
|
|
11087
|
+
key: `source:${specId}:${seq}`,
|
|
11088
|
+
specId,
|
|
11089
|
+
sessionId: sessionId2,
|
|
11090
|
+
title: `${specId} \xB7 type ${from} \u2192 ${to} \u2014 ${title}`,
|
|
11091
|
+
projectId
|
|
11092
|
+
}
|
|
11093
|
+
}).catch(() => {
|
|
11094
|
+
});
|
|
11095
|
+
}
|
|
11096
|
+
async function recordNewTicket(ticketId, projectId, projectName, category, title) {
|
|
11097
|
+
const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
|
|
11098
|
+
const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
|
|
11099
|
+
await prisma.notification.create({
|
|
11100
|
+
data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
|
|
11101
|
+
}).catch(() => {
|
|
11102
|
+
});
|
|
11103
|
+
}
|
|
11104
|
+
async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
|
|
11105
|
+
await prisma.notification.create({
|
|
11106
|
+
data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
|
|
11107
|
+
}).catch(() => {
|
|
11108
|
+
});
|
|
11109
|
+
}
|
|
11110
|
+
async function recordCronRun(cronId, cronName, projectId, dueAt, status, note) {
|
|
11111
|
+
const verb = status === "launched" ? "berjalan" : status === "skipped" ? "dilewati" : "gagal";
|
|
11112
|
+
const title = `Cron "${cronName}" ${verb}${note ? ` \u2014 ${note}` : ""}`;
|
|
11113
|
+
await prisma.notification.create({
|
|
11114
|
+
data: { type: "cron", key: `cron:${cronId}:${dueAt.toISOString()}`, title, projectId }
|
|
11115
|
+
}).catch(() => {
|
|
11116
|
+
});
|
|
11117
|
+
}
|
|
11118
|
+
async function scanDecisions(read2 = liveDecisions) {
|
|
11119
|
+
const next = /* @__PURE__ */ new Set();
|
|
11120
|
+
const fresh = [];
|
|
11121
|
+
for (const s2 of read2()) {
|
|
11122
|
+
if (!markerFilled(s2.decisionFile)) continue;
|
|
11123
|
+
next.add(s2.id);
|
|
11124
|
+
if (!awaiting.has(s2.id)) fresh.push(s2);
|
|
11125
|
+
}
|
|
11126
|
+
awaiting = next;
|
|
11127
|
+
for (const s2 of fresh) {
|
|
11128
|
+
const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
|
|
11129
|
+
await prisma.notification.create({
|
|
11130
|
+
data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
|
|
11131
|
+
});
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
11134
|
+
async function notificationsFeed(p = {}) {
|
|
11135
|
+
await scanDecisions();
|
|
11136
|
+
const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
|
|
11137
|
+
const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
|
|
11138
|
+
const total = await prisma.notification.count();
|
|
11139
|
+
const items = await prisma.notification.findMany({
|
|
11140
|
+
orderBy: { createdAt: "desc" },
|
|
11141
|
+
skip: (page - 1) * pageSize,
|
|
11142
|
+
take: pageSize
|
|
11143
|
+
});
|
|
11144
|
+
const unread = await prisma.notification.count({ where: { readAt: null } });
|
|
11145
|
+
return { items, unread, total, page, pageSize };
|
|
11146
|
+
}
|
|
11147
|
+
var awaiting, DEFAULT_FEED_TAKE;
|
|
11148
|
+
var init_notifications = __esm({
|
|
11149
|
+
"src/services/notifications.ts"() {
|
|
11150
|
+
"use strict";
|
|
11151
|
+
init_db();
|
|
11152
|
+
init_pty();
|
|
11153
|
+
awaiting = /* @__PURE__ */ new Set();
|
|
11154
|
+
DEFAULT_FEED_TAKE = 50;
|
|
11155
|
+
}
|
|
11156
|
+
});
|
|
11157
|
+
|
|
10891
11158
|
// src/services/agent-token.ts
|
|
10892
11159
|
import { randomBytes as randomBytes2, createHash, timingSafeEqual } from "node:crypto";
|
|
10893
11160
|
function toAgentTokenView(t) {
|
|
@@ -16171,7 +16438,14 @@ function validateIncomingRecord(input) {
|
|
|
16171
16438
|
if (!row.data || typeof row.data !== "object" || Array.isArray(row.data)) throw new Error("sync data invalid");
|
|
16172
16439
|
if (Buffer.byteLength(JSON.stringify(input)) > MAX_SYNC_RECORD_BYTES) throw new Error("sync record terlalu besar");
|
|
16173
16440
|
validateSyncData(row.entity, row.data, { allowProjectRename: true });
|
|
16174
|
-
|
|
16441
|
+
const op = row.op === void 0 || row.op === null || row.op === "upsert" ? "upsert" : row.op === "delete" ? "delete" : null;
|
|
16442
|
+
return {
|
|
16443
|
+
entity: row.entity,
|
|
16444
|
+
recordId: row.recordId,
|
|
16445
|
+
version: Number(row.version),
|
|
16446
|
+
data: row.data,
|
|
16447
|
+
op
|
|
16448
|
+
};
|
|
16175
16449
|
}
|
|
16176
16450
|
async function getCursor() {
|
|
16177
16451
|
const s2 = await prisma.syncState.findUnique({ where: { id: 1 } });
|
|
@@ -16180,15 +16454,50 @@ async function getCursor() {
|
|
|
16180
16454
|
async function setCursor(cursor) {
|
|
16181
16455
|
await prisma.syncState.upsert({ where: { id: 1 }, create: { id: 1, cursor }, update: { cursor } });
|
|
16182
16456
|
}
|
|
16183
|
-
async function applyRemote(entity, recordId, version2, data) {
|
|
16184
|
-
if (!isEntity(entity)) return;
|
|
16457
|
+
async function applyRemote(entity, recordId, version2, data, op = "upsert") {
|
|
16458
|
+
if (!isEntity(entity)) return "dropped";
|
|
16459
|
+
if (op === "delete") {
|
|
16460
|
+
await applyRemoteDelete(entity, recordId, version2, data);
|
|
16461
|
+
return "applied";
|
|
16462
|
+
}
|
|
16463
|
+
const tomb = await findTombstone(entity, recordId);
|
|
16464
|
+
if (tomb) {
|
|
16465
|
+
if (version2 <= tomb.version) return "dropped";
|
|
16466
|
+
await clearTombstone(entity, recordId);
|
|
16467
|
+
}
|
|
16468
|
+
if (await parentTombstoned(entity, data)) return "dropped";
|
|
16185
16469
|
await upsertLocal(entity, recordId, version2, data);
|
|
16470
|
+
return "applied";
|
|
16471
|
+
}
|
|
16472
|
+
async function applyRemoteDelete(entity, recordId, version2, data) {
|
|
16473
|
+
const existing = await snapshot(entity, recordId);
|
|
16474
|
+
await writeTombstone(entity, recordId, version2, existing?.data ?? data);
|
|
16475
|
+
if (existing) await deleteRow(entity, recordId);
|
|
16476
|
+
const pending2 = await prisma.syncOutbox.findFirst({ where: { entity, recordId } });
|
|
16477
|
+
if (!pending2) return;
|
|
16478
|
+
await clearOutbox(entity, recordId);
|
|
16479
|
+
if (existing) {
|
|
16480
|
+
await recordSyncDelete(
|
|
16481
|
+
entity,
|
|
16482
|
+
recordId,
|
|
16483
|
+
version2,
|
|
16484
|
+
`Dihapus di peer: ${entity} ${recordId} \u2014 suntingan lokal yang belum tersinkron dibuang`
|
|
16485
|
+
);
|
|
16486
|
+
}
|
|
16487
|
+
}
|
|
16488
|
+
async function parentTombstoned(entity, data) {
|
|
16489
|
+
for (const p of PARENTS[entity] ?? []) {
|
|
16490
|
+
const v = data[p.field];
|
|
16491
|
+
if (typeof v !== "string" || !v) continue;
|
|
16492
|
+
if (await findTombstone(p.entity, v)) return true;
|
|
16493
|
+
}
|
|
16494
|
+
return false;
|
|
16186
16495
|
}
|
|
16187
16496
|
async function applyFeedFrame(msg) {
|
|
16188
16497
|
if (!msg.entity || !msg.recordId) return true;
|
|
16189
16498
|
try {
|
|
16190
16499
|
const record2 = validateIncomingRecord({ ...msg, version: Number(msg.version ?? 0), data: msg.data ?? {} });
|
|
16191
|
-
await applyRemote(record2.entity, record2.recordId, record2.version, record2.data);
|
|
16500
|
+
if (record2.op) await applyRemote(record2.entity, record2.recordId, record2.version, record2.data, record2.op);
|
|
16192
16501
|
} catch {
|
|
16193
16502
|
feedHole = true;
|
|
16194
16503
|
return false;
|
|
@@ -16197,7 +16506,7 @@ async function applyFeedFrame(msg) {
|
|
|
16197
16506
|
return true;
|
|
16198
16507
|
}
|
|
16199
16508
|
async function syncOnce(transport) {
|
|
16200
|
-
let pulled = 0, pushed = 0, conflicts = 0;
|
|
16509
|
+
let pulled = 0, pushed = 0, conflicts = 0, deleted = 0, dropped = 0;
|
|
16201
16510
|
const cursor = await getCursor();
|
|
16202
16511
|
const outbox = await listOutbox();
|
|
16203
16512
|
const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
|
|
@@ -16216,7 +16525,11 @@ async function syncOnce(transport) {
|
|
|
16216
16525
|
const deferred = [];
|
|
16217
16526
|
for (const rec of records) {
|
|
16218
16527
|
if (!isEntity(rec.entity)) continue;
|
|
16219
|
-
if (
|
|
16528
|
+
if (!rec.op) {
|
|
16529
|
+
dropped++;
|
|
16530
|
+
continue;
|
|
16531
|
+
}
|
|
16532
|
+
if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
|
|
16220
16533
|
const local = await snapshot(rec.entity, rec.recordId);
|
|
16221
16534
|
if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
|
|
16222
16535
|
await markConflict(
|
|
@@ -16229,8 +16542,10 @@ async function syncOnce(transport) {
|
|
|
16229
16542
|
continue;
|
|
16230
16543
|
}
|
|
16231
16544
|
try {
|
|
16232
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16233
|
-
|
|
16545
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
|
|
16546
|
+
if (r === "dropped") dropped++;
|
|
16547
|
+
else if (rec.op === "delete") deleted++;
|
|
16548
|
+
else pulled++;
|
|
16234
16549
|
} catch {
|
|
16235
16550
|
deferred.push(rec);
|
|
16236
16551
|
}
|
|
@@ -16240,15 +16555,18 @@ async function syncOnce(transport) {
|
|
|
16240
16555
|
const still = [];
|
|
16241
16556
|
for (const rec of rest) {
|
|
16242
16557
|
try {
|
|
16243
|
-
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data);
|
|
16244
|
-
|
|
16558
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op ?? "upsert");
|
|
16559
|
+
if (r === "dropped") dropped++;
|
|
16560
|
+
else if (rec.op === "delete") deleted++;
|
|
16561
|
+
else pulled++;
|
|
16245
16562
|
} catch {
|
|
16246
16563
|
still.push(rec);
|
|
16247
16564
|
}
|
|
16248
16565
|
}
|
|
16249
16566
|
if (still.length === rest.length) {
|
|
16250
16567
|
for (const rec of still) {
|
|
16251
|
-
console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan
|
|
16568
|
+
console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
|
|
16569
|
+
dropped++;
|
|
16252
16570
|
}
|
|
16253
16571
|
break;
|
|
16254
16572
|
}
|
|
@@ -16282,7 +16600,24 @@ async function syncOnce(transport) {
|
|
|
16282
16600
|
}
|
|
16283
16601
|
const snap = await snapshot(item.entity, item.recordId);
|
|
16284
16602
|
if (!snap) {
|
|
16285
|
-
await
|
|
16603
|
+
const tomb = await findTombstone(item.entity, item.recordId);
|
|
16604
|
+
if (!tomb) {
|
|
16605
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16606
|
+
continue;
|
|
16607
|
+
}
|
|
16608
|
+
const res2 = await transport("POST", "/api/sync/push", {
|
|
16609
|
+
records: [{
|
|
16610
|
+
entity: item.entity,
|
|
16611
|
+
id: item.recordId,
|
|
16612
|
+
baseVersion: Math.max(tomb.version - 1, 0),
|
|
16613
|
+
op: "delete",
|
|
16614
|
+
data: tomb.data
|
|
16615
|
+
}]
|
|
16616
|
+
});
|
|
16617
|
+
if (res2.body?.results?.[0]?.ok) {
|
|
16618
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16619
|
+
pushed++;
|
|
16620
|
+
}
|
|
16286
16621
|
continue;
|
|
16287
16622
|
}
|
|
16288
16623
|
const res = await transport("POST", "/api/sync/push", {
|
|
@@ -16298,6 +16633,18 @@ async function syncOnce(transport) {
|
|
|
16298
16633
|
await clearOutbox(item.entity, item.recordId);
|
|
16299
16634
|
pushed++;
|
|
16300
16635
|
} else if (r?.conflict) {
|
|
16636
|
+
if (r.deleted) {
|
|
16637
|
+
await applyRemote(
|
|
16638
|
+
item.entity,
|
|
16639
|
+
item.recordId,
|
|
16640
|
+
Number(r.deletedVersion ?? snap.version + 1),
|
|
16641
|
+
snap.data,
|
|
16642
|
+
"delete"
|
|
16643
|
+
);
|
|
16644
|
+
await clearOutbox(item.entity, item.recordId);
|
|
16645
|
+
deleted++;
|
|
16646
|
+
continue;
|
|
16647
|
+
}
|
|
16301
16648
|
const server = r.server;
|
|
16302
16649
|
if (server && JSON.stringify(server.data) !== JSON.stringify(snap.data)) {
|
|
16303
16650
|
await markConflict(
|
|
@@ -16312,7 +16659,7 @@ async function syncOnce(transport) {
|
|
|
16312
16659
|
}
|
|
16313
16660
|
}
|
|
16314
16661
|
}
|
|
16315
|
-
return { pulled, pushed, conflicts };
|
|
16662
|
+
return { pulled, pushed, conflicts, deleted, dropped };
|
|
16316
16663
|
}
|
|
16317
16664
|
function fetchTransport(base2, token) {
|
|
16318
16665
|
return async (method, path2, body) => {
|
|
@@ -16343,13 +16690,15 @@ async function syncNow(opts) {
|
|
|
16343
16690
|
const transport = fetchTransport(base2, token);
|
|
16344
16691
|
if (!opts?.full) return syncOnce(transport);
|
|
16345
16692
|
await setCursor("0");
|
|
16346
|
-
const total = { pulled: 0, pushed: 0, conflicts: 0 };
|
|
16693
|
+
const total = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
|
|
16347
16694
|
let seen = "0";
|
|
16348
16695
|
for (let page = 0; page < FULL_PULL_MAX_PAGES; page++) {
|
|
16349
16696
|
const s2 = await syncOnce(transport);
|
|
16350
16697
|
total.pulled += s2.pulled;
|
|
16351
16698
|
total.pushed += s2.pushed;
|
|
16352
16699
|
total.conflicts += s2.conflicts;
|
|
16700
|
+
total.deleted += s2.deleted;
|
|
16701
|
+
total.dropped += s2.dropped;
|
|
16353
16702
|
const now = await getCursor();
|
|
16354
16703
|
if (now === seen) break;
|
|
16355
16704
|
seen = now;
|
|
@@ -16432,6 +16781,8 @@ var init_sync_client = __esm({
|
|
|
16432
16781
|
"use strict";
|
|
16433
16782
|
init_db();
|
|
16434
16783
|
init_sync();
|
|
16784
|
+
init_tombstone();
|
|
16785
|
+
init_notifications();
|
|
16435
16786
|
init_conflicts();
|
|
16436
16787
|
init_outbox();
|
|
16437
16788
|
init_rename_project();
|
|
@@ -24804,11 +25155,42 @@ init_sync();
|
|
|
24804
25155
|
async function notifySynced(entity, id) {
|
|
24805
25156
|
try {
|
|
24806
25157
|
if (!isEntity(entity)) return;
|
|
25158
|
+
await consumeTombstoneOnRecreate(entity, id);
|
|
24807
25159
|
if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
|
|
24808
25160
|
else await publishLocal(entity, id);
|
|
24809
25161
|
} catch {
|
|
24810
25162
|
}
|
|
24811
25163
|
}
|
|
25164
|
+
async function notifyDeleted(entity, id) {
|
|
25165
|
+
try {
|
|
25166
|
+
if (!isEntity(entity)) return;
|
|
25167
|
+
if (effectiveStr("SYNC_SERVER_URL")) await enqueueOutbox(entity, id);
|
|
25168
|
+
else await publishDelete(entity, id);
|
|
25169
|
+
} catch {
|
|
25170
|
+
}
|
|
25171
|
+
}
|
|
25172
|
+
|
|
25173
|
+
// src/services/sync-delete.ts
|
|
25174
|
+
init_sync();
|
|
25175
|
+
init_tombstone();
|
|
25176
|
+
init_outbox();
|
|
25177
|
+
async function deleteSynced(entity, id, deviceId) {
|
|
25178
|
+
const snap = await snapshot(entity, id);
|
|
25179
|
+
if (!snap) return false;
|
|
25180
|
+
await deleteRow(entity, id);
|
|
25181
|
+
await writeTombstone(entity, id, snap.version + 1, snap.data, deviceId);
|
|
25182
|
+
await notifyDeleted(entity, id);
|
|
25183
|
+
return true;
|
|
25184
|
+
}
|
|
25185
|
+
async function listPendingDeletes() {
|
|
25186
|
+
const out3 = [];
|
|
25187
|
+
for (const item of await listOutbox()) {
|
|
25188
|
+
const tomb = await findTombstone(item.entity, item.recordId);
|
|
25189
|
+
if (!tomb) continue;
|
|
25190
|
+
out3.push({ entity: item.entity, recordId: item.recordId, deletedAt: tomb.deletedAt.toISOString() });
|
|
25191
|
+
}
|
|
25192
|
+
return out3;
|
|
25193
|
+
}
|
|
24812
25194
|
|
|
24813
25195
|
// src/services/branches.ts
|
|
24814
25196
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -24962,7 +25344,7 @@ async function projects_default(app2) {
|
|
|
24962
25344
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
24963
25345
|
const active = listSessions().filter((s2) => s2.projectId === id && !s2.exited).length;
|
|
24964
25346
|
if (active) return reply.code(409).send({ error: `project "${id}" masih punya ${active} sesi aktif` });
|
|
24965
|
-
await
|
|
25347
|
+
await deleteSynced("project", id);
|
|
24966
25348
|
return reply.code(204).send();
|
|
24967
25349
|
});
|
|
24968
25350
|
app2.get("/projects/:id/branches", async (req, reply) => {
|
|
@@ -25413,120 +25795,64 @@ function appendSourceHistory(current, entry) {
|
|
|
25413
25795
|
return [...Array.isArray(current) ? current : [], entry];
|
|
25414
25796
|
}
|
|
25415
25797
|
|
|
25416
|
-
// src/
|
|
25798
|
+
// src/routes/specs.ts
|
|
25799
|
+
init_notifications();
|
|
25800
|
+
|
|
25801
|
+
// src/services/spec-complete.ts
|
|
25417
25802
|
init_db();
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25424
|
-
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
}
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
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
|
-
});
|
|
25803
|
+
init_notifications();
|
|
25804
|
+
|
|
25805
|
+
// src/services/session-result.ts
|
|
25806
|
+
init_db();
|
|
25807
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
25808
|
+
var WHITELIST = [
|
|
25809
|
+
"projectId",
|
|
25810
|
+
"specId",
|
|
25811
|
+
"oldStage",
|
|
25812
|
+
"newStage",
|
|
25813
|
+
"commitSha",
|
|
25814
|
+
"branch",
|
|
25815
|
+
"prUrl",
|
|
25816
|
+
"status",
|
|
25817
|
+
"deviceId",
|
|
25818
|
+
"author"
|
|
25819
|
+
];
|
|
25820
|
+
async function recordSessionResult(input) {
|
|
25821
|
+
const id = randomUUID3();
|
|
25822
|
+
const data = { id };
|
|
25823
|
+
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25824
|
+
if (data.status === void 0) data.status = "done";
|
|
25825
|
+
if (data.projectId === void 0) throw new Error("session result butuh projectId");
|
|
25826
|
+
await prisma.sessionResult.create({ data });
|
|
25827
|
+
await notifySynced("sessionResult", id);
|
|
25828
|
+
return { id };
|
|
25485
25829
|
}
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
|
|
25489
|
-
|
|
25830
|
+
|
|
25831
|
+
// src/services/spec-complete.ts
|
|
25832
|
+
async function completeSpecManually(spec, input) {
|
|
25833
|
+
const at = input.at ?? /* @__PURE__ */ new Date();
|
|
25834
|
+
const manualDone = {
|
|
25835
|
+
at: at.toISOString(),
|
|
25836
|
+
by: input.by,
|
|
25837
|
+
...input.reason ? { reason: input.reason } : {}
|
|
25838
|
+
};
|
|
25839
|
+
const { count } = await prisma.spec.updateMany({
|
|
25840
|
+
where: { id: spec.id, stage: { not: "done" } },
|
|
25841
|
+
data: { stage: "done", manualDone }
|
|
25490
25842
|
});
|
|
25491
|
-
}
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25843
|
+
if (count === 0) return { ok: false };
|
|
25844
|
+
await recordCompletion(spec.id, spec.title, spec.projectId);
|
|
25845
|
+
await recordSessionResult({
|
|
25846
|
+
projectId: spec.projectId,
|
|
25847
|
+
specId: spec.id,
|
|
25848
|
+
oldStage: spec.stage,
|
|
25849
|
+
newStage: "done",
|
|
25850
|
+
status: "done",
|
|
25851
|
+
author: input.by
|
|
25497
25852
|
}).catch(() => {
|
|
25498
25853
|
});
|
|
25499
|
-
|
|
25500
|
-
|
|
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 };
|
|
25854
|
+
await notifySynced("spec", spec.id);
|
|
25855
|
+
return { ok: true, spec: await prisma.spec.findUnique({ where: { id: spec.id } }) };
|
|
25530
25856
|
}
|
|
25531
25857
|
|
|
25532
25858
|
// src/routes/specs.ts
|
|
@@ -28187,6 +28513,7 @@ init_db();
|
|
|
28187
28513
|
init_pty();
|
|
28188
28514
|
init_session_phases();
|
|
28189
28515
|
init_stage_machine();
|
|
28516
|
+
init_notifications();
|
|
28190
28517
|
|
|
28191
28518
|
// src/services/spec-head.ts
|
|
28192
28519
|
init_db();
|
|
@@ -28449,6 +28776,23 @@ ${item.context}` : item.context;
|
|
|
28449
28776
|
await notifySynced("spec", id);
|
|
28450
28777
|
return updated;
|
|
28451
28778
|
});
|
|
28779
|
+
app2.post("/specs/:id/done", async (req, reply) => {
|
|
28780
|
+
const { id } = req.params;
|
|
28781
|
+
const parsed = zMarkSpecDone.safeParse(req.body ?? {});
|
|
28782
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
28783
|
+
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
28784
|
+
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
28785
|
+
if (spec.stage === "done") return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28786
|
+
const live = listSessions().find((s2) => s2.specId === id && !s2.exited);
|
|
28787
|
+
if (live && parsed.data.confirm !== true)
|
|
28788
|
+
return reply.code(409).send({ error: "confirm-required", session: { id: live.id, agent: live.agent } });
|
|
28789
|
+
const res = await completeSpecManually(spec, {
|
|
28790
|
+
by: req.user?.email ?? "system",
|
|
28791
|
+
reason: parsed.data.reason || void 0
|
|
28792
|
+
});
|
|
28793
|
+
if (!res.ok) return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28794
|
+
return res.spec;
|
|
28795
|
+
});
|
|
28452
28796
|
app2.get("/specs/:id/docs", async (req) => ({ files: await listSpecDocs(req.params.id) }));
|
|
28453
28797
|
app2.get("/specs/:id/docs/*", async (req, reply) => {
|
|
28454
28798
|
const { id } = req.params;
|
|
@@ -28469,7 +28813,7 @@ ${item.context}` : item.context;
|
|
|
28469
28813
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
28470
28814
|
const { id } = req.params;
|
|
28471
28815
|
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
28472
|
-
await
|
|
28816
|
+
await deleteSynced("spec", id).catch(() => {
|
|
28473
28817
|
});
|
|
28474
28818
|
if (gone) {
|
|
28475
28819
|
const rows = await prisma.spec.findMany({
|
|
@@ -28555,6 +28899,7 @@ ${item.context}` : item.context;
|
|
|
28555
28899
|
|
|
28556
28900
|
// src/routes/notifications.ts
|
|
28557
28901
|
init_db();
|
|
28902
|
+
init_notifications();
|
|
28558
28903
|
async function notifications_default(app2) {
|
|
28559
28904
|
app2.get("/notifications", async (req) => notificationsFeed(req.query));
|
|
28560
28905
|
app2.post("/notifications/read", async (_req, reply) => {
|
|
@@ -30176,6 +30521,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
30176
30521
|
import { readdir as readdir2, rm as rm2 } from "node:fs/promises";
|
|
30177
30522
|
import { promisify as promisify9 } from "node:util";
|
|
30178
30523
|
import { join as join9, resolve as resolve14 } from "node:path";
|
|
30524
|
+
init_notifications();
|
|
30179
30525
|
var TICK_MS = 6e4;
|
|
30180
30526
|
var trashDirOf = (repoDir) => resolve14(repoDir, ".worktrees", ".trash");
|
|
30181
30527
|
var exec8 = promisify9(execFile8);
|
|
@@ -30274,33 +30620,8 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30274
30620
|
timer.unref();
|
|
30275
30621
|
}
|
|
30276
30622
|
|
|
30277
|
-
// src/services/session-result.ts
|
|
30278
|
-
init_db();
|
|
30279
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
30280
|
-
var WHITELIST = [
|
|
30281
|
-
"projectId",
|
|
30282
|
-
"specId",
|
|
30283
|
-
"oldStage",
|
|
30284
|
-
"newStage",
|
|
30285
|
-
"commitSha",
|
|
30286
|
-
"branch",
|
|
30287
|
-
"prUrl",
|
|
30288
|
-
"status",
|
|
30289
|
-
"deviceId",
|
|
30290
|
-
"author"
|
|
30291
|
-
];
|
|
30292
|
-
async function recordSessionResult(input) {
|
|
30293
|
-
const id = randomUUID3();
|
|
30294
|
-
const data = { id };
|
|
30295
|
-
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
30296
|
-
if (data.status === void 0) data.status = "done";
|
|
30297
|
-
if (data.projectId === void 0) throw new Error("session result butuh projectId");
|
|
30298
|
-
await prisma.sessionResult.create({ data });
|
|
30299
|
-
await notifySynced("sessionResult", id);
|
|
30300
|
-
return { id };
|
|
30301
|
-
}
|
|
30302
|
-
|
|
30303
30623
|
// src/routes/terminal.ts
|
|
30624
|
+
init_notifications();
|
|
30304
30625
|
init_stage_machine();
|
|
30305
30626
|
init_pty();
|
|
30306
30627
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
@@ -31011,6 +31332,7 @@ function computeDrift(prev, curr) {
|
|
|
31011
31332
|
}
|
|
31012
31333
|
|
|
31013
31334
|
// src/services/vps-audit.ts
|
|
31335
|
+
init_notifications();
|
|
31014
31336
|
var CRITICAL = [
|
|
31015
31337
|
"sudo_ok",
|
|
31016
31338
|
"os_supported",
|
|
@@ -31274,12 +31596,9 @@ async function vps_default(app2) {
|
|
|
31274
31596
|
return updated;
|
|
31275
31597
|
});
|
|
31276
31598
|
app2.delete("/vps/:id", async (req, reply) => {
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
} catch {
|
|
31281
|
-
return reply.code(404).send({ error: "not found" });
|
|
31282
|
-
}
|
|
31599
|
+
const { id } = req.params;
|
|
31600
|
+
if (!await deleteSynced("vps", id)) return reply.code(404).send({ error: "not found" });
|
|
31601
|
+
return reply.code(204).send();
|
|
31283
31602
|
});
|
|
31284
31603
|
app2.post("/vps/:id/audit", async (req, reply) => {
|
|
31285
31604
|
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
@@ -31807,6 +32126,7 @@ async function update(app2) {
|
|
|
31807
32126
|
|
|
31808
32127
|
// src/services/events.ts
|
|
31809
32128
|
init_pty();
|
|
32129
|
+
init_notifications();
|
|
31810
32130
|
|
|
31811
32131
|
// src/services/lead/deciding.ts
|
|
31812
32132
|
var deciding = /* @__PURE__ */ new Set();
|
|
@@ -31975,7 +32295,7 @@ async function events_default(app2, opts) {
|
|
|
31975
32295
|
// src/routes/device-tokens.ts
|
|
31976
32296
|
init_src();
|
|
31977
32297
|
init_db();
|
|
31978
|
-
var
|
|
32298
|
+
var view2 = (t) => ({
|
|
31979
32299
|
id: t.id,
|
|
31980
32300
|
name: t.name,
|
|
31981
32301
|
createdAt: t.createdAt.toISOString(),
|
|
@@ -31983,7 +32303,7 @@ var view = (t) => ({
|
|
|
31983
32303
|
revokedAt: t.revokedAt?.toISOString() ?? null
|
|
31984
32304
|
});
|
|
31985
32305
|
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(
|
|
32306
|
+
app2.get("/device-tokens", async (req) => (await prisma.deviceToken.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" } })).map(view2));
|
|
31987
32307
|
app2.post("/device-tokens", async (req, reply) => {
|
|
31988
32308
|
const p = zIssueDeviceToken.safeParse(req.body);
|
|
31989
32309
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -32133,7 +32453,10 @@ var zPush = external_exports.object({
|
|
|
32133
32453
|
entity: external_exports.string(),
|
|
32134
32454
|
id: external_exports.string(),
|
|
32135
32455
|
baseVersion: external_exports.number().int().nonnegative(),
|
|
32136
|
-
data: external_exports.record(external_exports.unknown())
|
|
32456
|
+
data: external_exports.record(external_exports.unknown()),
|
|
32457
|
+
// SPEC-799 · ADR-0119 · absen = "upsert" (client versi lama). Hub versi lama membuang field ini
|
|
32458
|
+
// dan sekadar memperlakukan push delete sebagai update — status quo, bukan galat.
|
|
32459
|
+
op: external_exports.enum(["upsert", "delete"]).optional()
|
|
32137
32460
|
}))
|
|
32138
32461
|
});
|
|
32139
32462
|
var AUTHORED = ["spec", "sessionResult"];
|
|
@@ -32163,7 +32486,7 @@ async function sync_default(app2) {
|
|
|
32163
32486
|
}
|
|
32164
32487
|
const data = { ...rec.data };
|
|
32165
32488
|
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);
|
|
32489
|
+
const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id, rec.op ?? "upsert");
|
|
32167
32490
|
results.push({ id: rec.id, ...r });
|
|
32168
32491
|
}
|
|
32169
32492
|
return { results };
|
|
@@ -32175,6 +32498,10 @@ async function sync_default(app2) {
|
|
|
32175
32498
|
return { ok: true, full, ...stats2 };
|
|
32176
32499
|
});
|
|
32177
32500
|
app2.get("/sync/conflicts", async () => ({ conflicts: await listConflicts() }));
|
|
32501
|
+
app2.get("/sync/pending", async () => {
|
|
32502
|
+
const deletes = await listPendingDeletes();
|
|
32503
|
+
return { deletes, total: deletes.length };
|
|
32504
|
+
});
|
|
32178
32505
|
const zResolve = external_exports.object({ choice: external_exports.enum(["local", "server"]) });
|
|
32179
32506
|
app2.post("/sync/conflicts/:entity/:recordId/resolve", async (req, reply) => {
|
|
32180
32507
|
const { entity, recordId } = req.params;
|
|
@@ -32225,7 +32552,7 @@ async function sync_default(app2) {
|
|
|
32225
32552
|
|
|
32226
32553
|
// src/routes/session-results.ts
|
|
32227
32554
|
init_db();
|
|
32228
|
-
var
|
|
32555
|
+
var view3 = (r) => ({
|
|
32229
32556
|
id: r.id,
|
|
32230
32557
|
projectId: r.projectId,
|
|
32231
32558
|
specId: r.specId,
|
|
@@ -32248,7 +32575,7 @@ async function session_results_default(app2) {
|
|
|
32248
32575
|
orderBy: { createdAt: "desc" },
|
|
32249
32576
|
take: take2
|
|
32250
32577
|
});
|
|
32251
|
-
return rows.map(
|
|
32578
|
+
return rows.map(view3);
|
|
32252
32579
|
});
|
|
32253
32580
|
app2.delete("/session-results", async (req, reply) => {
|
|
32254
32581
|
const { projectId, before: before2 } = req.query;
|
|
@@ -32260,8 +32587,10 @@ async function session_results_default(app2) {
|
|
|
32260
32587
|
if (Number.isNaN(d.getTime())) return reply.code(400).send({ error: "before bukan tanggal valid" });
|
|
32261
32588
|
where.createdAt = { lt: d };
|
|
32262
32589
|
}
|
|
32263
|
-
const
|
|
32264
|
-
|
|
32590
|
+
const rows = await prisma.sessionResult.findMany({ where, select: { id: true } });
|
|
32591
|
+
let purged = 0;
|
|
32592
|
+
for (const r of rows) if (await deleteSynced("sessionResult", r.id)) purged++;
|
|
32593
|
+
return { purged };
|
|
32265
32594
|
});
|
|
32266
32595
|
}
|
|
32267
32596
|
|
|
@@ -32314,7 +32643,7 @@ async function deleteTranscript(key) {
|
|
|
32314
32643
|
}
|
|
32315
32644
|
|
|
32316
32645
|
// src/services/session-history.ts
|
|
32317
|
-
var
|
|
32646
|
+
var view4 = (r) => ({
|
|
32318
32647
|
id: r.id,
|
|
32319
32648
|
sessionId: r.sessionId,
|
|
32320
32649
|
projectId: r.projectId,
|
|
@@ -32398,11 +32727,11 @@ async function listHistory(q) {
|
|
|
32398
32727
|
skip: (page - 1) * pageSize,
|
|
32399
32728
|
take: pageSize
|
|
32400
32729
|
});
|
|
32401
|
-
return { items: rows.map(
|
|
32730
|
+
return { items: rows.map(view4), total, page, pageSize };
|
|
32402
32731
|
}
|
|
32403
32732
|
async function getHistory(id) {
|
|
32404
32733
|
const r = await prisma.sessionHistory.findUnique({ where: { id } });
|
|
32405
|
-
return r ? { ...
|
|
32734
|
+
return r ? { ...view4(r), hasTranscript: !!r.transcriptKey } : null;
|
|
32406
32735
|
}
|
|
32407
32736
|
async function transcriptOf(id) {
|
|
32408
32737
|
const r = await prisma.sessionHistory.findUnique({ where: { id }, select: { transcriptKey: true } });
|
|
@@ -32478,7 +32807,7 @@ init_config_apply();
|
|
|
32478
32807
|
init_sync_client();
|
|
32479
32808
|
var isSecret2 = (e) => e.kind === "secret";
|
|
32480
32809
|
var agentBlocked = (req, e) => Boolean(req.agent) && e.category === "credential";
|
|
32481
|
-
function
|
|
32810
|
+
function view5(e) {
|
|
32482
32811
|
const eff = effectiveStr(e.key);
|
|
32483
32812
|
const base2 = {
|
|
32484
32813
|
key: e.key,
|
|
@@ -32498,7 +32827,7 @@ function view4(e) {
|
|
|
32498
32827
|
}
|
|
32499
32828
|
async function config_default(app2) {
|
|
32500
32829
|
app2.get("/config", async () => ({
|
|
32501
|
-
entries: CONFIG_REGISTRY.map(
|
|
32830
|
+
entries: CONFIG_REGISTRY.map(view5),
|
|
32502
32831
|
sync: syncStatus()
|
|
32503
32832
|
}));
|
|
32504
32833
|
app2.put("/config", async (req, reply) => {
|
|
@@ -32510,7 +32839,7 @@ async function config_default(app2) {
|
|
|
32510
32839
|
const raw2 = b.value ?? "";
|
|
32511
32840
|
if (isSecret2(entry) && raw2.trim() === "") {
|
|
32512
32841
|
if (rawDbValue(entry.key) === void 0) return reply.code(400).send({ error: "tak boleh kosong" });
|
|
32513
|
-
return
|
|
32842
|
+
return view5(entry);
|
|
32514
32843
|
}
|
|
32515
32844
|
const parsed = parseConfigValue(entry, raw2);
|
|
32516
32845
|
if (!parsed.ok) return reply.code(400).send({ error: parsed.error });
|
|
@@ -32520,11 +32849,11 @@ async function config_default(app2) {
|
|
|
32520
32849
|
} catch (error) {
|
|
32521
32850
|
return reply.code(400).send({ error: error.message });
|
|
32522
32851
|
}
|
|
32523
|
-
return
|
|
32852
|
+
return view5(entry);
|
|
32524
32853
|
}
|
|
32525
32854
|
await setConfig(entry.key, parsed.value);
|
|
32526
32855
|
await applyConfigSideEffect(entry.key);
|
|
32527
|
-
return
|
|
32856
|
+
return view5(entry);
|
|
32528
32857
|
});
|
|
32529
32858
|
app2.delete("/config/:key", async (req, reply) => {
|
|
32530
32859
|
const { key } = req.params;
|
|
@@ -32590,6 +32919,7 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
32590
32919
|
|
|
32591
32920
|
// src/services/ticket-intake.ts
|
|
32592
32921
|
init_db();
|
|
32922
|
+
init_notifications();
|
|
32593
32923
|
|
|
32594
32924
|
// src/services/upload-pipeline.ts
|
|
32595
32925
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -33823,25 +34153,25 @@ var cachedDecoders = {
|
|
|
33823
34153
|
};
|
|
33824
34154
|
var cachedEncoder = new globalThis.TextEncoder();
|
|
33825
34155
|
var byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
|
|
33826
|
-
function getUintBE(
|
|
33827
|
-
const { byteLength } =
|
|
34156
|
+
function getUintBE(view12) {
|
|
34157
|
+
const { byteLength } = view12;
|
|
33828
34158
|
if (byteLength === 6) {
|
|
33829
|
-
return
|
|
34159
|
+
return view12.getUint16(0) * 2 ** 32 + view12.getUint32(2);
|
|
33830
34160
|
}
|
|
33831
34161
|
if (byteLength === 5) {
|
|
33832
|
-
return
|
|
34162
|
+
return view12.getUint8(0) * 2 ** 32 + view12.getUint32(1);
|
|
33833
34163
|
}
|
|
33834
34164
|
if (byteLength === 4) {
|
|
33835
|
-
return
|
|
34165
|
+
return view12.getUint32(0);
|
|
33836
34166
|
}
|
|
33837
34167
|
if (byteLength === 3) {
|
|
33838
|
-
return
|
|
34168
|
+
return view12.getUint8(0) * 2 ** 16 + view12.getUint16(1);
|
|
33839
34169
|
}
|
|
33840
34170
|
if (byteLength === 2) {
|
|
33841
|
-
return
|
|
34171
|
+
return view12.getUint16(0);
|
|
33842
34172
|
}
|
|
33843
34173
|
if (byteLength === 1) {
|
|
33844
|
-
return
|
|
34174
|
+
return view12.getUint8(0);
|
|
33845
34175
|
}
|
|
33846
34176
|
}
|
|
33847
34177
|
|
|
@@ -40231,9 +40561,9 @@ async function help_default(app2) {
|
|
|
40231
40561
|
return reply.code(201).send({ number: ticket.number, key, statusPath });
|
|
40232
40562
|
});
|
|
40233
40563
|
app2.get("/help/:slug/tickets/:key", async (req, reply) => {
|
|
40234
|
-
const {
|
|
40564
|
+
const { key } = req.params;
|
|
40235
40565
|
const t = await prisma.ticket.findFirst({
|
|
40236
|
-
where: {
|
|
40566
|
+
where: { OR: [{ accessKeyHash: hashAccessKey(key) }, { shareToken: key }] }
|
|
40237
40567
|
});
|
|
40238
40568
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40239
40569
|
let stage = null;
|
|
@@ -40326,7 +40656,7 @@ async function acceptTicket(t, opts) {
|
|
|
40326
40656
|
|
|
40327
40657
|
// src/routes/tickets.ts
|
|
40328
40658
|
init_src();
|
|
40329
|
-
var
|
|
40659
|
+
var view6 = (t) => ({
|
|
40330
40660
|
id: t.id,
|
|
40331
40661
|
projectId: t.projectId,
|
|
40332
40662
|
number: t.number,
|
|
@@ -40338,7 +40668,7 @@ var view5 = (t) => ({
|
|
|
40338
40668
|
attachmentCount: t._count?.attachments ?? 0,
|
|
40339
40669
|
createdAt: t.createdAt.toISOString()
|
|
40340
40670
|
});
|
|
40341
|
-
async function tickets_default(app2) {
|
|
40671
|
+
async function tickets_default(app2, opts = {}) {
|
|
40342
40672
|
app2.get("/tickets", async (req) => {
|
|
40343
40673
|
const { project, status, q, page, limit } = req.query;
|
|
40344
40674
|
const where = {};
|
|
@@ -40354,7 +40684,7 @@ async function tickets_default(app2) {
|
|
|
40354
40684
|
rows = rows.filter((t) => `${t.title} ${t.reporterEmail}`.toLowerCase().includes(n2));
|
|
40355
40685
|
}
|
|
40356
40686
|
const unreviewed = rows.filter((t) => t.status === "new").length;
|
|
40357
|
-
return { ...paginate(rows.map(
|
|
40687
|
+
return { ...paginate(rows.map(view6), page, limit), unreviewed };
|
|
40358
40688
|
});
|
|
40359
40689
|
app2.get("/tickets/:id", async (req, reply) => {
|
|
40360
40690
|
const { id } = req.params;
|
|
@@ -40369,10 +40699,10 @@ async function tickets_default(app2) {
|
|
|
40369
40699
|
shareToken = generateShareToken();
|
|
40370
40700
|
await prisma.ticket.update({ where: { id }, data: { shareToken } });
|
|
40371
40701
|
}
|
|
40372
|
-
const base2 = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40702
|
+
const base2 = opts.publicBase ?? `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40373
40703
|
const publicStatusUrl = `${base2}/help/${encodeURIComponent(t.projectId)}/status/${shareToken}`;
|
|
40374
40704
|
return {
|
|
40375
|
-
...
|
|
40705
|
+
...view6(t),
|
|
40376
40706
|
detail: t.detail,
|
|
40377
40707
|
attachments: t.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40378
40708
|
spec,
|
|
@@ -40432,7 +40762,7 @@ async function tickets_default(app2) {
|
|
|
40432
40762
|
});
|
|
40433
40763
|
const spec = updated.specId ? await prisma.spec.findUnique({ where: { id: updated.specId } }) : null;
|
|
40434
40764
|
return {
|
|
40435
|
-
...
|
|
40765
|
+
...view6(updated),
|
|
40436
40766
|
detail: updated.detail,
|
|
40437
40767
|
attachments: updated.attachments.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size })),
|
|
40438
40768
|
spec
|
|
@@ -40443,7 +40773,7 @@ async function tickets_default(app2) {
|
|
|
40443
40773
|
const t = await prisma.ticket.findUnique({ where: { id }, include: { attachments: true } });
|
|
40444
40774
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40445
40775
|
for (const a of t.attachments) await deleteUpload(a.storageKey);
|
|
40446
|
-
await
|
|
40776
|
+
await deleteSynced("ticket", id);
|
|
40447
40777
|
return { ok: true };
|
|
40448
40778
|
});
|
|
40449
40779
|
}
|
|
@@ -40550,6 +40880,7 @@ async function isQueued(id) {
|
|
|
40550
40880
|
// src/services/scheduler/cron.ts
|
|
40551
40881
|
init_db();
|
|
40552
40882
|
init_src();
|
|
40883
|
+
init_notifications();
|
|
40553
40884
|
var GRACE_MS = 30 * 6e4;
|
|
40554
40885
|
function computeNextRun(expr, after) {
|
|
40555
40886
|
const spec = parseCron(expr);
|
|
@@ -40900,6 +41231,7 @@ async function leadProjects() {
|
|
|
40900
41231
|
init_src();
|
|
40901
41232
|
init_db();
|
|
40902
41233
|
init_pty();
|
|
41234
|
+
init_notifications();
|
|
40903
41235
|
|
|
40904
41236
|
// src/services/lead/prompt.ts
|
|
40905
41237
|
init_src();
|
|
@@ -41671,6 +42003,7 @@ init_src();
|
|
|
41671
42003
|
init_db();
|
|
41672
42004
|
init_pty();
|
|
41673
42005
|
init_session_phases();
|
|
42006
|
+
init_notifications();
|
|
41674
42007
|
|
|
41675
42008
|
// src/services/webhooks/actor.ts
|
|
41676
42009
|
init_src();
|
|
@@ -41823,6 +42156,7 @@ Bukti integrasi: ${evidence.join("; ")} \u2192 ${verdict}.` }
|
|
|
41823
42156
|
init_src();
|
|
41824
42157
|
init_pty();
|
|
41825
42158
|
init_tui_dialog();
|
|
42159
|
+
init_notifications();
|
|
41826
42160
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
41827
42161
|
|
|
41828
42162
|
// src/services/lead/pane.ts
|
|
@@ -42143,6 +42477,7 @@ function sweep(liveIds) {
|
|
|
42143
42477
|
init_db();
|
|
42144
42478
|
init_pty();
|
|
42145
42479
|
init_session_phases();
|
|
42480
|
+
init_notifications();
|
|
42146
42481
|
var moduleOf = (p) => p.split("/").slice(0, 2).join("/");
|
|
42147
42482
|
function findCollisions(areas) {
|
|
42148
42483
|
const out3 = [];
|
|
@@ -42410,6 +42745,7 @@ async function orderProject(projectId, deps) {
|
|
|
42410
42745
|
}
|
|
42411
42746
|
|
|
42412
42747
|
// src/services/lead/engine.ts
|
|
42748
|
+
init_notifications();
|
|
42413
42749
|
var TICK_MS2 = 5e3;
|
|
42414
42750
|
var busyDetect = false;
|
|
42415
42751
|
var busyPulse = false;
|
|
@@ -42924,7 +43260,7 @@ async function generateChangelog(projectId, req, deps = {}) {
|
|
|
42924
43260
|
}
|
|
42925
43261
|
|
|
42926
43262
|
// src/routes/changelog.ts
|
|
42927
|
-
var
|
|
43263
|
+
var view7 = (c) => ({
|
|
42928
43264
|
id: c.id,
|
|
42929
43265
|
projectId: c.projectId,
|
|
42930
43266
|
mode: c.mode,
|
|
@@ -42964,7 +43300,7 @@ async function changelog_default(app2) {
|
|
|
42964
43300
|
if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
|
|
42965
43301
|
const { page, limit, q } = req.query;
|
|
42966
43302
|
const rows = await prisma.changelog.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
|
|
42967
|
-
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(
|
|
43303
|
+
return paginate(rows.filter((r) => changelogMatches(r, q ?? "")).map(view7), page, limit);
|
|
42968
43304
|
});
|
|
42969
43305
|
app2.get("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42970
43306
|
const { id, cid } = req.params;
|
|
@@ -42978,7 +43314,7 @@ async function changelog_default(app2) {
|
|
|
42978
43314
|
eyebrow: `hanoman \xB7 ${id} \xB7 changelog`,
|
|
42979
43315
|
path: row.title
|
|
42980
43316
|
});
|
|
42981
|
-
return
|
|
43317
|
+
return view7(row);
|
|
42982
43318
|
});
|
|
42983
43319
|
app2.post("/projects/:id/changelog", async (req, reply) => {
|
|
42984
43320
|
const { id } = req.params;
|
|
@@ -42987,7 +43323,7 @@ async function changelog_default(app2) {
|
|
|
42987
43323
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
42988
43324
|
const r = await generateChangelog(id, parsed.data);
|
|
42989
43325
|
if (!r.ok) return reply.code(422).send({ error: r.reason });
|
|
42990
|
-
return reply.code(201).send(
|
|
43326
|
+
return reply.code(201).send(view7(r.row));
|
|
42991
43327
|
});
|
|
42992
43328
|
app2.delete("/projects/:id/changelog/:cid", async (req, reply) => {
|
|
42993
43329
|
const { id, cid } = req.params;
|
|
@@ -43126,7 +43462,7 @@ async function installCustomAgents() {
|
|
|
43126
43462
|
|
|
43127
43463
|
// src/routes/custom-agents.ts
|
|
43128
43464
|
var rowsOf = async () => await prisma.customAgent.findMany();
|
|
43129
|
-
var
|
|
43465
|
+
var view8 = (r, projectId) => ({
|
|
43130
43466
|
id: r.id,
|
|
43131
43467
|
projectId: r.projectId,
|
|
43132
43468
|
name: r.name,
|
|
@@ -43171,7 +43507,7 @@ async function custom_agents_default(app2) {
|
|
|
43171
43507
|
const byName = /* @__PURE__ */ new Map();
|
|
43172
43508
|
for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
|
|
43173
43509
|
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) =>
|
|
43510
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view8(r, projectId));
|
|
43175
43511
|
});
|
|
43176
43512
|
app2.post("/custom-agents", async (req, reply) => {
|
|
43177
43513
|
const parsed = zCreateCustomAgent.safeParse(req.body);
|
|
@@ -43218,7 +43554,7 @@ async function custom_agents_default(app2) {
|
|
|
43218
43554
|
} });
|
|
43219
43555
|
await loadCustomAgents();
|
|
43220
43556
|
await notifySynced("customAgent", id);
|
|
43221
|
-
return reply.code(201).send(
|
|
43557
|
+
return reply.code(201).send(view8(row));
|
|
43222
43558
|
});
|
|
43223
43559
|
app2.patch("/custom-agents/:id", async (req, reply) => {
|
|
43224
43560
|
const { id } = req.params;
|
|
@@ -43265,13 +43601,13 @@ async function custom_agents_default(app2) {
|
|
|
43265
43601
|
} });
|
|
43266
43602
|
await loadCustomAgents();
|
|
43267
43603
|
await notifySynced("customAgent", id);
|
|
43268
|
-
return
|
|
43604
|
+
return view8(row);
|
|
43269
43605
|
});
|
|
43270
43606
|
app2.delete("/custom-agents/:id", async (req, reply) => {
|
|
43271
43607
|
const { id } = req.params;
|
|
43272
43608
|
const existing = await prisma.customAgent.findUnique({ where: { id } });
|
|
43273
43609
|
if (!existing) return reply.code(404).send({ error: "not found" });
|
|
43274
|
-
await
|
|
43610
|
+
await deleteSynced("customAgent", id);
|
|
43275
43611
|
const name2 = existing.name;
|
|
43276
43612
|
for (const r of await rowsOf()) {
|
|
43277
43613
|
const m = mentionsOf(r.mentions);
|
|
@@ -43606,7 +43942,7 @@ var STATUS = {
|
|
|
43606
43942
|
unauthorized: 401,
|
|
43607
43943
|
other: 502
|
|
43608
43944
|
};
|
|
43609
|
-
var
|
|
43945
|
+
var view9 = (i) => ({
|
|
43610
43946
|
id: i.id,
|
|
43611
43947
|
projectId: i.projectId,
|
|
43612
43948
|
repoSlug: i.repoSlug,
|
|
@@ -43641,7 +43977,7 @@ async function githubIssues(app2) {
|
|
|
43641
43977
|
where: { projectId: id, ...status ? { status } : {} },
|
|
43642
43978
|
orderBy: [{ number: "desc" }]
|
|
43643
43979
|
});
|
|
43644
|
-
return reply.send(paginate(items.map(
|
|
43980
|
+
return reply.send(paginate(items.map(view9), page, limit));
|
|
43645
43981
|
});
|
|
43646
43982
|
app2.post("/github-issues/accept", async (req, reply) => {
|
|
43647
43983
|
const parsed = zAcceptMany.safeParse(req.body ?? {});
|
|
@@ -44401,7 +44737,7 @@ async function portal_default(app2) {
|
|
|
44401
44737
|
// src/routes/client-accounts.ts
|
|
44402
44738
|
init_src();
|
|
44403
44739
|
init_db();
|
|
44404
|
-
var
|
|
44740
|
+
var view10 = (u) => ({
|
|
44405
44741
|
id: u.id,
|
|
44406
44742
|
email: u.email,
|
|
44407
44743
|
disabled: u.disabled,
|
|
@@ -44432,7 +44768,7 @@ async function client_accounts_default(app2) {
|
|
|
44432
44768
|
orderBy: { createdAt: "asc" },
|
|
44433
44769
|
include: { projectAccess: { select: { projectId: true } } }
|
|
44434
44770
|
});
|
|
44435
|
-
return { items: rows.map(
|
|
44771
|
+
return { items: rows.map(view10) };
|
|
44436
44772
|
});
|
|
44437
44773
|
app2.post("/client-accounts", async (req, reply) => {
|
|
44438
44774
|
const p = zCreateClientAccount.safeParse(req.body);
|
|
@@ -44446,7 +44782,7 @@ async function client_accounts_default(app2) {
|
|
|
44446
44782
|
await prisma.user.delete({ where: { id: user.id } });
|
|
44447
44783
|
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
44448
44784
|
}
|
|
44449
|
-
return reply.code(201).send(
|
|
44785
|
+
return reply.code(201).send(view10(await load(user.id)));
|
|
44450
44786
|
});
|
|
44451
44787
|
app2.patch("/client-accounts/:id", async (req, reply) => {
|
|
44452
44788
|
const { id } = req.params;
|
|
@@ -44460,7 +44796,7 @@ async function client_accounts_default(app2) {
|
|
|
44460
44796
|
if (p.data.password)
|
|
44461
44797
|
await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(p.data.password) } });
|
|
44462
44798
|
if (p.data.disabled || p.data.password) await deleteUserSessions(id);
|
|
44463
|
-
return
|
|
44799
|
+
return view10(await load(id));
|
|
44464
44800
|
});
|
|
44465
44801
|
app2.delete("/client-accounts/:id", async (req, reply) => {
|
|
44466
44802
|
const { id } = req.params;
|
|
@@ -44556,7 +44892,7 @@ async function consumeSetupToken(home3) {
|
|
|
44556
44892
|
|
|
44557
44893
|
// src/routes/auth.ts
|
|
44558
44894
|
var BOOTSTRAP_USER_ID = "bootstrap-admin";
|
|
44559
|
-
var
|
|
44895
|
+
var view11 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
44560
44896
|
async function issue(reply, userId) {
|
|
44561
44897
|
const token = await createSession2(userId);
|
|
44562
44898
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -44596,7 +44932,7 @@ async function auth_default(app2, opts) {
|
|
|
44596
44932
|
if (opts.bootstrapRequired) await consumeSetupToken(opts.home);
|
|
44597
44933
|
setupAttempts.clear(req.ip);
|
|
44598
44934
|
await issue(reply, user.id);
|
|
44599
|
-
return { user:
|
|
44935
|
+
return { user: view11(user) };
|
|
44600
44936
|
});
|
|
44601
44937
|
app2.post("/auth/login", async (req, reply) => {
|
|
44602
44938
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
@@ -44609,7 +44945,7 @@ async function auth_default(app2, opts) {
|
|
|
44609
44945
|
}
|
|
44610
44946
|
clearLoginFails(req.ip);
|
|
44611
44947
|
await issue(reply, user.id);
|
|
44612
|
-
return { user:
|
|
44948
|
+
return { user: view11(user) };
|
|
44613
44949
|
});
|
|
44614
44950
|
app2.post("/auth/logout", async (req, reply) => {
|
|
44615
44951
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -44617,7 +44953,7 @@ async function auth_default(app2, opts) {
|
|
|
44617
44953
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
44618
44954
|
return reply.code(204).send();
|
|
44619
44955
|
});
|
|
44620
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
44956
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view11));
|
|
44621
44957
|
app2.post("/auth/users", async (req, reply) => {
|
|
44622
44958
|
const p = zSignup.safeParse(req.body);
|
|
44623
44959
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
@@ -44628,7 +44964,7 @@ async function auth_default(app2, opts) {
|
|
|
44628
44964
|
// (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
|
|
44629
44965
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "admin" }
|
|
44630
44966
|
});
|
|
44631
|
-
return
|
|
44967
|
+
return view11(user);
|
|
44632
44968
|
});
|
|
44633
44969
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
44634
44970
|
const target2 = await prisma.user.findUnique({ where: { id: req.params.id } });
|
|
@@ -44651,7 +44987,7 @@ async function auth_default(app2, opts) {
|
|
|
44651
44987
|
});
|
|
44652
44988
|
await deleteUserSessions(user.id);
|
|
44653
44989
|
await issue(reply, user.id);
|
|
44654
|
-
return { user:
|
|
44990
|
+
return { user: view11(user) };
|
|
44655
44991
|
});
|
|
44656
44992
|
}
|
|
44657
44993
|
|
|
@@ -44664,15 +45000,15 @@ async function agent_tokens_default(app2) {
|
|
|
44664
45000
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
44665
45001
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
44666
45002
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44667
|
-
const { view:
|
|
44668
|
-
return reply.code(201).send({ ...
|
|
45003
|
+
const { view: view12, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
45004
|
+
return reply.code(201).send({ ...view12, token });
|
|
44669
45005
|
});
|
|
44670
45006
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
44671
45007
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
44672
45008
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
44673
45009
|
const { id } = req.params;
|
|
44674
|
-
const
|
|
44675
|
-
return
|
|
45010
|
+
const view12 = await patchAgentToken(id, p.data);
|
|
45011
|
+
return view12 ? reply.send(view12) : reply.code(404).send({ error: "not found" });
|
|
44676
45012
|
});
|
|
44677
45013
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
44678
45014
|
const { id } = req.params;
|
|
@@ -44881,22 +45217,31 @@ init_runtime();
|
|
|
44881
45217
|
|
|
44882
45218
|
// src/services/ingress-policy.ts
|
|
44883
45219
|
function origins(value, key) {
|
|
44884
|
-
const out3 =
|
|
45220
|
+
const out3 = [];
|
|
44885
45221
|
for (const raw2 of value?.split(",") ?? []) {
|
|
44886
45222
|
const input = raw2.trim();
|
|
44887
45223
|
if (!input) continue;
|
|
44888
45224
|
const parsed = new URL(input);
|
|
44889
45225
|
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== "/")
|
|
44890
45226
|
throw new Error(`${key} harus berisi exact http(s) origin`);
|
|
44891
|
-
out3.
|
|
45227
|
+
out3.push({ host: parsed.host.toLowerCase(), base: `${parsed.protocol}//${parsed.host}` });
|
|
44892
45228
|
}
|
|
44893
45229
|
return out3;
|
|
44894
45230
|
}
|
|
44895
45231
|
function loadIngressPolicy(env) {
|
|
44896
|
-
const
|
|
44897
|
-
const
|
|
45232
|
+
const pub = origins(env.HANOMAN_PUBLIC_ORIGINS, "HANOMAN_PUBLIC_ORIGINS");
|
|
45233
|
+
const control = origins(env.HANOMAN_CONTROL_ORIGINS, "HANOMAN_CONTROL_ORIGINS");
|
|
45234
|
+
const publicHosts = new Set(pub.map((o) => o.host));
|
|
45235
|
+
const controlHosts = new Set(control.map((o) => o.host));
|
|
44898
45236
|
for (const host2 of publicHosts) if (controlHosts.has(host2)) throw new Error("public dan control origin harus berbeda");
|
|
44899
|
-
return {
|
|
45237
|
+
return {
|
|
45238
|
+
publicHosts,
|
|
45239
|
+
controlHosts,
|
|
45240
|
+
// SPEC-805 · origin publik pertama, lengkap dengan scheme-nya: satu-satunya basis sah untuk
|
|
45241
|
+
// link status yang dibagikan operator, yang selalu lahir di request ber-host control.
|
|
45242
|
+
publicBase: pub[0]?.base ?? null,
|
|
45243
|
+
enforce: publicHosts.size > 0 || controlHosts.size > 0
|
|
45244
|
+
};
|
|
44900
45245
|
}
|
|
44901
45246
|
function publicPath(method, path2) {
|
|
44902
45247
|
if (method === "GET" && path2 === "/api/health") return true;
|
|
@@ -44908,7 +45253,8 @@ function classifyIngress(req, policy) {
|
|
|
44908
45253
|
const host2 = req.host.trim().toLowerCase();
|
|
44909
45254
|
const path2 = req.url.split("?")[0] ?? req.url;
|
|
44910
45255
|
if (policy.publicHosts.has(host2)) return publicPath(req.method, path2) ? "public" : "denied";
|
|
44911
|
-
if (policy.controlHosts.has(host2))
|
|
45256
|
+
if (policy.controlHosts.has(host2))
|
|
45257
|
+
return policy.publicBase && path2.startsWith("/api/help") ? "denied" : "control";
|
|
44912
45258
|
return "denied";
|
|
44913
45259
|
}
|
|
44914
45260
|
function trustProxyFromEnv(env) {
|
|
@@ -44941,6 +45287,8 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
44941
45287
|
app2.addHook("onRequest", async (req, reply) => {
|
|
44942
45288
|
const role = classifyIngress({ host: req.headers.host ?? "", method: req.method, url: req.url }, ingress);
|
|
44943
45289
|
if (role === "denied") return reply.code(404).send({ error: "not found" });
|
|
45290
|
+
if (role === "control" && ingress.publicBase && req.url.startsWith("/help/"))
|
|
45291
|
+
return reply.redirect(`${ingress.publicBase}${req.url}`, 302);
|
|
44944
45292
|
});
|
|
44945
45293
|
app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
44946
45294
|
if (!body) return done(null, void 0);
|
|
@@ -44971,7 +45319,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
44971
45319
|
if (PUBLIC.has(`${req.method} ${path2}`)) return;
|
|
44972
45320
|
if (user?.role === "client" && !clientRouteAllowed(req.method, path2))
|
|
44973
45321
|
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;
|
|
45322
|
+
if (path2.startsWith("/api/sync") && path2 !== "/api/sync/now" && path2 !== "/api/sync/pending" && !path2.startsWith("/api/sync/conflicts")) return;
|
|
44975
45323
|
if (path2.startsWith("/api/help")) return;
|
|
44976
45324
|
if (user) return;
|
|
44977
45325
|
const agentTok = agentTokenFromReq(req);
|
|
@@ -45020,7 +45368,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45020
45368
|
await api.register(session_history_default);
|
|
45021
45369
|
await api.register(config_default);
|
|
45022
45370
|
await api.register(help_default);
|
|
45023
|
-
await api.register(tickets_default);
|
|
45371
|
+
await api.register(tickets_default, { publicBase: ingress.publicBase });
|
|
45024
45372
|
await api.register(scheduler_default);
|
|
45025
45373
|
await api.register(codex);
|
|
45026
45374
|
await api.register(methods);
|
|
@@ -45085,6 +45433,7 @@ init_src();
|
|
|
45085
45433
|
|
|
45086
45434
|
// src/services/scheduler/governor.ts
|
|
45087
45435
|
init_db();
|
|
45436
|
+
init_notifications();
|
|
45088
45437
|
var ALREADY_DONE_NOTE = "spec sudah selesai \u2014 tak diluncurkan";
|
|
45089
45438
|
var canceledRaceNote = (sessionId2) => `dibatalkan saat sesi ${sessionId2} sudah terlanjur lahir \u2014 sesi dibiarkan hidup, tutup dari Terminal bila tak diperlukan`;
|
|
45090
45439
|
var CAP_FULL_NOTE = "cap penuh \u2014 tak ada slot sesi";
|
|
@@ -45219,6 +45568,7 @@ async function startCronSession(cron) {
|
|
|
45219
45568
|
// src/services/scheduler/reconcile.ts
|
|
45220
45569
|
init_db();
|
|
45221
45570
|
init_src2();
|
|
45571
|
+
init_notifications();
|
|
45222
45572
|
init_stage_machine();
|
|
45223
45573
|
init_pty();
|
|
45224
45574
|
init_session_phases();
|
|
@@ -45282,6 +45632,7 @@ var reconcileProdDeps = {
|
|
|
45282
45632
|
};
|
|
45283
45633
|
|
|
45284
45634
|
// src/services/scheduler/engine.ts
|
|
45635
|
+
init_notifications();
|
|
45285
45636
|
init_pty();
|
|
45286
45637
|
var prodEnd = {
|
|
45287
45638
|
reconcile: () => reconcile(reconcileProdDeps),
|
|
@@ -45656,6 +46007,7 @@ init_src();
|
|
|
45656
46007
|
init_db();
|
|
45657
46008
|
import { execFile as execFile13 } from "node:child_process";
|
|
45658
46009
|
import { promisify as promisify12 } from "node:util";
|
|
46010
|
+
init_notifications();
|
|
45659
46011
|
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
45660
46012
|
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
45661
46013
|
var TICK_MS5 = 6e4;
|
|
@@ -45709,6 +46061,7 @@ async function sweepAutoMerge(deps = prodAutoMergeDeps, now = /* @__PURE__ */ ne
|
|
|
45709
46061
|
async function settleOne(c, deps, now) {
|
|
45710
46062
|
const spec = await prisma.spec.findUnique({ where: { id: c.specId } });
|
|
45711
46063
|
if (!spec || spec.stage !== "done") return false;
|
|
46064
|
+
if (spec.manualDone) return false;
|
|
45712
46065
|
const project = await prisma.project.findUnique({ where: { id: spec.projectId } });
|
|
45713
46066
|
if (!project) return false;
|
|
45714
46067
|
const policy = resolveAutoMerge(
|