hanoman 0.9.13 → 0.9.14
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 +1 -1
- package/dist/server.js +386 -236
- package/package.json +1 -1
- package/web/assets/{ChangelogScreen-Ceqp0J6L.js → ChangelogScreen-CS_GSORI.js} +1 -1
- package/web/assets/{DalangHanomanScreen-CPJtZxV3.js → DalangHanomanScreen-maIEXuvi.js} +1 -1
- package/web/assets/{DocPreviewModal-C_vQOOHV.js → DocPreviewModal-DFqCURCs.js} +1 -1
- package/web/assets/{DocsWorkspace-D4P_5t9b.js → DocsWorkspace-hxJ037xu.js} +1 -1
- package/web/assets/{IdeScreen-bD4qiBDt.js → IdeScreen-CHXLJ0Az.js} +1 -1
- package/web/assets/{LeadScreen-Db9PHjdT.js → LeadScreen-DI8TW6QX.js} +1 -1
- package/web/assets/{ReviewScreen-DLlDOQod.js → ReviewScreen-CAi2WDfW.js} +1 -1
- package/web/assets/{SchedulerScreen-D6ffDlTM.js → SchedulerScreen-D1vCNuRr.js} +1 -1
- package/web/assets/{SettingsScreen-72EvmiY4.js → SettingsScreen-1GM6-O69.js} +1 -1
- package/web/assets/{TeamScreen-CY-5tnPa.js → TeamScreen-eYlsFpzN.js} +1 -1
- package/web/assets/{TerminalScreen-CKac8Ti6.js → TerminalScreen-u6e-6CB7.js} +1 -1
- package/web/assets/{TriageScreen-BEb2vxPb.js → TriageScreen-B6czWJOz.js} +1 -1
- package/web/assets/{VpsScreen-DTJd5Gks.js → VpsScreen-CaYPux2B.js} +1 -1
- package/web/assets/{index-DYNYB1Nj.js → index--wY0Cvay.js} +215 -211
- package/web/assets/{live-nMnpVkPn.js → live-WWHlTNwv.js} +1 -1
- package/web/index.html +1 -1
package/dist/server.js
CHANGED
|
@@ -8194,7 +8194,7 @@ var init_api = __esm({
|
|
|
8194
8194
|
qaReportBacklog: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/backlog`,
|
|
8195
8195
|
qaCasesImport: (pid, rid) => `${API}/projects/${encodeURIComponent(pid)}/qa/reports/${encodeURIComponent(rid)}/cases/import`,
|
|
8196
8196
|
qaImport: (pid) => `${API}/projects/${encodeURIComponent(pid)}/qa/import`,
|
|
8197
|
-
qaTemplate: `${API}/qa/template.
|
|
8197
|
+
qaTemplate: `${API}/qa/template.xlsx`,
|
|
8198
8198
|
// SPEC-945 · ADR-0150 · papan tim. `members` GLOBAL (bukan per project) — task boleh tanpa
|
|
8199
8199
|
// project, jadi direktori orang tak bisa digantung pada project.
|
|
8200
8200
|
members: `${API}/members`,
|
|
@@ -17638,9 +17638,9 @@ import {
|
|
|
17638
17638
|
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join11, normalize, relative as relative4, sep as sep5 } from "node:path";
|
|
17639
17639
|
function safeJoin(dir2, rel) {
|
|
17640
17640
|
if (!rel || isAbsolute6(rel)) throw new SkillError(400, "path harus relatif terhadap skill");
|
|
17641
|
-
const
|
|
17642
|
-
if (
|
|
17643
|
-
const full = join11(dir2,
|
|
17641
|
+
const norm5 = normalize(rel);
|
|
17642
|
+
if (norm5 === ".." || norm5.startsWith(".." + sep5)) throw new SkillError(400, "path keluar dari skill");
|
|
17643
|
+
const full = join11(dir2, norm5);
|
|
17644
17644
|
let probe = full;
|
|
17645
17645
|
while (!existsSync7(probe) && probe !== dir2) probe = dirname6(probe);
|
|
17646
17646
|
const realDir = realpathSync5(dir2);
|
|
@@ -54953,11 +54953,11 @@ async function listConflicts() {
|
|
|
54953
54953
|
detectedAt: r.detectedAt.toISOString()
|
|
54954
54954
|
}));
|
|
54955
54955
|
}
|
|
54956
|
-
async function resolveConflict(entity, recordId,
|
|
54956
|
+
async function resolveConflict(entity, recordId, choice2, push) {
|
|
54957
54957
|
if (!isEntity(entity)) return { ok: false, reason: "not-found" };
|
|
54958
54958
|
const c = await prisma.syncConflict.findUnique({ where: { entity_recordId: { entity, recordId } } });
|
|
54959
54959
|
if (!c || c.resolvedAt) return { ok: false, reason: "not-found" };
|
|
54960
|
-
if (
|
|
54960
|
+
if (choice2 === "server") {
|
|
54961
54961
|
await upsertLocal(entity, recordId, c.serverVersion, c.serverData);
|
|
54962
54962
|
} else {
|
|
54963
54963
|
let r = (await push([{ entity, id: recordId, baseVersion: c.serverVersion, data: c.localData }])).results?.[0];
|
|
@@ -55289,7 +55289,7 @@ function createRelayDispatcher(o) {
|
|
|
55289
55289
|
send(i === 0 ? { t: "res", id: id2, status: status2, contentType, part, end: end2 } : { t: "res", id: id2, part, end: end2 });
|
|
55290
55290
|
});
|
|
55291
55291
|
};
|
|
55292
|
-
const
|
|
55292
|
+
const fail4 = (id2, status2, error) => reply(id2, status2, JSON_TYPE, JSON.stringify({ error }));
|
|
55293
55293
|
async function handleReq(f) {
|
|
55294
55294
|
const started2 = now();
|
|
55295
55295
|
const record2 = (status2, extra = {}) => audit({
|
|
@@ -55300,19 +55300,19 @@ function createRelayDispatcher(o) {
|
|
|
55300
55300
|
});
|
|
55301
55301
|
if (inflight3.has(f.id)) return;
|
|
55302
55302
|
if (inflight3.size >= RELAY_MAX_INFLIGHT) {
|
|
55303
|
-
|
|
55303
|
+
fail4(f.id, 429, "relay-busy");
|
|
55304
55304
|
record2(429);
|
|
55305
55305
|
return;
|
|
55306
55306
|
}
|
|
55307
55307
|
const payload = f.body === void 0 ? void 0 : JSON.stringify(f.body);
|
|
55308
55308
|
if (payload !== void 0 && utf8Bytes(payload) > RELAY_REQUEST_BODY_MAX_BYTES) {
|
|
55309
|
-
|
|
55309
|
+
fail4(f.id, 413, "relay-body-too-large");
|
|
55310
55310
|
record2(413);
|
|
55311
55311
|
return;
|
|
55312
55312
|
}
|
|
55313
55313
|
const url2 = f.query ? `${f.path}?${f.query.replace(/^\?/, "")}` : f.path;
|
|
55314
55314
|
if (!relayRouteAllowed(f.method, url2) || !relayBodyAllowed(f.method, f.path, f.body)) {
|
|
55315
|
-
|
|
55315
|
+
fail4(f.id, 403, "relay route not allowed");
|
|
55316
55316
|
record2(403);
|
|
55317
55317
|
return;
|
|
55318
55318
|
}
|
|
@@ -55360,13 +55360,13 @@ function createRelayDispatcher(o) {
|
|
|
55360
55360
|
});
|
|
55361
55361
|
if (entry2.cancelled) return;
|
|
55362
55362
|
if (utf8Bytes(res.body) > RELAY_RESPONSE_MAX_BYTES) {
|
|
55363
|
-
|
|
55363
|
+
fail4(f.id, 502, "relay-response-too-large");
|
|
55364
55364
|
return;
|
|
55365
55365
|
}
|
|
55366
55366
|
reply(f.id, res.statusCode, String(res.headers["content-type"] ?? "application/octet-stream"), res.body);
|
|
55367
55367
|
} catch {
|
|
55368
55368
|
record2(502);
|
|
55369
|
-
if (!entry2.cancelled)
|
|
55369
|
+
if (!entry2.cancelled) fail4(f.id, 502, "relay-dispatch-failed");
|
|
55370
55370
|
} finally {
|
|
55371
55371
|
inflight3.delete(f.id);
|
|
55372
55372
|
}
|
|
@@ -72960,20 +72960,20 @@ async function decide(req, deps = prodDecideDeps) {
|
|
|
72960
72960
|
const resolved = resolveChoices(rawChoices, options2);
|
|
72961
72961
|
const countProblem = options2.length && resolved.choices.length ? checkChoiceCount(resolved.choices.length, bounds) : null;
|
|
72962
72962
|
const choices = countProblem ? [] : resolved.choices;
|
|
72963
|
-
const
|
|
72963
|
+
const choice2 = choices[0] ?? null;
|
|
72964
72964
|
const choiceRejected = options2.length > 0 && rawChoices.length > 0 && (resolved.rejected.length > 0 || !!countProblem);
|
|
72965
72965
|
const missing = verdict.missing.map((m) => m.trim()).filter(Boolean);
|
|
72966
72966
|
let action = allowed ? verdict.action : "none";
|
|
72967
72967
|
let actionNote = "";
|
|
72968
72968
|
let conflict = false;
|
|
72969
|
-
if (allowed && choices.length === 1 &&
|
|
72970
|
-
const hint = optionActionHint(
|
|
72969
|
+
if (allowed && choices.length === 1 && choice2) {
|
|
72970
|
+
const hint = optionActionHint(choice2.option);
|
|
72971
72971
|
if (hint && action === "none" && hint !== "none") {
|
|
72972
72972
|
action = hint;
|
|
72973
72973
|
actionNote = `Tindakan diturunkan dari opsi terpilih ("${hint}") karena lead tak menyebutnya sendiri (SPEC-480).`;
|
|
72974
72974
|
} else if (hint && action !== "none" && action !== hint) {
|
|
72975
72975
|
conflict = true;
|
|
72976
|
-
actionNote = `KONFLIK: lead memilih opsi "${
|
|
72976
|
+
actionNote = `KONFLIK: lead memilih opsi "${choice2.option}" tetapi menyetel action "${action}" \u2014 tindakan dibatalkan (SPEC-480).`;
|
|
72977
72977
|
action = "none";
|
|
72978
72978
|
}
|
|
72979
72979
|
}
|
|
@@ -73006,8 +73006,8 @@ ${notes.join("\n\n")}` : "";
|
|
|
73006
73006
|
confidence,
|
|
73007
73007
|
action,
|
|
73008
73008
|
weighty,
|
|
73009
|
-
choice:
|
|
73010
|
-
choiceIndex:
|
|
73009
|
+
choice: choice2?.option ?? null,
|
|
73010
|
+
choiceIndex: choice2?.index ?? null,
|
|
73011
73011
|
choices,
|
|
73012
73012
|
select: bounds,
|
|
73013
73013
|
flowId: flow2.id,
|
|
@@ -73030,7 +73030,7 @@ ${notes.join("\n\n")}` : "";
|
|
|
73030
73030
|
reason: `${clampProse(verdict.reason, LEAD_REASON_MAX)}${tail3}`,
|
|
73031
73031
|
reply: verdict.reply,
|
|
73032
73032
|
choices,
|
|
73033
|
-
choice,
|
|
73033
|
+
choice: choice2,
|
|
73034
73034
|
missing
|
|
73035
73035
|
});
|
|
73036
73036
|
return row;
|
|
@@ -79614,16 +79614,13 @@ async function qaAttachments(app3) {
|
|
|
79614
79614
|
// src/routes/qa-transfer.ts
|
|
79615
79615
|
init_src();
|
|
79616
79616
|
|
|
79617
|
-
// src/services/qa-
|
|
79617
|
+
// src/services/qa-workbook.ts
|
|
79618
79618
|
init_src();
|
|
79619
|
-
init_db();
|
|
79620
79619
|
|
|
79621
|
-
// src/services/
|
|
79622
|
-
|
|
79623
|
-
|
|
79624
|
-
|
|
79625
|
-
init_qa_attachment_transfer();
|
|
79626
|
-
init_sync_notify();
|
|
79620
|
+
// src/services/ooxml.ts
|
|
79621
|
+
var xmlEsc = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
79622
|
+
var xmlClean = (s2) => s2.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
79623
|
+
var XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
79627
79624
|
|
|
79628
79625
|
// src/services/zip.ts
|
|
79629
79626
|
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
@@ -79739,7 +79736,319 @@ function readZip(buf, o = {}) {
|
|
|
79739
79736
|
return out4;
|
|
79740
79737
|
}
|
|
79741
79738
|
|
|
79739
|
+
// src/services/xlsx.ts
|
|
79740
|
+
var XlsxError = class extends Error {
|
|
79741
|
+
};
|
|
79742
|
+
var MAX_ROWS = 5e3;
|
|
79743
|
+
var MAX_COLS = 60;
|
|
79744
|
+
var MAX_CELL = 32767;
|
|
79745
|
+
var esc2 = xmlEsc;
|
|
79746
|
+
var clean2 = (s2) => xmlClean(s2).slice(0, MAX_CELL);
|
|
79747
|
+
var colLetter = (i) => {
|
|
79748
|
+
let s2 = "";
|
|
79749
|
+
for (let n2 = i + 1; n2 > 0; n2 = Math.floor((n2 - 1) / 26)) s2 = String.fromCharCode(65 + (n2 - 1) % 26) + s2;
|
|
79750
|
+
return s2;
|
|
79751
|
+
};
|
|
79752
|
+
var colIndex = (letters) => [...letters].reduce((n2, c) => n2 * 26 + (c.charCodeAt(0) - 64), 0) - 1;
|
|
79753
|
+
var sheetName = (n2, i) => n2.replace(/[[\]:*?/\\]/g, "_").trim().slice(0, 31) || `Sheet${i + 1}`;
|
|
79754
|
+
var NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
79755
|
+
var NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
79756
|
+
var HEAD = XML_HEAD;
|
|
79757
|
+
function writeXlsx(sheets) {
|
|
79758
|
+
const parts = [];
|
|
79759
|
+
const add = (name2, body) => parts.push({ name: name2, data: Buffer.from(HEAD + body, "utf8"), deflate: true });
|
|
79760
|
+
add(
|
|
79761
|
+
"[Content_Types].xml",
|
|
79762
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' + sheets.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join("") + "</Types>"
|
|
79763
|
+
);
|
|
79764
|
+
add(
|
|
79765
|
+
"_rels/.rels",
|
|
79766
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'
|
|
79767
|
+
);
|
|
79768
|
+
add(
|
|
79769
|
+
"xl/workbook.xml",
|
|
79770
|
+
`<workbook xmlns="${NS}" xmlns:r="${NS_R}"><sheets>` + sheets.map((s2, i) => `<sheet name="${esc2(sheetName(s2.name, i))}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets></workbook>"
|
|
79771
|
+
);
|
|
79772
|
+
add(
|
|
79773
|
+
"xl/_rels/workbook.xml.rels",
|
|
79774
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + sheets.map((_, i) => `<Relationship Id="rId${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`).join("") + `<Relationship Id="rId${sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`
|
|
79775
|
+
);
|
|
79776
|
+
add(
|
|
79777
|
+
"xl/styles.xml",
|
|
79778
|
+
`<styleSheet xmlns="${NS}"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFF1E6CC"/><bgColor indexed="64"/></patternFill></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="3"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`
|
|
79779
|
+
);
|
|
79780
|
+
sheets.forEach((s2, si) => {
|
|
79781
|
+
const cols = s2.widths?.length ? `<cols>${s2.widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join("")}</cols>` : "";
|
|
79782
|
+
const data = s2.rows.map((r, ri) => {
|
|
79783
|
+
const cells = r.map((v, ci) => {
|
|
79784
|
+
const t = clean2(v);
|
|
79785
|
+
return t === "" ? "" : `<c r="${colLetter(ci)}${ri + 1}" s="${ri === 0 ? 1 : 2}" t="inlineStr"><is><t xml:space="preserve">${esc2(t)}</t></is></c>`;
|
|
79786
|
+
}).join("");
|
|
79787
|
+
const lines2 = Math.max(1, ...r.map((v, ci) => v.split("\n").reduce((n2, line) => n2 + Math.max(1, Math.ceil(line.length / Math.max(8, (s2.widths?.[ci] ?? 24) - 2))), 0)));
|
|
79788
|
+
const height = Math.min(240, Math.max(24, lines2 * 15 + 8));
|
|
79789
|
+
return `<row r="${ri + 1}" ht="${height}" customHeight="1">${cells}</row>`;
|
|
79790
|
+
}).join("");
|
|
79791
|
+
const validations = s2.dropdowns?.length ? `<dataValidations count="${s2.dropdowns.length}">${s2.dropdowns.map(
|
|
79792
|
+
(d) => `<dataValidation type="list" allowBlank="1" showErrorMessage="1" errorTitle="Nilai tidak valid" error="Pilih nilai dari daftar" sqref="${esc2(d.range)}"><formula1>${esc2(`"${d.values.join(",")}"`)}</formula1></dataValidation>`
|
|
79793
|
+
).join("")}</dataValidations>` : "";
|
|
79794
|
+
add(
|
|
79795
|
+
`xl/worksheets/sheet${si + 1}.xml`,
|
|
79796
|
+
`<worksheet xmlns="${NS}"><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${data}</sheetData>${validations}</worksheet>`
|
|
79797
|
+
);
|
|
79798
|
+
});
|
|
79799
|
+
return writeZip(parts);
|
|
79800
|
+
}
|
|
79801
|
+
function unesc2(s2) {
|
|
79802
|
+
return s2.replace(/&(#x[0-9a-fA-F]+|#\d+|lt|gt|amp|quot|apos);/g, (_, e) => {
|
|
79803
|
+
if (e === "lt") return "<";
|
|
79804
|
+
if (e === "gt") return ">";
|
|
79805
|
+
if (e === "amp") return "&";
|
|
79806
|
+
if (e === "quot") return '"';
|
|
79807
|
+
if (e === "apos") return "'";
|
|
79808
|
+
const cp = e[1] === "x" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
79809
|
+
return Number.isFinite(cp) && cp > 0 && cp <= 1114111 ? String.fromCodePoint(cp) : "";
|
|
79810
|
+
});
|
|
79811
|
+
}
|
|
79812
|
+
var attrs = (s2) => {
|
|
79813
|
+
const out4 = {};
|
|
79814
|
+
for (const m of s2.matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) out4[m[1]] = unesc2(m[2]);
|
|
79815
|
+
return out4;
|
|
79816
|
+
};
|
|
79817
|
+
var textOf = (xmlFragment) => [...xmlFragment.replace(/<rPh\b[\s\S]*?<\/rPh>/g, "").matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)].map((m) => unesc2(m[1])).join("");
|
|
79818
|
+
function readXlsx(buf, opts2 = {}) {
|
|
79819
|
+
let files;
|
|
79820
|
+
try {
|
|
79821
|
+
files = readZip(buf, { maxEntries: 100, maxTotalBytes: 40 * 1024 * 1024 });
|
|
79822
|
+
} catch (e) {
|
|
79823
|
+
if (e instanceof ZipError) throw new XlsxError(`bukan berkas XLSX yang valid: ${e.message}`);
|
|
79824
|
+
throw e;
|
|
79825
|
+
}
|
|
79826
|
+
const text2 = (name2) => files.get(name2)?.toString("utf8");
|
|
79827
|
+
let target3 = "xl/worksheets/sheet1.xml";
|
|
79828
|
+
const wb = text2("xl/workbook.xml");
|
|
79829
|
+
const rels = text2("xl/_rels/workbook.xml.rels");
|
|
79830
|
+
if (wb && rels) {
|
|
79831
|
+
const all = [...wb.matchAll(/<sheet\b([^>]*)>/g)].map((m) => attrs(m[1]));
|
|
79832
|
+
const want = opts2.sheet?.trim().toLowerCase();
|
|
79833
|
+
if (want && opts2.required && !all.some((a) => (a.name ?? "").trim().toLowerCase() === want)) throw new XlsxError(`lembar "${opts2.sheet}" wajib ada \u2014 gunakan template QA Excel`);
|
|
79834
|
+
const chosen = (want ? all.find((a) => (a.name ?? "").trim().toLowerCase() === want) : void 0) ?? all[0];
|
|
79835
|
+
const rid = chosen?.["r:id"];
|
|
79836
|
+
for (const m of rels.matchAll(/<Relationship\b([^>]*)>/g)) {
|
|
79837
|
+
const a = attrs(m[1]);
|
|
79838
|
+
if (rid && a.Id === rid && a.Target) {
|
|
79839
|
+
target3 = a.Target.startsWith("/") ? a.Target.slice(1) : `xl/${a.Target}`;
|
|
79840
|
+
break;
|
|
79841
|
+
}
|
|
79842
|
+
}
|
|
79843
|
+
}
|
|
79844
|
+
const sheet = text2(target3);
|
|
79845
|
+
if (sheet === void 0) throw new XlsxError("lembar (worksheet) tak ditemukan di XLSX");
|
|
79846
|
+
const shared = [];
|
|
79847
|
+
const sst = text2("xl/sharedStrings.xml");
|
|
79848
|
+
if (sst) for (const m of sst.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g)) shared.push(textOf(m[1]));
|
|
79849
|
+
const rows = [];
|
|
79850
|
+
let rowSeq = 0;
|
|
79851
|
+
for (const rm9 of sheet.matchAll(/<row\b([^>]*?)(?:\/>|>([\s\S]*?)<\/row>)/g)) {
|
|
79852
|
+
rowSeq++;
|
|
79853
|
+
const rn = Number(attrs(rm9[1]).r) || rowSeq;
|
|
79854
|
+
if (rn > MAX_ROWS) throw new XlsxError(`terlalu banyak baris (maks ${MAX_ROWS})`);
|
|
79855
|
+
const row = rows[rn - 1] ?? [];
|
|
79856
|
+
let colSeq = -1;
|
|
79857
|
+
for (const cm of (rm9[2] ?? "").matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
|
|
79858
|
+
const a = attrs(cm[1]);
|
|
79859
|
+
const ref = /^([A-Z]+)\d+$/.exec(a.r ?? "");
|
|
79860
|
+
const ci = ref ? colIndex(ref[1]) : colSeq + 1;
|
|
79861
|
+
colSeq = ci;
|
|
79862
|
+
if (ci >= MAX_COLS) throw new XlsxError(`terlalu banyak kolom (maks ${MAX_COLS})`);
|
|
79863
|
+
const inner = cm[2] ?? "";
|
|
79864
|
+
const v = /<v\b[^>]*>([\s\S]*?)<\/v>/.exec(inner)?.[1];
|
|
79865
|
+
let val = "";
|
|
79866
|
+
if (a.t === "s") val = shared[Number(v)] ?? "";
|
|
79867
|
+
else if (a.t === "inlineStr") val = textOf(inner);
|
|
79868
|
+
else if (a.t === "str") val = v === void 0 ? "" : unesc2(v);
|
|
79869
|
+
else if (a.t === "b") val = v === "1" ? "TRUE" : "FALSE";
|
|
79870
|
+
else if (a.t === "e") val = "";
|
|
79871
|
+
else val = v === void 0 ? "" : unesc2(v);
|
|
79872
|
+
while (row.length < ci) row.push("");
|
|
79873
|
+
row[ci] = val;
|
|
79874
|
+
}
|
|
79875
|
+
while (rows.length < rn - 1) rows.push([]);
|
|
79876
|
+
rows[rn - 1] = row;
|
|
79877
|
+
}
|
|
79878
|
+
return rows;
|
|
79879
|
+
}
|
|
79880
|
+
|
|
79881
|
+
// src/services/qa-workbook.ts
|
|
79882
|
+
var GUIDE2 = [
|
|
79883
|
+
["Bagian", "Cara mengisi"],
|
|
79884
|
+
["Mulai di sini", "Isi Ringkasan, lalu Test case dan Temuan. Hapus atau ganti baris contoh. Sheet Panduan tidak diimpor."],
|
|
79885
|
+
["Ringkasan", "Judul wajib. Kolom lain boleh kosong. Status: draft, submitted, closed. Keputusan: go (siap), no-go (belum siap), conditional (siap dengan catatan); wajib untuk submitted/closed."],
|
|
79886
|
+
["Test case", "Satu baris = satu pengujian. Status: belum, lulus, gagal, terblokir, dilewati (atau todo, pass, fail, blocked, skipped). Langkah: satu per baris di dalam sel (Alt+Enter)."],
|
|
79887
|
+
["Temuan", "Satu baris = satu masalah. Severity: blocker (tidak bisa dipakai), critical (kritis), major (fitur terganggu), minor (gangguan kecil), trivial (tampilan). Prioritas: P0 segera, P1 tinggi, P2 normal, P3 rendah."],
|
|
79888
|
+
["Lampiran", "Satu baris = satu berkas. Pemilik: Laporan, TC-01, atau F-01 sesuai Kode. Berkas: nama screenshot/log yang dipilih bersama Excel, atau attachments/nama.png dalam ZIP. Maks 10 MB/berkas, 30 berkas dan 100 MB/laporan."],
|
|
79889
|
+
["Impor dengan lampiran", "Pilih Excel dan berkas lampiran bersama-sama di dashboard, atau ZIP berisi report.xlsx dan folder attachments/. Gambar yang hanya ditempel di sel Excel tidak diimpor; sertakan berkas gambarnya."],
|
|
79890
|
+
["Ref", "Biarkan Ref laporan/test case/temuan hasil ekspor untuk memperbarui laporan asal. Kosongkan untuk entri baru. Kode harus unik; Test case pada Temuan merujuk Kode TC yang ada."],
|
|
79891
|
+
["Lingkungan", "Satu per baris: os=Windows 11, browser=Chrome, device=Laptop, url=https://\u2026 ."]
|
|
79892
|
+
];
|
|
79893
|
+
function writeQaWorkbook(d, paths2 = {}) {
|
|
79894
|
+
const owner = (type, id2) => type === "report" ? "Laporan" : type === "case" ? d.cases.find((c) => c.id === id2)?.code ?? "" : d.findings.find((f) => f.id === id2)?.code ?? "";
|
|
79895
|
+
return writeXlsx([
|
|
79896
|
+
{ name: "Panduan", rows: GUIDE2, widths: [24, 100] },
|
|
79897
|
+
{ name: "Ringkasan", widths: [24, 80], dropdowns: [{ range: "B7", values: QA_REPORT_STATUSES }, { range: "B8", values: QA_VERDICTS }], rows: [
|
|
79898
|
+
["Kolom", "Nilai"],
|
|
79899
|
+
["Judul", d.title],
|
|
79900
|
+
["Build / versi", d.buildVersion],
|
|
79901
|
+
["Penguji", d.tester],
|
|
79902
|
+
["Cakupan", d.scope],
|
|
79903
|
+
["Ringkasan", d.summary],
|
|
79904
|
+
["Status", d.status],
|
|
79905
|
+
["Keputusan", d.verdict ?? ""],
|
|
79906
|
+
["Lingkungan", Object.entries(d.environment).map(([k, v]) => `${k}=${v}`).join("\n")],
|
|
79907
|
+
["Ref", d.id]
|
|
79908
|
+
] },
|
|
79909
|
+
{ name: "Test case", dropdowns: [{ range: "F2:F5000", values: ["belum", "lulus", "gagal", "terblokir", "dilewati", "todo", "pass", "fail", "blocked", "skipped"] }], widths: [12, 34, 40, 32, 32, 14, 28], rows: [
|
|
79910
|
+
["Kode", "Judul", "Langkah", "Diharapkan", "Aktual", "Status", "Ref"],
|
|
79911
|
+
...d.cases.map((c) => [c.code, c.title, c.steps, c.expected, c.actual, c.status, c.id])
|
|
79912
|
+
] },
|
|
79913
|
+
{ name: "Temuan", dropdowns: [{ range: "C2:C5000", values: QA_SEVERITIES }, { range: "D2:D5000", values: QA_PRIORITIES }, { range: "E2:E5000", values: QA_FINDING_STATUSES }], widths: [12, 34, 14, 12, 14, 20, 14, 40, 32, 32, 14, 16, 28, 28], rows: [
|
|
79914
|
+
["Kode", "Judul", "Severity", "Prioritas", "Status", "Area", "Test case", "Langkah", "Expected", "Actual", "Backlog", "Stage backlog", "Lampiran", "Ref"],
|
|
79915
|
+
...d.findings.map((f) => [f.code, f.title, f.severity, f.priority, f.status, f.area, f.caseCode ?? "", f.steps.join("\n"), f.expected, f.actual, f.backlogId ?? "", f.spec?.stage ?? "", d.attachments.filter((a) => a.ownerType === "finding" && a.ownerId === f.id).map((a) => a.filename).join(", "), f.id])
|
|
79916
|
+
] },
|
|
79917
|
+
{ name: "Lampiran", widths: [20, 70], rows: [
|
|
79918
|
+
["Pemilik", "Berkas"],
|
|
79919
|
+
...d.attachments.map((a) => [owner(a.ownerType, a.ownerId), paths2[a.id] ?? a.filename])
|
|
79920
|
+
] }
|
|
79921
|
+
]);
|
|
79922
|
+
}
|
|
79923
|
+
function qaTemplateWorkbook() {
|
|
79924
|
+
const p3 = parseQaMarkdown(qaTemplateMarkdown());
|
|
79925
|
+
const at = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
79926
|
+
return writeQaWorkbook({
|
|
79927
|
+
...p3,
|
|
79928
|
+
id: "",
|
|
79929
|
+
projectId: "",
|
|
79930
|
+
code: "QA-001",
|
|
79931
|
+
createdAt: at,
|
|
79932
|
+
updatedAt: at,
|
|
79933
|
+
cases: p3.cases.map((c, i) => ({ ...c, id: "", reportId: "", order: i + 1, createdAt: at, updatedAt: at })),
|
|
79934
|
+
findings: p3.findings.map((f) => ({ ...f, id: "", reportId: "", backlogId: null, spec: null, createdAt: at, updatedAt: at })),
|
|
79935
|
+
attachments: [],
|
|
79936
|
+
stats: qaStats(p3.cases, p3.findings)
|
|
79937
|
+
});
|
|
79938
|
+
}
|
|
79939
|
+
var norm3 = (s2) => s2.trim().toLowerCase();
|
|
79940
|
+
function fail3(sheet, row, message) {
|
|
79941
|
+
throw new XlsxError(`${sheet}, baris ${row}: ${message}`);
|
|
79942
|
+
}
|
|
79943
|
+
var choice = (s2, allowed, sheet, row) => {
|
|
79944
|
+
const v = allowed.find((a) => norm3(a) === norm3(s2));
|
|
79945
|
+
return v ?? fail3(sheet, row, `nilai "${s2}" tidak dikenal; pilih ${allowed.join(", ")}`);
|
|
79946
|
+
};
|
|
79947
|
+
function parseQaWorkbook(buf) {
|
|
79948
|
+
const read2 = (sheet) => readXlsx(buf, { sheet, required: true });
|
|
79949
|
+
const summary = read2("Ringkasan");
|
|
79950
|
+
if (norm3(summary[0]?.[0] ?? "") !== "kolom" || norm3(summary[0]?.[1] ?? "") !== "nilai") fail3("Ringkasan", 1, "butuh kolom Kolom dan Nilai");
|
|
79951
|
+
const fields = new Map(summary.slice(1).map((r) => [norm3(r[0] ?? ""), r[1] ?? ""]));
|
|
79952
|
+
const val = (k) => fields.get(norm3(k)) ?? "";
|
|
79953
|
+
const rowOf = (k) => Math.max(1, summary.findIndex((r) => norm3(r[0] ?? "") === norm3(k)) + 1);
|
|
79954
|
+
if (!val("Judul").trim()) fail3("Ringkasan", rowOf("Judul"), "Judul wajib diisi");
|
|
79955
|
+
const environment = /* @__PURE__ */ Object.create(null);
|
|
79956
|
+
for (const line of val("Lingkungan").split("\n").filter((l) => l.trim())) {
|
|
79957
|
+
const match = /^\s*([^=:]+)[=:]\s*(.*)$/.exec(line);
|
|
79958
|
+
if (!match) fail3("Ringkasan", rowOf("Lingkungan"), "Lingkungan: gunakan kunci=nilai, satu per baris");
|
|
79959
|
+
environment[match[1].trim()] = match[2];
|
|
79960
|
+
}
|
|
79961
|
+
const base3 = zCreateQaReport.safeParse({
|
|
79962
|
+
title: val("Judul"),
|
|
79963
|
+
buildVersion: val("Build / versi"),
|
|
79964
|
+
tester: val("Penguji"),
|
|
79965
|
+
scope: val("Cakupan"),
|
|
79966
|
+
summary: val("Ringkasan"),
|
|
79967
|
+
environment,
|
|
79968
|
+
verdict: val("Keputusan") ? choice(val("Keputusan"), QA_VERDICTS, "Ringkasan", rowOf("Keputusan")) : null
|
|
79969
|
+
});
|
|
79970
|
+
if (!base3.success) fail3("Ringkasan", 1, base3.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; "));
|
|
79971
|
+
const parsed = {
|
|
79972
|
+
...base3.data,
|
|
79973
|
+
reportId: val("Ref") || null,
|
|
79974
|
+
status: choice(val("Status") || "draft", QA_REPORT_STATUSES, "Ringkasan", rowOf("Status")),
|
|
79975
|
+
cases: [],
|
|
79976
|
+
findings: [],
|
|
79977
|
+
attachments: []
|
|
79978
|
+
};
|
|
79979
|
+
let cases;
|
|
79980
|
+
try {
|
|
79981
|
+
cases = parseCaseRows(read2("Test case"));
|
|
79982
|
+
} catch (e) {
|
|
79983
|
+
if (e instanceof QaTableError) throw new XlsxError(`Test case, ${e.message}`);
|
|
79984
|
+
throw e;
|
|
79985
|
+
}
|
|
79986
|
+
const codes = /* @__PURE__ */ new Set();
|
|
79987
|
+
const refs2 = /* @__PURE__ */ new Set();
|
|
79988
|
+
for (const [i, c] of cases.entries()) {
|
|
79989
|
+
const code = c.code || `TC-${String(i + 1).padStart(2, "0")}`;
|
|
79990
|
+
if (!/^TC-\d+$/.test(code) || codes.has(code)) fail3("Test case", c.row, "Kode harus unik dan berformat TC-01");
|
|
79991
|
+
if (c.ref && refs2.has(c.ref)) fail3("Test case", c.row, "Ref berulang");
|
|
79992
|
+
codes.add(code);
|
|
79993
|
+
if (c.ref) refs2.add(c.ref);
|
|
79994
|
+
const result = zCreateQaCase.safeParse({ title: c.title, steps: c.steps ?? "", expected: c.expected ?? "", actual: c.actual ?? "", status: c.status ?? "todo" });
|
|
79995
|
+
if (!result.success) fail3("Test case", c.row, result.error.issues.map((e) => `${e.path}: ${e.message}`).join("; "));
|
|
79996
|
+
parsed.cases.push({ ...result.data, id: c.ref, code, attachments: [] });
|
|
79997
|
+
}
|
|
79998
|
+
const rows = read2("Temuan");
|
|
79999
|
+
const headers = rows[0]?.map(norm3) ?? [];
|
|
80000
|
+
for (const h2 of ["judul", "severity", "prioritas", "langkah", "expected", "actual"]) if (!headers.includes(h2)) fail3("Temuan", 1, `kolom ${h2} wajib ada`);
|
|
80001
|
+
const findingCodes = /* @__PURE__ */ new Set();
|
|
80002
|
+
const findingRefs = /* @__PURE__ */ new Set();
|
|
80003
|
+
for (let i = 1; i < rows.length; i++) {
|
|
80004
|
+
const r = rows[i];
|
|
80005
|
+
if (r.every((v) => !v.trim())) continue;
|
|
80006
|
+
const get = (k) => r[headers.indexOf(norm3(k))] ?? "";
|
|
80007
|
+
const code = get("Kode") || `F-${String(parsed.findings.length + 1).padStart(2, "0")}`;
|
|
80008
|
+
if (!/^F-\d+$/.test(code) || findingCodes.has(code)) fail3("Temuan", i + 1, "Kode harus unik dan berformat F-01");
|
|
80009
|
+
findingCodes.add(code);
|
|
80010
|
+
const id2 = get("Ref") || null;
|
|
80011
|
+
if (id2 && findingRefs.has(id2)) fail3("Temuan", i + 1, "Ref berulang");
|
|
80012
|
+
if (id2) findingRefs.add(id2);
|
|
80013
|
+
const caseCode = get("Test case").trim() || null;
|
|
80014
|
+
if (caseCode && !codes.has(caseCode)) fail3("Temuan", i + 1, `Test case ${caseCode} tidak ditemukan`);
|
|
80015
|
+
const status2 = choice(get("Status") || "open", QA_FINDING_STATUSES, "Temuan", i + 1);
|
|
80016
|
+
const result = zCreateQaFinding.safeParse({
|
|
80017
|
+
title: get("Judul"),
|
|
80018
|
+
severity: choice(get("Severity") || "major", QA_SEVERITIES, "Temuan", i + 1),
|
|
80019
|
+
priority: choice(get("Prioritas") || "P2", QA_PRIORITIES, "Temuan", i + 1),
|
|
80020
|
+
area: get("Area"),
|
|
80021
|
+
steps: get("Langkah").split("\n").map((s2) => s2.trim().replace(/^\d+[.)]\s+/, "")).filter(Boolean),
|
|
80022
|
+
expected: get("Expected"),
|
|
80023
|
+
actual: get("Actual"),
|
|
80024
|
+
status: status2 === "sent" ? "open" : status2
|
|
80025
|
+
});
|
|
80026
|
+
if (!result.success) fail3("Temuan", i + 1, result.error.issues.map((e) => `${e.path}: ${e.message}`).join("; "));
|
|
80027
|
+
parsed.findings.push({ ...result.data, status: status2, id: id2, code, caseId: null, caseCode, attachments: [] });
|
|
80028
|
+
}
|
|
80029
|
+
const attachments = read2("Lampiran");
|
|
80030
|
+
if (norm3(attachments[0]?.[0] ?? "") !== "pemilik" || norm3(attachments[0]?.[1] ?? "") !== "berkas") fail3("Lampiran", 1, "butuh kolom Pemilik dan Berkas");
|
|
80031
|
+
for (let i = 1; i < attachments.length; i++) {
|
|
80032
|
+
const [owner = "", path = ""] = attachments[i];
|
|
80033
|
+
if (!owner.trim() && !path.trim()) continue;
|
|
80034
|
+
if (!path.trim() || path.includes("\\") || path.startsWith("/") || path.split("/").some((p3) => p3 === ".." || p3 === "." || !p3)) fail3("Lampiran", i + 1, "Berkas harus nama berkas atau path relatif yang aman");
|
|
80035
|
+
const target3 = norm3(owner) === "laporan" ? parsed.attachments : parsed.cases.find((c) => c.code === owner.trim())?.attachments ?? parsed.findings.find((f) => f.code === owner.trim())?.attachments;
|
|
80036
|
+
if (!target3) fail3("Lampiran", i + 1, `Pemilik "${owner}" tidak ditemukan; gunakan Laporan atau Kode TC/F yang ada`);
|
|
80037
|
+
target3.push(path.trim());
|
|
80038
|
+
}
|
|
80039
|
+
return parsed;
|
|
80040
|
+
}
|
|
80041
|
+
|
|
80042
|
+
// src/services/qa-cases-import.ts
|
|
80043
|
+
init_src();
|
|
80044
|
+
init_db();
|
|
80045
|
+
|
|
79742
80046
|
// src/services/qa-transfer.ts
|
|
80047
|
+
init_src();
|
|
80048
|
+
init_db();
|
|
80049
|
+
import { basename as basename10 } from "node:path";
|
|
80050
|
+
init_qa_attachment_transfer();
|
|
80051
|
+
init_sync_notify();
|
|
79743
80052
|
var QaImportError = class extends Error {
|
|
79744
80053
|
constructor(status2, message) {
|
|
79745
80054
|
super(message);
|
|
@@ -79779,23 +80088,31 @@ async function exportReport(projectId, reportId) {
|
|
|
79779
80088
|
present.push(a);
|
|
79780
80089
|
}
|
|
79781
80090
|
const markdown = renderQaMarkdown({ ...full, attachments: present }, paths2);
|
|
79782
|
-
const zip = writeZip([{ name: "report.md", data: Buffer.from(markdown, "utf8"), deflate: true }, ...files]);
|
|
80091
|
+
const zip = writeZip([{ name: "report.xlsx", data: writeQaWorkbook({ ...full, attachments: present }, paths2), deflate: true }, { name: "report.md", data: Buffer.from(markdown, "utf8"), deflate: true }, ...files]);
|
|
79783
80092
|
return { code: full.code, markdown, zip };
|
|
79784
80093
|
}
|
|
79785
|
-
async function importReport(projectId, file) {
|
|
80094
|
+
async function importReport(projectId, file, companions = []) {
|
|
79786
80095
|
if (!await prisma.project.findUnique({ where: { id: projectId }, select: { id: true } }))
|
|
79787
80096
|
throw new QaImportError(404, "project tak ditemukan");
|
|
79788
|
-
let markdown;
|
|
80097
|
+
let markdown = "";
|
|
80098
|
+
let workbook;
|
|
79789
80099
|
let assets = /* @__PURE__ */ new Map();
|
|
79790
80100
|
let dir2 = "";
|
|
79791
80101
|
const isZip2 = file.buf.length >= 4 && file.buf.readUInt32LE(0) === 67324752;
|
|
79792
|
-
if (
|
|
80102
|
+
if (/\.xlsx$/i.test(file.name)) {
|
|
80103
|
+
workbook = file.buf;
|
|
80104
|
+
} else if (isZip2) {
|
|
79793
80105
|
try {
|
|
79794
80106
|
const entries3 = readZip(file.buf);
|
|
80107
|
+
const xlsxNames = [...entries3.keys()].filter((k) => /\.xlsx$/i.test(k) && !/(^|\/)attachments\//.test(k));
|
|
80108
|
+
if (xlsxNames.length > 1) throw new QaImportError(400, "ZIP harus memuat satu laporan Excel saja");
|
|
80109
|
+
const xlsxName = xlsxNames[0];
|
|
79795
80110
|
const mdName = [...entries3.keys()].find((k) => /(^|\/)report\.md$/i.test(k)) ?? [...entries3.keys()].find((k) => /\.md$/i.test(k) && !/(^|\/)attachments\//.test(k));
|
|
79796
|
-
if (!mdName) throw new QaImportError(400, "ZIP tak memuat report.md");
|
|
79797
|
-
|
|
79798
|
-
|
|
80111
|
+
if (!mdName && !xlsxName) throw new QaImportError(400, "ZIP tak memuat report.xlsx atau report.md");
|
|
80112
|
+
const reportName = xlsxName ?? mdName;
|
|
80113
|
+
if (xlsxName) workbook = entries3.get(xlsxName);
|
|
80114
|
+
else markdown = entries3.get(mdName).toString("utf8");
|
|
80115
|
+
dir2 = reportName.includes("/") ? reportName.slice(0, reportName.lastIndexOf("/") + 1) : "";
|
|
79799
80116
|
assets = entries3;
|
|
79800
80117
|
} catch (e) {
|
|
79801
80118
|
if (e instanceof ZipError) throw new QaImportError(400, `ZIP tak valid: ${e.message}`);
|
|
@@ -79805,10 +80122,14 @@ async function importReport(projectId, file) {
|
|
|
79805
80122
|
markdown = file.buf.toString("utf8");
|
|
79806
80123
|
}
|
|
79807
80124
|
let parsed;
|
|
80125
|
+
for (const c of companions) {
|
|
80126
|
+
if (assets.has(c.name)) throw new QaImportError(400, `nama lampiran berulang: ${c.name}`);
|
|
80127
|
+
assets.set(c.name, c.buf);
|
|
80128
|
+
}
|
|
79808
80129
|
try {
|
|
79809
|
-
parsed = parseQaMarkdown(markdown);
|
|
80130
|
+
parsed = workbook ? parseQaWorkbook(workbook) : parseQaMarkdown(markdown);
|
|
79810
80131
|
} catch (e) {
|
|
79811
|
-
if (e instanceof QaMarkdownError) throw new QaImportError(400, e.message);
|
|
80132
|
+
if (e instanceof QaMarkdownError || e instanceof XlsxError) throw new QaImportError(400, e.message);
|
|
79812
80133
|
throw e;
|
|
79813
80134
|
}
|
|
79814
80135
|
if ((parsed.status === "submitted" || parsed.status === "closed") && !parsed.verdict)
|
|
@@ -79872,13 +80193,13 @@ async function importReport(projectId, file) {
|
|
|
79872
80193
|
for (const o of owners) {
|
|
79873
80194
|
const uploads = [];
|
|
79874
80195
|
for (const path of o.paths) {
|
|
79875
|
-
const buf = assets.get(dir2 + path);
|
|
79876
80196
|
const name2 = originalName(path);
|
|
80197
|
+
if (have.some((h2) => h2.ownerType === o.ownerType && h2.ownerId === o.ownerId && h2.filename === name2)) continue;
|
|
80198
|
+
const buf = assets.get(dir2 + path) ?? assets.get(path);
|
|
79877
80199
|
if (!buf) {
|
|
79878
80200
|
rejected.push({ filename: name2, reason: "missing" });
|
|
79879
80201
|
continue;
|
|
79880
80202
|
}
|
|
79881
|
-
if (have.some((h2) => h2.ownerType === o.ownerType && h2.ownerId === o.ownerId && h2.filename === name2)) continue;
|
|
79882
80203
|
const mime = MIME_BY_EXT[name2.split(".").pop()?.toLowerCase() ?? ""];
|
|
79883
80204
|
if (!mime) {
|
|
79884
80205
|
rejected.push({ filename: name2, reason: "type" });
|
|
@@ -79891,6 +80212,8 @@ async function importReport(projectId, file) {
|
|
|
79891
80212
|
saved += r.saved.length;
|
|
79892
80213
|
rejected.push(...r.rejected);
|
|
79893
80214
|
}
|
|
80215
|
+
const referenced = new Set(owners.flatMap((o) => o.paths));
|
|
80216
|
+
for (const c of companions) if (!referenced.has(c.name)) rejected.push({ filename: c.name, reason: "Tidak tercantum pada sheet Lampiran atau tautan lampiran laporan" });
|
|
79894
80217
|
return {
|
|
79895
80218
|
reportId: out4.id,
|
|
79896
80219
|
created: !existing,
|
|
@@ -79902,149 +80225,6 @@ async function importReport(projectId, file) {
|
|
|
79902
80225
|
|
|
79903
80226
|
// src/services/qa-cases-import.ts
|
|
79904
80227
|
init_sync_notify();
|
|
79905
|
-
|
|
79906
|
-
// src/services/ooxml.ts
|
|
79907
|
-
var xmlEsc = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
79908
|
-
var xmlClean = (s2) => s2.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
79909
|
-
var XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
79910
|
-
|
|
79911
|
-
// src/services/xlsx.ts
|
|
79912
|
-
var XlsxError = class extends Error {
|
|
79913
|
-
};
|
|
79914
|
-
var MAX_ROWS = 5e3;
|
|
79915
|
-
var MAX_COLS = 60;
|
|
79916
|
-
var MAX_CELL = 32767;
|
|
79917
|
-
var esc2 = xmlEsc;
|
|
79918
|
-
var clean2 = (s2) => xmlClean(s2).slice(0, MAX_CELL);
|
|
79919
|
-
var colLetter = (i) => {
|
|
79920
|
-
let s2 = "";
|
|
79921
|
-
for (let n2 = i + 1; n2 > 0; n2 = Math.floor((n2 - 1) / 26)) s2 = String.fromCharCode(65 + (n2 - 1) % 26) + s2;
|
|
79922
|
-
return s2;
|
|
79923
|
-
};
|
|
79924
|
-
var colIndex = (letters) => [...letters].reduce((n2, c) => n2 * 26 + (c.charCodeAt(0) - 64), 0) - 1;
|
|
79925
|
-
var sheetName = (n2, i) => n2.replace(/[[\]:*?/\\]/g, "_").trim().slice(0, 31) || `Sheet${i + 1}`;
|
|
79926
|
-
var NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
79927
|
-
var NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
79928
|
-
var HEAD = XML_HEAD;
|
|
79929
|
-
function writeXlsx(sheets) {
|
|
79930
|
-
const parts = [];
|
|
79931
|
-
const add = (name2, body) => parts.push({ name: name2, data: Buffer.from(HEAD + body, "utf8"), deflate: true });
|
|
79932
|
-
add(
|
|
79933
|
-
"[Content_Types].xml",
|
|
79934
|
-
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' + sheets.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join("") + "</Types>"
|
|
79935
|
-
);
|
|
79936
|
-
add(
|
|
79937
|
-
"_rels/.rels",
|
|
79938
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'
|
|
79939
|
-
);
|
|
79940
|
-
add(
|
|
79941
|
-
"xl/workbook.xml",
|
|
79942
|
-
`<workbook xmlns="${NS}" xmlns:r="${NS_R}"><sheets>` + sheets.map((s2, i) => `<sheet name="${esc2(sheetName(s2.name, i))}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets></workbook>"
|
|
79943
|
-
);
|
|
79944
|
-
add(
|
|
79945
|
-
"xl/_rels/workbook.xml.rels",
|
|
79946
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + sheets.map((_, i) => `<Relationship Id="rId${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`).join("") + `<Relationship Id="rId${sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`
|
|
79947
|
-
);
|
|
79948
|
-
add(
|
|
79949
|
-
"xl/styles.xml",
|
|
79950
|
-
`<styleSheet xmlns="${NS}"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFF1E6CC"/><bgColor indexed="64"/></patternFill></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="3"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`
|
|
79951
|
-
);
|
|
79952
|
-
sheets.forEach((s2, si) => {
|
|
79953
|
-
const cols = s2.widths?.length ? `<cols>${s2.widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join("")}</cols>` : "";
|
|
79954
|
-
const data = s2.rows.map((r, ri) => {
|
|
79955
|
-
const cells = r.map((v, ci) => {
|
|
79956
|
-
const t = clean2(v);
|
|
79957
|
-
return t === "" ? "" : `<c r="${colLetter(ci)}${ri + 1}" s="${ri === 0 ? 1 : 2}" t="inlineStr"><is><t xml:space="preserve">${esc2(t)}</t></is></c>`;
|
|
79958
|
-
}).join("");
|
|
79959
|
-
return `<row r="${ri + 1}">${cells}</row>`;
|
|
79960
|
-
}).join("");
|
|
79961
|
-
add(
|
|
79962
|
-
`xl/worksheets/sheet${si + 1}.xml`,
|
|
79963
|
-
`<worksheet xmlns="${NS}"><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${data}</sheetData></worksheet>`
|
|
79964
|
-
);
|
|
79965
|
-
});
|
|
79966
|
-
return writeZip(parts);
|
|
79967
|
-
}
|
|
79968
|
-
function unesc2(s2) {
|
|
79969
|
-
return s2.replace(/&(#x[0-9a-fA-F]+|#\d+|lt|gt|amp|quot|apos);/g, (_, e) => {
|
|
79970
|
-
if (e === "lt") return "<";
|
|
79971
|
-
if (e === "gt") return ">";
|
|
79972
|
-
if (e === "amp") return "&";
|
|
79973
|
-
if (e === "quot") return '"';
|
|
79974
|
-
if (e === "apos") return "'";
|
|
79975
|
-
const cp = e[1] === "x" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
79976
|
-
return Number.isFinite(cp) && cp > 0 && cp <= 1114111 ? String.fromCodePoint(cp) : "";
|
|
79977
|
-
});
|
|
79978
|
-
}
|
|
79979
|
-
var attrs = (s2) => {
|
|
79980
|
-
const out4 = {};
|
|
79981
|
-
for (const m of s2.matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) out4[m[1]] = unesc2(m[2]);
|
|
79982
|
-
return out4;
|
|
79983
|
-
};
|
|
79984
|
-
var textOf = (xmlFragment) => [...xmlFragment.replace(/<rPh\b[\s\S]*?<\/rPh>/g, "").matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)].map((m) => unesc2(m[1])).join("");
|
|
79985
|
-
function readXlsx(buf, opts2 = {}) {
|
|
79986
|
-
let files;
|
|
79987
|
-
try {
|
|
79988
|
-
files = readZip(buf, { maxEntries: 100, maxTotalBytes: 40 * 1024 * 1024 });
|
|
79989
|
-
} catch (e) {
|
|
79990
|
-
if (e instanceof ZipError) throw new XlsxError(`bukan berkas XLSX yang valid: ${e.message}`);
|
|
79991
|
-
throw e;
|
|
79992
|
-
}
|
|
79993
|
-
const text2 = (name2) => files.get(name2)?.toString("utf8");
|
|
79994
|
-
let target3 = "xl/worksheets/sheet1.xml";
|
|
79995
|
-
const wb = text2("xl/workbook.xml");
|
|
79996
|
-
const rels = text2("xl/_rels/workbook.xml.rels");
|
|
79997
|
-
if (wb && rels) {
|
|
79998
|
-
const all = [...wb.matchAll(/<sheet\b([^>]*)>/g)].map((m) => attrs(m[1]));
|
|
79999
|
-
const want = opts2.sheet?.trim().toLowerCase();
|
|
80000
|
-
const chosen = (want ? all.find((a) => (a.name ?? "").trim().toLowerCase() === want) : void 0) ?? all[0];
|
|
80001
|
-
const rid = chosen?.["r:id"];
|
|
80002
|
-
for (const m of rels.matchAll(/<Relationship\b([^>]*)>/g)) {
|
|
80003
|
-
const a = attrs(m[1]);
|
|
80004
|
-
if (rid && a.Id === rid && a.Target) {
|
|
80005
|
-
target3 = a.Target.startsWith("/") ? a.Target.slice(1) : `xl/${a.Target}`;
|
|
80006
|
-
break;
|
|
80007
|
-
}
|
|
80008
|
-
}
|
|
80009
|
-
}
|
|
80010
|
-
const sheet = text2(target3);
|
|
80011
|
-
if (sheet === void 0) throw new XlsxError("lembar (worksheet) tak ditemukan di XLSX");
|
|
80012
|
-
const shared = [];
|
|
80013
|
-
const sst = text2("xl/sharedStrings.xml");
|
|
80014
|
-
if (sst) for (const m of sst.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g)) shared.push(textOf(m[1]));
|
|
80015
|
-
const rows = [];
|
|
80016
|
-
let rowSeq = 0;
|
|
80017
|
-
for (const rm9 of sheet.matchAll(/<row\b([^>]*?)(?:\/>|>([\s\S]*?)<\/row>)/g)) {
|
|
80018
|
-
rowSeq++;
|
|
80019
|
-
const rn = Number(attrs(rm9[1]).r) || rowSeq;
|
|
80020
|
-
if (rn > MAX_ROWS) throw new XlsxError(`terlalu banyak baris (maks ${MAX_ROWS})`);
|
|
80021
|
-
const row = rows[rn - 1] ?? [];
|
|
80022
|
-
let colSeq = -1;
|
|
80023
|
-
for (const cm of (rm9[2] ?? "").matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
|
|
80024
|
-
const a = attrs(cm[1]);
|
|
80025
|
-
const ref = /^([A-Z]+)\d+$/.exec(a.r ?? "");
|
|
80026
|
-
const ci = ref ? colIndex(ref[1]) : colSeq + 1;
|
|
80027
|
-
colSeq = ci;
|
|
80028
|
-
if (ci >= MAX_COLS) throw new XlsxError(`terlalu banyak kolom (maks ${MAX_COLS})`);
|
|
80029
|
-
const inner = cm[2] ?? "";
|
|
80030
|
-
const v = /<v\b[^>]*>([\s\S]*?)<\/v>/.exec(inner)?.[1];
|
|
80031
|
-
let val = "";
|
|
80032
|
-
if (a.t === "s") val = shared[Number(v)] ?? "";
|
|
80033
|
-
else if (a.t === "inlineStr") val = textOf(inner);
|
|
80034
|
-
else if (a.t === "str") val = v === void 0 ? "" : unesc2(v);
|
|
80035
|
-
else if (a.t === "b") val = v === "1" ? "TRUE" : "FALSE";
|
|
80036
|
-
else if (a.t === "e") val = "";
|
|
80037
|
-
else val = v === void 0 ? "" : unesc2(v);
|
|
80038
|
-
while (row.length < ci) row.push("");
|
|
80039
|
-
row[ci] = val;
|
|
80040
|
-
}
|
|
80041
|
-
while (rows.length < rn - 1) rows.push([]);
|
|
80042
|
-
rows[rn - 1] = row;
|
|
80043
|
-
}
|
|
80044
|
-
return rows;
|
|
80045
|
-
}
|
|
80046
|
-
|
|
80047
|
-
// src/services/qa-cases-import.ts
|
|
80048
80228
|
var isZip = (b) => b.length >= 4 && b.readUInt32LE(0) === 67324752;
|
|
80049
80229
|
async function importCases(projectId, reportId, file) {
|
|
80050
80230
|
const report = await prisma.qaReport.findFirst({ where: { id: reportId, projectId }, select: { id: true, status: true } });
|
|
@@ -80400,44 +80580,6 @@ var OFFICE_MIME = {
|
|
|
80400
80580
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
80401
80581
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
80402
80582
|
};
|
|
80403
|
-
var sheetSummary = (d) => {
|
|
80404
|
-
const s2 = d.stats;
|
|
80405
|
-
return [
|
|
80406
|
-
["Kolom", "Nilai"],
|
|
80407
|
-
["Kode", d.code],
|
|
80408
|
-
["Judul", d.title],
|
|
80409
|
-
["Build / versi", d.buildVersion],
|
|
80410
|
-
["Penguji", d.tester],
|
|
80411
|
-
["Status", d.status],
|
|
80412
|
-
["Keputusan", d.verdict ?? "belum diputuskan"],
|
|
80413
|
-
["Cakupan", d.scope],
|
|
80414
|
-
["Lingkungan", Object.entries(d.environment).map(([k, v]) => `${k}: ${v}`).join("\n")],
|
|
80415
|
-
["Ringkasan", d.summary],
|
|
80416
|
-
["Test case", `${s2.cases.total} \xB7 pass ${s2.cases.pass} \xB7 fail ${s2.cases.fail} \xB7 blocked ${s2.cases.blocked} \xB7 skipped ${s2.cases.skipped} \xB7 todo ${s2.cases.todo} \xB7 pass rate ${s2.passRate === null ? "-" : `${Math.round(s2.passRate * 100)}%`}`],
|
|
80417
|
-
["Temuan", `${s2.findings.total} \xB7 blocker ${s2.findings.blocker} \xB7 critical ${s2.findings.critical} \xB7 major ${s2.findings.major} \xB7 minor ${s2.findings.minor} \xB7 trivial ${s2.findings.trivial} \xB7 open ${s2.findings.open}`],
|
|
80418
|
-
["Dibuat", d.createdAt],
|
|
80419
|
-
["Diubah", d.updatedAt]
|
|
80420
|
-
];
|
|
80421
|
-
};
|
|
80422
|
-
var sheetFindings = (d) => [
|
|
80423
|
-
["Kode", "Judul", "Severity", "Prioritas", "Status", "Area", "Test case", "Langkah", "Expected", "Actual", "Backlog", "Stage backlog", "Lampiran", "Ref"],
|
|
80424
|
-
...d.findings.map((f) => [
|
|
80425
|
-
f.code,
|
|
80426
|
-
f.title,
|
|
80427
|
-
f.severity,
|
|
80428
|
-
f.priority,
|
|
80429
|
-
f.status,
|
|
80430
|
-
f.area,
|
|
80431
|
-
f.caseCode ?? "",
|
|
80432
|
-
f.steps.map((t, i) => `${i + 1}. ${t}`).join("\n"),
|
|
80433
|
-
f.expected,
|
|
80434
|
-
f.actual,
|
|
80435
|
-
f.backlogId ?? "",
|
|
80436
|
-
f.spec?.stage ?? "",
|
|
80437
|
-
d.attachments.filter((a) => a.ownerType === "finding" && a.ownerId === f.id).map((a) => a.filename).join(", "),
|
|
80438
|
-
f.id
|
|
80439
|
-
])
|
|
80440
|
-
];
|
|
80441
80583
|
async function renderExport(projectId, reportId, format) {
|
|
80442
80584
|
const d = await reportDetail(projectId, reportId);
|
|
80443
80585
|
if (!d) return null;
|
|
@@ -80451,11 +80593,7 @@ async function renderExport(projectId, reportId, format) {
|
|
|
80451
80593
|
return { filename: `${d.code}.pdf`, mime: "application/pdf", body: await writeQaPdf(d, images) };
|
|
80452
80594
|
}
|
|
80453
80595
|
case "xlsx":
|
|
80454
|
-
return { filename: `${d.code}.xlsx`, mime: OFFICE_MIME.xlsx, body:
|
|
80455
|
-
{ name: "Ringkasan", rows: sheetSummary(d), widths: [18, 80] },
|
|
80456
|
-
{ name: "Test case", rows: casesToRows(d.cases), widths: [10, 34, 40, 32, 32, 11, 28] },
|
|
80457
|
-
{ name: "Temuan", rows: sheetFindings(d), widths: [8, 34, 11, 10, 9, 14, 10, 40, 30, 30, 11, 14, 28, 28] }
|
|
80458
|
-
]) };
|
|
80596
|
+
return { filename: `${d.code}.xlsx`, mime: OFFICE_MIME.xlsx, body: writeQaWorkbook(d) };
|
|
80459
80597
|
case "csv":
|
|
80460
80598
|
return { filename: `${d.code}-testcase.csv`, mime: "text/csv; charset=utf-8", body: csvEncode(casesToRows(d.cases)) };
|
|
80461
80599
|
}
|
|
@@ -80464,6 +80602,11 @@ async function renderExport(projectId, reportId, format) {
|
|
|
80464
80602
|
// src/routes/qa-transfer.ts
|
|
80465
80603
|
var IMPORT_MAX = QA_ATTACHMENT_LIMITS.reportBytes + 5 * 1024 * 1024;
|
|
80466
80604
|
async function qaTransfer(app3) {
|
|
80605
|
+
app3.get("/qa/template.xlsx", async (_req, reply) => {
|
|
80606
|
+
reply.header("content-type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
80607
|
+
reply.header("content-disposition", 'attachment; filename="qa-template.xlsx"');
|
|
80608
|
+
return reply.send(qaTemplateWorkbook());
|
|
80609
|
+
});
|
|
80467
80610
|
app3.get("/qa/template.md", async (_req, reply) => {
|
|
80468
80611
|
reply.header("content-type", "text/markdown; charset=utf-8");
|
|
80469
80612
|
reply.header("content-disposition", 'attachment; filename="qa-template.md"');
|
|
@@ -80496,19 +80639,26 @@ async function qaTransfer(app3) {
|
|
|
80496
80639
|
const { pid } = req.params;
|
|
80497
80640
|
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
80498
80641
|
let file = null;
|
|
80642
|
+
const companions = [];
|
|
80643
|
+
let totalBytes = 0;
|
|
80499
80644
|
try {
|
|
80500
|
-
for await (const part of req.parts({ limits: { fileSize: IMPORT_MAX, files:
|
|
80501
|
-
if (part.type !== "file"
|
|
80645
|
+
for await (const part of req.parts({ limits: { fileSize: IMPORT_MAX, files: 31 } })) {
|
|
80646
|
+
if (part.type !== "file") continue;
|
|
80502
80647
|
const buf = await part.toBuffer();
|
|
80503
80648
|
if (part.file?.truncated) return reply.code(413).send({ error: "berkas terlalu besar" });
|
|
80504
|
-
|
|
80649
|
+
totalBytes += buf.length;
|
|
80650
|
+
if (totalBytes > IMPORT_MAX) return reply.code(413).send({ error: "total unggahan terlalu besar (maks 105 MB)" });
|
|
80651
|
+
const upload = { name: String(part.filename ?? "import"), buf };
|
|
80652
|
+
if (part.fieldname === "attachments") companions.push(upload);
|
|
80653
|
+
else if (file) return reply.code(400).send({ error: "pilih satu laporan Excel, ZIP, atau Markdown" });
|
|
80654
|
+
else file = upload;
|
|
80505
80655
|
}
|
|
80506
80656
|
} catch {
|
|
80507
80657
|
return reply.code(400).send({ error: "unggahan tak valid" });
|
|
80508
80658
|
}
|
|
80509
80659
|
if (!file || file.buf.length === 0) return reply.code(400).send({ error: "tak ada berkas" });
|
|
80510
80660
|
try {
|
|
80511
|
-
const r = await importReport(pid, file);
|
|
80661
|
+
const r = await importReport(pid, file, companions);
|
|
80512
80662
|
return reply.code(r.created ? 201 : 200).send(r);
|
|
80513
80663
|
} catch (e) {
|
|
80514
80664
|
if (e instanceof QaImportError) return reply.code(e.status).send({ error: e.message });
|
|
@@ -80748,7 +80898,7 @@ async function resolveGithubRepo(projectId) {
|
|
|
80748
80898
|
init_config2();
|
|
80749
80899
|
import { execFile as execFile18 } from "node:child_process";
|
|
80750
80900
|
var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
|
|
80751
|
-
var
|
|
80901
|
+
var norm4 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
|
|
80752
80902
|
function issueFromGh(raw) {
|
|
80753
80903
|
return {
|
|
80754
80904
|
number: raw.number,
|
|
@@ -80757,7 +80907,7 @@ function issueFromGh(raw) {
|
|
|
80757
80907
|
authorLogin: raw.author?.login ?? "",
|
|
80758
80908
|
labels: labelNames(raw.labels),
|
|
80759
80909
|
url: raw.url,
|
|
80760
|
-
issueState:
|
|
80910
|
+
issueState: norm4(raw.state),
|
|
80761
80911
|
issueCreatedAt: raw.createdAt,
|
|
80762
80912
|
issueUpdatedAt: raw.updatedAt
|
|
80763
80913
|
};
|
|
@@ -80777,7 +80927,7 @@ function issuesFromRest(raw) {
|
|
|
80777
80927
|
authorLogin: r.user?.login ?? "",
|
|
80778
80928
|
labels: labelNames(r.labels),
|
|
80779
80929
|
url: r.html_url,
|
|
80780
|
-
issueState:
|
|
80930
|
+
issueState: norm4(r.state),
|
|
80781
80931
|
issueCreatedAt: r.created_at,
|
|
80782
80932
|
issueUpdatedAt: r.updated_at
|
|
80783
80933
|
});
|
|
@@ -81443,13 +81593,13 @@ init_ssrf();
|
|
|
81443
81593
|
init_safe_outbound_request();
|
|
81444
81594
|
async function sendOnce(o, deps = {}) {
|
|
81445
81595
|
const started2 = Date.now();
|
|
81446
|
-
const
|
|
81596
|
+
const fail4 = (error, httpStatus = null, gone = false) => ({ ok: false, httpStatus, durationMs: Date.now() - started2, error, gone });
|
|
81447
81597
|
const parsed = validateWebhookUrl(o.endpoint.url);
|
|
81448
|
-
if (!parsed.ok) return
|
|
81598
|
+
if (!parsed.ok) return fail4(`URL tak sah: ${parsed.error}`);
|
|
81449
81599
|
const guard = await checkDestination(parsed.url, o.endpoint.allowPrivate, deps.lookup);
|
|
81450
|
-
if (!guard.ok) return
|
|
81600
|
+
if (!guard.ok) return fail4(guard.error);
|
|
81451
81601
|
const secret = secretOf(o.endpoint);
|
|
81452
|
-
if (!secret) return
|
|
81602
|
+
if (!secret) return fail4("secret tak bisa dibuka \u2014 rotasi secret endpoint ini");
|
|
81453
81603
|
const headers = signedHeaders({
|
|
81454
81604
|
secret,
|
|
81455
81605
|
body: o.body,
|
|
@@ -81492,7 +81642,7 @@ async function sendOnce(o, deps = {}) {
|
|
|
81492
81642
|
return { ok: false, httpStatus: res.status, durationMs, error: `HTTP ${res.status}`, gone: false };
|
|
81493
81643
|
} catch (e) {
|
|
81494
81644
|
const msg = e.name === "AbortError" ? `timeout ${WEBHOOK_TIMEOUT_MS} ms` : e.message;
|
|
81495
|
-
return
|
|
81645
|
+
return fail4(msg);
|
|
81496
81646
|
} finally {
|
|
81497
81647
|
clearTimeout(timer10);
|
|
81498
81648
|
}
|