Implement Phase 0b base project against the frozen test contract
All seven services (gateway, ledger, fraud, sanctions, insights, notifier, audit), db/schema.sql plus optional_indexes.sql, scripts/seed.py and smoke.py, the gateway-served frontend, and docker-compose.yml. Plain-text stdlib logging and a weak manual X-Request-Id only -- no observability yet, by design, per test_no_observability.py. Implements all 13 named chaos-flag pathologies from docs/PATHOLOGIES.md behind Redis-backed runtime flags. make smoke and make test-contract pass; make test-baseline and make test-pathology pass except for a handful of load-sensitive tests documented in tests/README.md rather than papered over.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
const accountSelect = document.getElementById("account-select");
|
||||
const txForm = document.getElementById("tx-form");
|
||||
const resultJson = document.getElementById("result-json");
|
||||
const resultRtt = document.getElementById("result-rtt");
|
||||
const resultRequestId = document.getElementById("result-request-id");
|
||||
const burstButton = document.getElementById("burst-button");
|
||||
const burstN = document.getElementById("burst-n");
|
||||
const burstTableBody = document.querySelector("#burst-table tbody");
|
||||
const chaosFlagsDiv = document.getElementById("chaos-flags");
|
||||
const healthStrip = document.getElementById("health-strip");
|
||||
|
||||
async function loadAccounts() {
|
||||
const resp = await fetch("/api/accounts");
|
||||
const accounts = await resp.json();
|
||||
accountSelect.innerHTML = accounts
|
||||
.slice(0, 200)
|
||||
.map((a) => `<option value="${a.id}">${a.holder_name} (${a.currency})</option>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
txForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
account_id: accountSelect.value,
|
||||
kind: document.querySelector('input[name="kind"]:checked').value,
|
||||
amount_minor: Number(document.getElementById("amount-input").value),
|
||||
currency: "EUR",
|
||||
merchant_name: document.getElementById("merchant-input").value,
|
||||
};
|
||||
const start = performance.now();
|
||||
const resp = await fetch("/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const rtt = performance.now() - start;
|
||||
const body = await resp.json();
|
||||
resultRtt.textContent = rtt.toFixed(1);
|
||||
resultRequestId.textContent = body.request_id || "--";
|
||||
resultJson.textContent = JSON.stringify(body, null, 2);
|
||||
});
|
||||
|
||||
function percentile(sorted, p) {
|
||||
if (sorted.length === 0) return 0;
|
||||
const rank = Math.max(1, Math.min(sorted.length, Math.ceil(p * sorted.length)));
|
||||
return sorted[rank - 1];
|
||||
}
|
||||
|
||||
burstButton.addEventListener("click", async () => {
|
||||
const n = Number(burstN.value);
|
||||
const accounts = Array.from(accountSelect.options).map((o) => o.value);
|
||||
burstTableBody.innerHTML = `<tr><td colspan="8">running ${n} requests...</td></tr>`;
|
||||
|
||||
const latencies = [];
|
||||
let success = 0;
|
||||
let errors = 0;
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: n }, async () => {
|
||||
const accountId = accounts[Math.floor(Math.random() * accounts.length)];
|
||||
const start = performance.now();
|
||||
try {
|
||||
const resp = await fetch("/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
account_id: accountId,
|
||||
kind: "purchase",
|
||||
amount_minor: 500,
|
||||
currency: "EUR",
|
||||
merchant_name: "Load Burst",
|
||||
}),
|
||||
});
|
||||
latencies.push(performance.now() - start);
|
||||
if (resp.ok) success += 1;
|
||||
else errors += 1;
|
||||
} catch (e) {
|
||||
latencies.push(performance.now() - start);
|
||||
errors += 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
latencies.sort((a, b) => a - b);
|
||||
const mean = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
||||
const row = [
|
||||
latencies.length,
|
||||
success,
|
||||
errors,
|
||||
latencies[0].toFixed(1),
|
||||
mean.toFixed(1),
|
||||
percentile(latencies, 0.5).toFixed(1),
|
||||
percentile(latencies, 0.95).toFixed(1),
|
||||
latencies[latencies.length - 1].toFixed(1),
|
||||
];
|
||||
burstTableBody.innerHTML = `<tr>${row.map((v) => `<td>${v}</td>`).join("")}</tr>`;
|
||||
});
|
||||
|
||||
async function loadChaos() {
|
||||
const resp = await fetch("/api/admin/chaos");
|
||||
const { flags } = await resp.json();
|
||||
chaosFlagsDiv.innerHTML = Object.entries(flags)
|
||||
.map(
|
||||
([flag, enabled]) => `
|
||||
<label>
|
||||
<input type="checkbox" data-flag="${flag}" ${enabled ? "checked" : ""}>
|
||||
${flag}
|
||||
</label>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
chaosFlagsDiv.querySelectorAll("input[type=checkbox]").forEach((el) => {
|
||||
el.addEventListener("change", async () => {
|
||||
await fetch("/api/admin/chaos", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ flag: el.dataset.flag, enabled: el.checked }),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("#chaos-profiles button").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
await fetch("/api/admin/chaos/profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: btn.dataset.profile }),
|
||||
});
|
||||
await loadChaos();
|
||||
});
|
||||
});
|
||||
|
||||
async function pollHealth() {
|
||||
try {
|
||||
const resp = await fetch("/api/health/all");
|
||||
const { services } = await resp.json();
|
||||
healthStrip.innerHTML = Object.entries(services)
|
||||
.map(([name, status]) => `<span class="dot ${status === "ok" ? "green" : "red"}">${name}</span>`)
|
||||
.join("");
|
||||
} catch (e) {
|
||||
// ignore transient polling failures
|
||||
}
|
||||
}
|
||||
|
||||
loadAccounts();
|
||||
loadChaos();
|
||||
pollHealth();
|
||||
setInterval(pollHealth, 5000);
|
||||
Reference in New Issue
Block a user