zombee11/scorecardapi
1
1<!doctype html>2<html lang="en">3 <head>4 <meta charset="utf-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />6 <title>Two Wheeler Scorecard Console</title>7 <style>8 :root {9 --bg: #f4f7fb;10 --panel: #ffffff;11 --line: #d9e2ec;12 --text: #172033;13 --muted: #637083;14 --accent: #116d6e;15 --accent-2: #204b8c;16 --danger: #b42318;17 --warn: #a15c07;18 --ok: #16794c;19 }20 21 * {22 box-sizing: border-box;23 }24 25 body {26 margin: 0;27 background: var(--bg);28 color: var(--text);29 font-family: Arial, Helvetica, sans-serif;30 }31 32 header {33 background: #0d1b2a;34 color: white;35 padding: 18px 24px;36 }37 38 header h1 {39 margin: 0;40 font-size: 22px;41 font-weight: 700;42 }43 44 header p {45 margin: 6px 0 0;46 color: #cbd5e1;47 font-size: 14px;48 }49 50 main {51 display: grid;52 grid-template-columns: minmax(360px, 1.05fr) minmax(360px, 0.95fr);53 gap: 16px;54 padding: 16px;55 }56 57 section {58 background: var(--panel);59 border: 1px solid var(--line);60 border-radius: 8px;61 min-width: 0;62 }63 64 .section-head {65 display: flex;66 justify-content: space-between;67 gap: 12px;68 align-items: center;69 padding: 14px 16px;70 border-bottom: 1px solid var(--line);71 }72 73 h2 {74 margin: 0;75 font-size: 17px;76 }77 78 .body {79 padding: 16px;80 }81 82 .row {83 display: grid;84 grid-template-columns: repeat(4, minmax(0, 1fr));85 gap: 10px;86 margin-bottom: 10px;87 }88 89 .api-row {90 display: grid;91 grid-template-columns: minmax(220px, 1.2fr) minmax(170px, 0.8fr) minmax(150px, 0.7fr);92 gap: 10px;93 margin-bottom: 10px;94 }95 96 label {97 display: block;98 color: var(--muted);99 font-size: 12px;100 margin-bottom: 4px;101 }102 103 input,104 select,105 textarea {106 width: 100%;107 border: 1px solid var(--line);108 border-radius: 6px;109 padding: 9px 10px;110 background: white;111 color: var(--text);112 font-size: 14px;113 }114 115 textarea {116 min-height: 320px;117 resize: vertical;118 font-family: Consolas, monospace;119 line-height: 1.35;120 }121 122 button {123 border: 0;124 border-radius: 6px;125 background: var(--accent);126 color: white;127 cursor: pointer;128 font-weight: 700;129 padding: 10px 13px;130 }131 132 button.secondary {133 background: var(--accent-2);134 }135 136 button.ghost {137 background: #e8eef6;138 color: var(--text);139 }140 141 button.danger {142 background: var(--danger);143 }144 145 .actions {146 display: flex;147 flex-wrap: wrap;148 gap: 8px;149 margin: 12px 0;150 }151 152 .decision {153 display: grid;154 grid-template-columns: repeat(4, minmax(0, 1fr));155 gap: 10px;156 margin-top: 12px;157 }158 159 .metric {160 border: 1px solid var(--line);161 border-radius: 8px;162 padding: 12px;163 background: #fbfdff;164 }165 166 .metric span {167 color: var(--muted);168 display: block;169 font-size: 12px;170 }171 172 .metric strong {173 display: block;174 font-size: 18px;175 margin-top: 5px;176 overflow-wrap: anywhere;177 }178 179 .STP {180 color: var(--ok);181 }182 183 .CONDITIONAL_STP {184 color: var(--warn);185 }186 187 .NON_STP,188 .REJECT {189 color: var(--danger);190 }191 192 pre {193 background: #101827;194 color: #dbeafe;195 border-radius: 8px;196 padding: 12px;197 overflow: auto;198 max-height: 360px;199 font-size: 13px;200 }201 202 table {203 width: 100%;204 border-collapse: collapse;205 font-size: 13px;206 }207 208 th,209 td {210 border-bottom: 1px solid var(--line);211 padding: 9px 7px;212 text-align: left;213 vertical-align: top;214 }215 216 th {217 color: var(--muted);218 font-weight: 700;219 background: #f8fafc;220 position: sticky;221 top: 0;222 }223 224 .table-wrap {225 border: 1px solid var(--line);226 border-radius: 8px;227 overflow: auto;228 max-height: 390px;229 }230 231 .status {232 min-height: 20px;233 color: var(--muted);234 font-size: 13px;235 }236 237 .status.ok {238 color: var(--ok);239 }240 241 .status.error {242 color: var(--danger);243 }244 245 .hint {246 color: var(--muted);247 font-size: 12px;248 line-height: 1.45;249 margin: 4px 0 12px;250 }251 252 .request-line {253 background: #eef6f6;254 border: 1px solid var(--line);255 border-radius: 6px;256 color: var(--accent);257 font-family: Consolas, monospace;258 font-size: 13px;259 margin: 8px 0 12px;260 overflow-wrap: anywhere;261 padding: 9px 10px;262 }263 264 .edit-banner {265 display: none;266 align-items: center;267 justify-content: space-between;268 gap: 10px;269 border: 1px solid #f2c94c;270 background: #fff8db;271 border-radius: 8px;272 padding: 10px 12px;273 margin-bottom: 12px;274 color: #6b4e00;275 font-size: 13px;276 }277 278 .rule-form {279 display: grid;280 grid-template-columns: repeat(3, minmax(0, 1fr));281 gap: 10px;282 margin-bottom: 12px;283 }284 285 .wide {286 grid-column: span 2;287 }288 289 @media (max-width: 980px) {290 main,291 .api-row,292 .row,293 .decision,294 .rule-form {295 grid-template-columns: 1fr;296 }297 298 .wide {299 grid-column: auto;300 }301 }302 </style>303 </head>304 <body>305 <header>306 <h1>Two Wheeler Loan STP Scorecard Console</h1>307 <p>Configure rules, simulate decisions, and generate persisted scorecards from one screen.</p>308 </header>309 310 <main>311 <section>312 <div class="section-head">313 <h2>Scorecard Tester</h2>314 <button class="ghost" onclick="loadSample()">Load Sample</button>315 </div>316 <div class="body">317 <div class="row">318 <div>319 <label>Tenant</label>320 <input id="tenantId" value="default" />321 </div>322 <div>323 <label>Product</label>324 <select id="productType" onchange="syncProductChange()">325 <option value="TWO_WHEELER_LOAN">Two Wheeler Loan</option>326 <option value="NEW_CAR_LOAN">New Car Loan</option>327 <option value="USED_CAR_LOAN">Used Car Loan</option>328 <option value="NEW_COMMERCIAL_VEHICLE_LOAN">New Commercial Vehicle</option>329 <option value="USED_COMMERCIAL_VEHICLE_LOAN">Used Commercial Vehicle</option>330 <option value="NEW_TRACTOR_LOAN">New Tractor Loan</option>331 <option value="USED_TRACTOR_LOAN">Used Tractor Loan</option>332 </select>333 </div>334 <div>335 <label>Lead ID</label>336 <input id="leadId" value="TW10001" />337 </div>338 <div>339 <label>Journey Trigger</label>340 <select id="dueDiligenceStatus">341 <option value="APPROVED">Due Diligence Approved</option>342 <option value="PENDING">Due Diligence Pending</option>343 <option value="REJECTED">Due Diligence Rejected</option>344 </select>345 </div>346 </div>347 348 <div class="api-row">349 <div>350 <label>API Base URL</label>351 <input id="apiBaseUrl" />352 </div>353 <div>354 <label>Endpoint</label>355 <select id="mode" onchange="refreshRequestPreview()">356 <option value="simulate-scorecard">POST /simulate-scorecard</option>357 <option value="generate-scorecard">POST /generate-scorecard</option>358 </select>359 </div>360 <div>361 <label>Method</label>362 <input id="method" value="POST" disabled />363 </div>364 </div>365 <div class="hint">366 Use simulate for testing only. Use generate after due diligence approval when you want the scorecard result saved against the lead.367 </div>368 <div class="request-line" id="requestPreview"></div>369 370 <label>Request JSON</label>371 <textarea id="payload"></textarea>372 373 <div class="actions">374 <button onclick="runScorecard()">Send API Request</button>375 <button class="secondary" onclick="triggerAfterDueDiligence()">Trigger After Due Diligence</button>376 <button class="secondary" onclick="getSavedResult()">Get Saved Result</button>377 <button class="ghost" onclick="openDocs()">Swagger Docs</button>378 <button class="ghost" onclick="copyCurl()">Copy cURL</button>379 </div>380 <div class="status" id="scoreStatus"></div>381 382 <div class="decision">383 <div class="metric">384 <span>Decision</span>385 <strong id="decision">-</strong>386 </div>387 <div class="metric">388 <span>Final Score</span>389 <strong id="finalScore">-</strong>390 </div>391 <div class="metric">392 <span>Journey Action</span>393 <strong id="journeyAction">-</strong>394 </div>395 <div class="metric">396 <span>Bureau Risk</span>397 <strong id="bureauRisk">-</strong>398 </div>399 </div>400 401 <pre id="scoreOutput">{}</pre>402 <label>Request Preview</label>403 <pre id="requestOutput">{}</pre>404 </div>405 </section>406 407 <section>408 <div class="section-head">409 <h2>Rule Configuration</h2>410 <button class="ghost" onclick="loadRules()">Refresh Rules</button>411 </div>412 <div class="body">413 <div class="rule-form">414 <input id="editingRuleId" type="hidden" />415 <div>416 <label>Rule Code</label>417 <input id="ruleCode" placeholder="CIBIL_SUPER_HIGH" />418 </div>419 <div class="wide">420 <label>Rule Name</label>421 <input id="ruleName" placeholder="CIBIL score above 800" />422 </div>423 <div>424 <label>Rule Type</label>425 <select id="ruleType">426 <option>SCORE</option>427 <option>HARD_REJECT</option>428 <option>NON_STP</option>429 <option>STP_ELIGIBILITY</option>430 </select>431 </div>432 <div>433 <label>Field Name</label>434 <input id="fieldName" placeholder="cibilScore" />435 </div>436 <div>437 <label>Operator</label>438 <select id="operator">439 <option>>=</option>440 <option>></option>441 <option><=</option>442 <option><</option>443 <option>=</option>444 <option>!=</option>445 <option>BETWEEN</option>446 <option>IN</option>447 <option>NOT_IN</option>448 </select>449 </div>450 <div>451 <label>Value</label>452 <input id="fieldValue" placeholder="750 or 700,749" />453 </div>454 <div>455 <label>Score Value</label>456 <input id="scoreValue" placeholder="10" />457 </div>458 <div>459 <label>Score Component</label>460 <input id="scoreComponent" placeholder="cibil" />461 </div>462 <div>463 <label>Decision Action</label>464 <select id="decisionAction">465 <option value="">None</option>466 <option>SCORE</option>467 <option>STP</option>468 <option>UW</option>469 <option>REJECT</option>470 <option>NON_STP</option>471 </select>472 </div>473 <div>474 <label>Reason Code</label>475 <input id="reasonCode" placeholder="HIGH_CIBIL" />476 </div>477 <div>478 <label>Priority</label>479 <input id="priority" value="100" />480 </div>481 </div>482 483 <div class="edit-banner" id="editBanner">484 <span id="editText">Editing rule</span>485 <button class="ghost" onclick="cancelEdit()">Cancel Edit</button>486 </div>487 488 <div class="actions">489 <button id="saveRuleButton" onclick="saveRule()">Create Rule</button>490 <button class="ghost" onclick="clearRuleForm()">Clear</button>491 <button class="secondary" onclick="exportRules()">Export Product Rules</button>492 </div>493 <div class="status" id="ruleStatus"></div>494 495 <div class="table-wrap">496 <table>497 <thead>498 <tr>499 <th>ID</th>500 <th>Code</th>501 <th>Type</th>502 <th>Condition</th>503 <th>Score/Action</th>504 <th>Status</th>505 <th>Actions</th>506 </tr>507 </thead>508 <tbody id="rulesTable"></tbody>509 </table>510 </div>511 </div>512 </section>513 </main>514 515 <script>516 const samplePayload = {517 tenantId: "default",518 productType: "TWO_WHEELER_LOAN",519 application: {520 leadId: "TW10001",521 customerName: "ABC",522 mobile: "9999999999",523 employmentType: "SALARIED",524 monthlyIncome: 45000,525 existingEMI: 8000,526 foir: 32,527 loanAmount: 95000,528 assetCost: 125000,529 ltv: 76,530 tenure: 36,531 dealerId: "DL101",532 state: "Karnataka",533 city: "Bangalore",534 negativeArea: false,535 fraudFlag: false,536 faceMatchScore: 92,537 panVerified: true,538 aadhaarVerified: true,539 bankingSurrogate: true,540 coApplicant: false541 },542 bureau: {543 cibilScore: 753,544 creditVintageMonths: 123,545 activeAccounts: 2,546 closedAccounts: 47,547 overdueAmount: 0,548 writtenOffAccounts: 0,549 settledAccounts: 0,550 active90PlusDpd: false,551 maxDPD: 0,552 dpd30Count: 0,553 dpd60Count: 0,554 dpd90Count: 0,555 creditEnquiries6M: 2,556 creditEnquiries12M: 4,557 activeUnsecuredLoans: 1,558 activeSecuredLoans: 1,559 jointAccounts: 0,560 creditUtilization: 2561 }562 };563 564 function setStatus(id, message) {565 const element = document.getElementById(id);566 element.textContent = message;567 element.className = "status";568 }569 570 function setStatusState(id, message, state) {571 const element = document.getElementById(id);572 element.textContent = message;573 element.className = `status ${state || ""}`;574 }575 576 function loadSample() {577 document.getElementById("payload").value = JSON.stringify(samplePayload, null, 2);578 document.getElementById("leadId").value = samplePayload.application.leadId;579 refreshRequestPreview();580 }581 582 function syncProductChange() {583 try {584 const payload = JSON.parse(document.getElementById("payload").value);585 payload.productType = document.getElementById("productType").value;586 document.getElementById("payload").value = JSON.stringify(payload, null, 2);587 } catch (_) {588 // Keep user-entered JSON untouched if it is mid-edit.589 }590 cancelEdit();591 loadRules();592 refreshRequestPreview();593 }594 595 function syncHeaderFields(payload) {596 payload.tenantId = document.getElementById("tenantId").value || "default";597 payload.productType = document.getElementById("productType").value || "TWO_WHEELER_LOAN";598 payload.application.leadId = document.getElementById("leadId").value || payload.application.leadId;599 return payload;600 }601 602 function getBaseUrl() {603 return (document.getElementById("apiBaseUrl").value || window.location.origin).replace(/\/$/, "");604 }605 606 function buildUrl(path) {607 return `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;608 }609 610 function getScorecardPath() {611 return `/${document.getElementById("mode").value}`;612 }613 614 function refreshRequestPreview() {615 const path = getScorecardPath();616 document.getElementById("requestPreview").textContent = `POST ${buildUrl(path)}`;617 }618 619 async function api(path, options = {}) {620 const response = await fetch(buildUrl(path), {621 headers: { "Content-Type": "application/json", ...(options.headers || {}) },622 ...options623 });624 const text = await response.text();625 let body = null;626 try {627 body = text ? JSON.parse(text) : null;628 } catch (_) {629 body = text;630 }631 if (!response.ok) {632 throw new Error(body?.detail || body || response.statusText);633 }634 return { body, status: response.status, statusText: response.statusText };635 }636 637 function readPayload() {638 const payload = syncHeaderFields(JSON.parse(document.getElementById("payload").value));639 document.getElementById("payload").value = JSON.stringify(payload, null, 2);640 return payload;641 }642 643 function renderRequest(path, payload) {644 const request = {645 method: "POST",646 url: buildUrl(path),647 headers: { "Content-Type": "application/json" },648 body: payload649 };650 document.getElementById("requestOutput").textContent = JSON.stringify(request, null, 2);651 }652 653 function renderScore(body) {654 document.getElementById("scoreOutput").textContent = JSON.stringify(body, null, 2);655 const decision = document.getElementById("decision");656 decision.textContent = body.decision || "-";657 decision.className = body.decision || "";658 document.getElementById("finalScore").textContent = body.finalScore ?? "-";659 document.getElementById("journeyAction").textContent = body.journeyAction || "-";660 document.getElementById("bureauRisk").textContent = body.bureauRisk || "-";661 }662 663 async function runScorecard() {664 try {665 setStatus("scoreStatus", "Sending API request...");666 const payload = readPayload();667 const path = getScorecardPath();668 renderRequest(path, payload);669 const result = await api(path, { method: "POST", body: JSON.stringify(payload) });670 renderScore(result.body);671 setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - scorecard completed.`, "ok");672 } catch (error) {673 setStatusState("scoreStatus", `Error: ${error.message}`, "error");674 }675 }676 677 async function triggerAfterDueDiligence() {678 const dueDiligenceStatus = document.getElementById("dueDiligenceStatus").value;679 if (dueDiligenceStatus !== "APPROVED") {680 setStatusState(681 "scoreStatus",682 `API not triggered because due diligence is ${dueDiligenceStatus}. Select Approved to call the scorecard.`,683 "error"684 );685 return;686 }687 document.getElementById("mode").value = "generate-scorecard";688 refreshRequestPreview();689 await runScorecard();690 }691 692 async function getSavedResult() {693 try {694 const leadId = document.getElementById("leadId").value;695 setStatus("scoreStatus", "Fetching saved result...");696 const result = await api(`/scorecard/${encodeURIComponent(leadId)}`);697 renderScore(result.body);698 setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - saved result loaded.`, "ok");699 } catch (error) {700 setStatusState("scoreStatus", `Error: ${error.message}`, "error");701 }702 }703 704 async function loadRules() {705 try {706 setStatus("ruleStatus", "Loading rules...");707 const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default");708 const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN");709 const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`);710 const rules = result.body;711 const rows = rules.map(rule => `712 <tr>713 <td>${rule.id}</td>714 <td>${rule.rule_code}<br><small>${rule.reason_code}</small></td>715 <td>${rule.rule_type}</td>716 <td>${rule.field_name} ${rule.operator} ${rule.field_value}</td>717 <td>${rule.score_value ?? ""} ${rule.decision_action ?? ""}</td>718 <td>${rule.is_active ? "Active" : "Disabled"}</td>719 <td>720 <button class="ghost" onclick='editRule(${JSON.stringify(rule)})'>Edit</button>721 <button class="${rule.is_active ? "danger" : "secondary"}" onclick="toggleRule(${rule.id}, ${!rule.is_active})">722 ${rule.is_active ? "Disable" : "Enable"}723 </button>724 </td>725 </tr>726 `).join("");727 document.getElementById("rulesTable").innerHTML = rows;728 setStatus("ruleStatus", `${rules.length} rules loaded.`);729 } catch (error) {730 setStatus("ruleStatus", `Error: ${error.message}`);731 }732 }733 734 function buildRulePayload() {735 const scoreValue = document.getElementById("scoreValue").value;736 return {737 tenant_id: document.getElementById("tenantId").value || "default",738 product_type: document.getElementById("productType").value || "TWO_WHEELER_LOAN",739 rule_code: document.getElementById("ruleCode").value,740 rule_name: document.getElementById("ruleName").value,741 rule_type: document.getElementById("ruleType").value,742 field_name: document.getElementById("fieldName").value,743 operator: document.getElementById("operator").value,744 field_value: document.getElementById("fieldValue").value,745 score_value: scoreValue ? Number(scoreValue) : null,746 score_component: document.getElementById("scoreComponent").value || null,747 decision_action: document.getElementById("decisionAction").value || null,748 reason_code: document.getElementById("reasonCode").value,749 priority: Number(document.getElementById("priority").value || 100),750 is_active: true751 };752 }753 754 async function saveRule() {755 const editingRuleId = document.getElementById("editingRuleId").value;756 if (editingRuleId) {757 await updateRule(editingRuleId);758 } else {759 await createRule();760 }761 }762 763 async function createRule() {764 try {765 setStatus("ruleStatus", "Creating rule...");766 const payload = buildRulePayload();767 await api("/rules", { method: "POST", body: JSON.stringify(payload) });768 clearRuleForm();769 await loadRules();770 setStatus("ruleStatus", "Rule created.");771 } catch (error) {772 setStatus("ruleStatus", `Error: ${error.message}`);773 }774 }775 776 async function updateRule(ruleId) {777 try {778 setStatus("ruleStatus", "Updating rule...");779 const payload = buildRulePayload();780 delete payload.tenant_id;781 delete payload.product_type;782 delete payload.rule_code;783 delete payload.rule_type;784 payload.actor = "admin-ui";785 await api(`/rules/${ruleId}`, { method: "PUT", body: JSON.stringify(payload) });786 clearRuleForm();787 await loadRules();788 setStatus("ruleStatus", "Rule updated.");789 } catch (error) {790 setStatus("ruleStatus", `Error: ${error.message}`);791 }792 }793 794 function editRule(rule) {795 document.getElementById("editingRuleId").value = rule.id;796 document.getElementById("ruleCode").value = rule.rule_code || "";797 document.getElementById("ruleName").value = rule.rule_name || "";798 document.getElementById("ruleType").value = rule.rule_type || "SCORE";799 document.getElementById("fieldName").value = rule.field_name || "";800 document.getElementById("operator").value = rule.operator || "=";801 document.getElementById("fieldValue").value = rule.field_value || "";802 document.getElementById("scoreValue").value = rule.score_value ?? "";803 document.getElementById("scoreComponent").value = rule.score_component || "";804 document.getElementById("decisionAction").value = rule.decision_action || "";805 document.getElementById("reasonCode").value = rule.reason_code || "";806 document.getElementById("priority").value = rule.priority || 100;807 document.getElementById("saveRuleButton").textContent = "Save Rule Changes";808 document.getElementById("editText").textContent = `Editing #${rule.id} - ${rule.rule_code}`;809 document.getElementById("editBanner").style.display = "flex";810 setStatus("ruleStatus", "Rule loaded into form. Change values and save.");811 window.scrollTo({ top: 0, behavior: "smooth" });812 }813 814 async function toggleRule(id, isActive) {815 try {816 await api(`/rules/${id}/status`, {817 method: "PATCH",818 body: JSON.stringify({ is_active: isActive, actor: "admin-ui" })819 });820 await loadRules();821 } catch (error) {822 setStatus("ruleStatus", `Error: ${error.message}`);823 }824 }825 826 function clearRuleForm() {827 [828 "ruleCode",829 "ruleName",830 "fieldName",831 "fieldValue",832 "scoreValue",833 "scoreComponent",834 "reasonCode"835 ].forEach(id => document.getElementById(id).value = "");836 document.getElementById("priority").value = "100";837 cancelEdit();838 }839 840 function cancelEdit() {841 document.getElementById("editingRuleId").value = "";842 document.getElementById("saveRuleButton").textContent = "Create Rule";843 document.getElementById("editBanner").style.display = "none";844 }845 846 async function exportRules() {847 try {848 const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default");849 const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN");850 const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`);851 const rules = result.body;852 const blob = new Blob([JSON.stringify(rules, null, 2)], { type: "application/json" });853 const url = URL.createObjectURL(blob);854 const link = document.createElement("a");855 link.href = url;856 link.download = `${product}_rules.json`;857 link.click();858 URL.revokeObjectURL(url);859 setStatus("ruleStatus", "Rules exported.");860 } catch (error) {861 setStatus("ruleStatus", `Error: ${error.message}`);862 }863 }864 865 function openDocs() {866 window.open(buildUrl("/docs"), "_blank");867 }868 869 async function copyCurl() {870 try {871 const payload = readPayload();872 const path = getScorecardPath();873 renderRequest(path, payload);874 const curl = [875 `curl -X POST "${buildUrl(path)}"`,876 `-H "Content-Type: application/json"`,877 `-d '${JSON.stringify(payload)}'`878 ].join(" ");879 await navigator.clipboard.writeText(curl);880 setStatusState("scoreStatus", "cURL copied.", "ok");881 } catch (error) {882 setStatusState("scoreStatus", `Error: ${error.message}`, "error");883 }884 }885 886 document.getElementById("apiBaseUrl").value = window.location.origin;887 document.getElementById("apiBaseUrl").addEventListener("input", refreshRequestPreview);888 loadSample();889 loadRules();890 </script>891 </body>892</html>893 