All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s
1320 lines
49 KiB
HTML
1320 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Tradhox Document Verification Studio & Test Portal</title>
|
||
<!-- Modern Typography -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
:root {
|
||
--bg-base: #0B0F19;
|
||
--bg-surface: #111827;
|
||
--bg-surface-elevated: #162032;
|
||
--bg-card: rgba(22, 32, 50, 0.7);
|
||
--bg-input: #0e1626;
|
||
--border-subtle: rgba(255, 255, 255, 0.08);
|
||
--border-accent: rgba(99, 102, 241, 0.3);
|
||
--text-main: #F3F4F6;
|
||
--text-muted: #9CA3AF;
|
||
--text-dim: #6B7280;
|
||
--primary: #6366F1;
|
||
--primary-glow: rgba(99, 102, 241, 0.35);
|
||
--primary-hover: #4F46E5;
|
||
--success: #10B981;
|
||
--success-glow: rgba(16, 185, 129, 0.25);
|
||
--danger: #EF4444;
|
||
--danger-glow: rgba(239, 68, 68, 0.25);
|
||
--warning: #F59E0B;
|
||
--info: #38BDF8;
|
||
--radius-sm: 8px;
|
||
--radius-md: 14px;
|
||
--radius-lg: 20px;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background-color: var(--bg-base);
|
||
color: var(--text-main);
|
||
min-height: 100vh;
|
||
line-height: 1.5;
|
||
background-image:
|
||
radial-gradient(circle at 15% 15%, rgba(99, 102, 241, 0.09) 0%, transparent 45%),
|
||
radial-gradient(circle at 85% 80%, rgba(139, 92, 246, 0.07) 0%, transparent 45%);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
/* Header Bar */
|
||
header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
backdrop-filter: blur(16px);
|
||
background: rgba(11, 15, 25, 0.82);
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
padding: 0.85rem 1.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.brand-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.brand-logo {
|
||
width: 38px;
|
||
height: 38px;
|
||
background: linear-gradient(135deg, #6366F1, #8B5CF6);
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 0 18px var(--primary-glow);
|
||
}
|
||
|
||
.brand-logo svg {
|
||
width: 22px;
|
||
height: 22px;
|
||
fill: #FFFFFF;
|
||
}
|
||
|
||
.brand-title {
|
||
font-size: 1.08rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.02em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
font-size: 0.76rem;
|
||
color: var(--text-dim);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.badge-live {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border-radius: 9999px;
|
||
background: rgba(16, 185, 129, 0.12);
|
||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||
color: var(--success);
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.pulse-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background-color: var(--success);
|
||
box-shadow: 0 0 8px var(--success);
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.4; transform: scale(0.85); }
|
||
}
|
||
|
||
.nav-btn {
|
||
background: var(--bg-surface-elevated);
|
||
color: var(--text-main);
|
||
border: 1px solid var(--border-subtle);
|
||
padding: 6px 14px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.nav-btn:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 12px var(--primary-glow);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* Main Container */
|
||
.main-wrapper {
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 1.75rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.75rem;
|
||
}
|
||
|
||
/* Top Service Selector Banner */
|
||
.service-nav {
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
padding-bottom: 6px;
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
.service-nav::-webkit-scrollbar {
|
||
height: 4px;
|
||
}
|
||
.service-nav::-webkit-scrollbar-thumb {
|
||
background: var(--border-subtle);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.service-tab {
|
||
background: var(--bg-surface);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
padding: 10px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
white-space: nowrap;
|
||
user-select: none;
|
||
}
|
||
|
||
.service-tab:hover {
|
||
border-color: rgba(99, 102, 241, 0.4);
|
||
background: var(--bg-surface-elevated);
|
||
}
|
||
|
||
.service-tab.active {
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.18), rgba(139, 92, 246, 0.18));
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 18px var(--primary-glow);
|
||
}
|
||
|
||
.service-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 8px;
|
||
background: var(--bg-input);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.service-tab.active .service-icon {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.service-name {
|
||
font-size: 0.86rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.service-tag {
|
||
font-size: 0.68rem;
|
||
font-weight: 600;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--text-muted);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
/* Two-Column Studio Layout */
|
||
.studio-grid {
|
||
display: grid;
|
||
grid-template-columns: 1.05fr 1.35fr;
|
||
gap: 1.5rem;
|
||
align-items: start;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.studio-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Card Panels */
|
||
.glass-card {
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 1.5rem;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 1.25rem;
|
||
padding-bottom: 0.85rem;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 1.02rem;
|
||
font-weight: 800;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* Form Controls */
|
||
.form-group {
|
||
margin-bottom: 1.15rem;
|
||
}
|
||
|
||
.form-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 0.72rem;
|
||
color: var(--text-dim);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
background: var(--bg-input);
|
||
border: 1.5px solid var(--border-subtle);
|
||
border-radius: var(--radius-sm);
|
||
padding: 11px 14px;
|
||
font-size: 0.92rem;
|
||
color: #FFFFFF;
|
||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||
outline: none;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.form-input:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px var(--primary-glow);
|
||
}
|
||
|
||
.form-input.code-font {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Quick-fill Chips */
|
||
.quick-fill-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.chip-btn {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||
color: var(--text-muted);
|
||
border-radius: 6px;
|
||
padding: 3px 8px;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.chip-btn:hover {
|
||
border-color: var(--primary);
|
||
color: #FFFFFF;
|
||
background: rgba(99, 102, 241, 0.12);
|
||
}
|
||
|
||
/* Config Grid */
|
||
.config-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-top: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
.select-input {
|
||
width: 100%;
|
||
background: var(--bg-input);
|
||
border: 1.5px solid var(--border-subtle);
|
||
border-radius: var(--radius-sm);
|
||
padding: 9px 12px;
|
||
font-size: 0.84rem;
|
||
color: var(--text-main);
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.select-input:focus {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* Verify Button */
|
||
.btn-verify {
|
||
width: 100%;
|
||
margin-top: 1.25rem;
|
||
background: linear-gradient(135deg, #6366F1, #8B5CF6);
|
||
color: #FFFFFF;
|
||
border: none;
|
||
border-radius: var(--radius-md);
|
||
padding: 13px 20px;
|
||
font-size: 0.96rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
box-shadow: 0 6px 20px var(--primary-glow);
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.btn-verify:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 26px rgba(99, 102, 241, 0.5);
|
||
background: linear-gradient(135deg, #4F46E5, #7C3AED);
|
||
}
|
||
|
||
.btn-verify:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn-verify:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
/* Spinner */
|
||
.spinner {
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2.5px solid rgba(255, 255, 255, 0.3);
|
||
border-top-color: #FFFFFF;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Results Panel */
|
||
.status-hero {
|
||
border-radius: var(--radius-md);
|
||
padding: 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 1.25rem;
|
||
border: 1px solid var(--border-subtle);
|
||
background: var(--bg-surface);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.status-hero.success {
|
||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(6, 95, 70, 0.1));
|
||
border-color: rgba(16, 185, 129, 0.4);
|
||
box-shadow: 0 0 24px var(--success-glow);
|
||
}
|
||
|
||
.status-hero.failed {
|
||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12), rgba(153, 27, 27, 0.1));
|
||
border-color: rgba(239, 68, 68, 0.4);
|
||
box-shadow: 0 0 24px var(--danger-glow);
|
||
}
|
||
|
||
.status-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.status-hero.success .status-indicator {
|
||
background: var(--success);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.status-hero.failed .status-indicator {
|
||
background: var(--danger);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.status-headline {
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.status-meta {
|
||
font-size: 0.76rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.latency-badge {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.8rem;
|
||
font-weight: 700;
|
||
padding: 4px 10px;
|
||
border-radius: 9999px;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
/* Entity Details Grid */
|
||
.entity-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.entity-card {
|
||
background: var(--bg-surface);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-sm);
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
.entity-label {
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
color: var(--text-dim);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.entity-value {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
margin-top: 2px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* Waterfall Timeline */
|
||
.waterfall-track {
|
||
background: var(--bg-surface);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
padding: 12px 16px;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.waterfall-title {
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.attempt-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: 6px;
|
||
font-size: 0.82rem;
|
||
border-left: 3px solid var(--primary);
|
||
}
|
||
|
||
.attempt-item.success {
|
||
border-left-color: var(--success);
|
||
}
|
||
|
||
.attempt-item.failed {
|
||
border-left-color: var(--danger);
|
||
}
|
||
|
||
/* Raw JSON Viewer */
|
||
.json-viewer-container {
|
||
position: relative;
|
||
background: #080B12;
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.json-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #0d121f;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
.tab-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tab-btn.active {
|
||
color: #FFFFFF;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.copy-btn {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid var(--border-subtle);
|
||
color: var(--text-muted);
|
||
border-radius: 4px;
|
||
padding: 3px 8px;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.copy-btn:hover {
|
||
color: #FFFFFF;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
pre.code-block {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 0.78rem;
|
||
color: #E2E8F0;
|
||
padding: 14px;
|
||
overflow-x: auto;
|
||
max-height: 280px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Transactions Audit Table */
|
||
.table-wrapper {
|
||
overflow-x: auto;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-subtle);
|
||
background: var(--bg-surface);
|
||
}
|
||
|
||
table.audit-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.82rem;
|
||
text-align: left;
|
||
}
|
||
|
||
table.audit-table th {
|
||
background: #0e1524;
|
||
color: var(--text-dim);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
table.audit-table td {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||
color: var(--text-main);
|
||
}
|
||
|
||
table.audit-table tr:hover {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.status-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 0.72rem;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 9999px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-pill.success {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: var(--success);
|
||
}
|
||
|
||
.status-pill.failed {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
color: var(--danger);
|
||
}
|
||
|
||
.mono {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Sticky Header -->
|
||
<header>
|
||
<div class="brand-container">
|
||
<div class="brand-logo">
|
||
<svg viewBox="0 0 24 24">
|
||
<path d="M12 2L3 7v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-9-5zm-2 16l-4-4 1.41-1.41L10 15.17l6.59-6.59L18 10l-8 8z"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div class="brand-title">
|
||
Tradhox Document Verification Studio
|
||
<span class="badge-live"><span class="pulse-dot"></span> LIVE ENGINE</span>
|
||
</div>
|
||
<div class="brand-subtitle">Automated Multi-Vendor Waterfall Engine • Codeplus Live Onboarded</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-actions">
|
||
<div style="font-size: 0.78rem; color: var(--text-dim); text-align: right; margin-right: 8px;">
|
||
Host: <span style="color: var(--text-main); font-weight: 600;">beta-documentverification.tradhox.com</span><br>
|
||
Active Vendors: <span style="color: var(--success); font-weight: 700;">{{ active_vendor_count|default:"1" }} Active</span>
|
||
</div>
|
||
<a href="/admin/" target="_blank" class="nav-btn">
|
||
<span>Django Admin</span>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>
|
||
</a>
|
||
<button onclick="fetchTransactions()" class="nav-btn">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||
<span>Refresh Log</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-wrapper">
|
||
|
||
<!-- Verification Function Selector Tabs -->
|
||
<div class="service-nav" id="serviceTabs">
|
||
<div class="service-tab active" data-doctype="pan_v5" onclick="selectService('pan_v5')">
|
||
<div class="service-icon">🪪</div>
|
||
<div>
|
||
<div class="service-name">PAN Verification (v5 Recommended)</div>
|
||
<span class="service-tag">POST /v5/pan-verify</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="pan_detailed" onclick="selectService('pan_detailed')">
|
||
<div class="service-icon">📄</div>
|
||
<div>
|
||
<div class="service-name">PAN Detailed API</div>
|
||
<span class="service-tag">POST /v4/pan-detailed</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="pan_advanced" onclick="selectService('pan_advanced')">
|
||
<div class="service-icon">🔍</div>
|
||
<div>
|
||
<div class="service-name">PAN Advanced API</div>
|
||
<span class="service-tag">POST /v4/pan-advanced</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="bank_verify_pd" onclick="selectService('bank_verify_pd')">
|
||
<div class="service-icon">🏦</div>
|
||
<div>
|
||
<div class="service-name">Bank Verify (Penny Drop - v5)</div>
|
||
<span class="service-tag">POST /v5/bank-verify-pd</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="gst_verify" onclick="selectService('gst_verify')">
|
||
<div class="service-icon">🏢</div>
|
||
<div>
|
||
<div class="service-name">GST Verification (v5)</div>
|
||
<span class="service-tag">POST /v5/gst-verify</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="fetch_gst" onclick="selectService('fetch_gst')">
|
||
<div class="service-icon">📑</div>
|
||
<div>
|
||
<div class="service-name">Fetch GST By PAN</div>
|
||
<span class="service-tag">POST /v4/fetch-gst</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-tab" data-doctype="msme_by_pan" onclick="selectService('msme_by_pan')">
|
||
<div class="service-icon">📜</div>
|
||
<div>
|
||
<div class="service-name">Fetch MSME By PAN</div>
|
||
<span class="service-tag">POST /v4/msme-pan</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Studio 2-Column Grid -->
|
||
<div class="studio-grid">
|
||
|
||
<!-- LEFT: Test Input Playground -->
|
||
<div class="glass-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>⚡ Test Execution Console</span>
|
||
</div>
|
||
<button type="button" class="chip-btn" onclick="clearInputs()">Reset Fields</button>
|
||
</div>
|
||
|
||
<form id="verifyForm" onsubmit="handleVerification(event)">
|
||
<!-- Dynamic Input Fields Rendered via JS -->
|
||
<div id="dynamicFormFields"></div>
|
||
|
||
<!-- Preset Quick Fill Chips -->
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
<span>Pre-filled Quick Test Data</span>
|
||
<span class="form-hint">One-click populate</span>
|
||
</label>
|
||
<div class="quick-fill-row" id="quickFillChips"></div>
|
||
</div>
|
||
|
||
<!-- Engine Configuration -->
|
||
<div class="config-row">
|
||
<div>
|
||
<label class="form-label">
|
||
<span>Engine Routing</span>
|
||
</label>
|
||
<select id="vendorPreference" class="select-input">
|
||
<option value="">Auto Waterfall (Codeplus First)</option>
|
||
<option value="codeplus">Force Codeplus API</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="form-label">
|
||
<span>Audit & Consent</span>
|
||
</label>
|
||
<div style="display: flex; align-items: center; gap: 8px; height: 38px;">
|
||
<input type="checkbox" id="consentCheck" checked style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||
<label for="consentCheck" style="font-size: 0.8rem; color: var(--text-muted); cursor: pointer;">User consent logged</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" id="submitBtn" class="btn-verify">
|
||
<span id="btnIcon">🚀</span>
|
||
<span id="btnText">Execute Live Verification</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- RIGHT: Inspection & Audit Studio -->
|
||
<div class="glass-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>📊 Live Response & Audit Inspection</span>
|
||
</div>
|
||
<div id="heroLatency" class="latency-badge" style="display: none;">0 ms</div>
|
||
</div>
|
||
|
||
<!-- Hero Status Banner -->
|
||
<div id="statusHero" class="status-hero">
|
||
<div class="status-info">
|
||
<div id="statusIcon" class="status-indicator">⏱️</div>
|
||
<div>
|
||
<div id="statusHeadline" class="status-headline">Engine Ready</div>
|
||
<div id="statusMeta" class="status-meta">Select a service and click Execute to test live waterfall verification.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Extracted Key Attributes Grid -->
|
||
<div id="entityGrid" class="entity-grid" style="display: none;"></div>
|
||
|
||
<!-- Waterfall Execution Step Trace -->
|
||
<div id="waterfallTrace" class="waterfall-track" style="display: none;">
|
||
<div class="waterfall-title">
|
||
<span>Waterfall Failover Trace</span>
|
||
<span id="attemptCountBadge" style="font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;">0 Attempts</span>
|
||
</div>
|
||
<div id="attemptItemsList"></div>
|
||
</div>
|
||
|
||
<!-- Raw JSON & Audit Logs -->
|
||
<div class="json-viewer-container">
|
||
<div class="json-tabs">
|
||
<div style="display: flex; gap: 6px;">
|
||
<button type="button" class="tab-btn active" id="tabClean" onclick="switchJsonTab('clean')">Clean Data</button>
|
||
<button type="button" class="tab-btn" id="tabRaw" onclick="switchJsonTab('raw')">Full Engine Payload</button>
|
||
</div>
|
||
<button type="button" class="copy-btn" onclick="copyCurrentJson()">Copy JSON</button>
|
||
</div>
|
||
<pre class="code-block" id="jsonDisplay">// Awaiting verification trigger...</pre>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- BOTTOM: Recent Verifications Audit Table -->
|
||
<div class="glass-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>📜 Recent PostgreSQL Transactions (Audit Log)</span>
|
||
</div>
|
||
<div style="font-size: 0.76rem; color: var(--text-dim);">Real-time stream from beta-documentverifcation DB</div>
|
||
</div>
|
||
|
||
<div class="table-wrapper">
|
||
<table class="audit-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Created At</th>
|
||
<th>Reference ID</th>
|
||
<th>Document</th>
|
||
<th>Vendor</th>
|
||
<th>Status</th>
|
||
<th>Latency</th>
|
||
<th>Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="transactionsTbody">
|
||
{% for tx in recent_transactions %}
|
||
<tr onclick="loadTransaction('{{ tx.reference_id }}')">
|
||
<td class="mono" style="color: var(--text-muted);">{{ tx.created_at|date:"Y-m-d H:i:s" }}</td>
|
||
<td class="mono" style="color: var(--info);">{{ tx.reference_id|stringformat:".8s" }}...</td>
|
||
<td><strong>{{ tx.document_type|upper }}</strong></td>
|
||
<td><span style="color: #A78BFA; font-weight: 600;">{{ tx.successful_vendor|default:"exhausted" }}</span></td>
|
||
<td>
|
||
<span class="status-pill {% if tx.status == 'SUCCESS' or tx.status == 'VERIFIED' %}success{% else %}failed{% endif %}">
|
||
{{ tx.status }}
|
||
</span>
|
||
</td>
|
||
<td class="mono">{{ tx.total_latency_ms|floatformat:1 }} ms</td>
|
||
<td><button class="chip-btn" onclick="event.stopPropagation(); loadTransaction('{{ tx.reference_id }}')">Inspect</button></td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; color: var(--text-dim); padding: 24px;">No transactions logged yet. Run your first verification above!</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Interactive Script -->
|
||
<script>
|
||
let currentService = 'pan_v5';
|
||
let latestCleanData = {};
|
||
let latestFullResponse = {};
|
||
let currentJsonTab = 'clean';
|
||
|
||
const serviceConfigs = {
|
||
pan_v5: {
|
||
title: "PAN Verification (v5 Recommended)",
|
||
docType: "PAN",
|
||
fields: [
|
||
{ id: "pan", label: "PAN Number", placeholder: "e.g. ABCDE1234F", type: "text", required: true, isCode: true, maxLength: 10 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Valid Sample PAN", data: { pan: "ABCDE1234F" } },
|
||
{ label: "Alternate PAN", data: { pan: "AAAPA1234K" } }
|
||
]
|
||
},
|
||
pan_detailed: {
|
||
title: "Detailed PAN API",
|
||
docType: "PAN_DETAILED",
|
||
fields: [
|
||
{ id: "pan_number", label: "PAN Number", placeholder: "e.g. ABCDE1234F", type: "text", required: true, isCode: true, maxLength: 10 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Sample Valid PAN", data: { pan_number: "ABCDE1234F" } }
|
||
]
|
||
},
|
||
pan_advanced: {
|
||
title: "PAN Advanced API",
|
||
docType: "PAN_ADVANCED",
|
||
fields: [
|
||
{ id: "pan", label: "PAN Number", placeholder: "e.g. ABCDE1234F", type: "text", required: true, isCode: true, maxLength: 10 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Sample Valid PAN", data: { pan: "ABCDE1234F" } }
|
||
]
|
||
},
|
||
bank_verify_pd: {
|
||
title: "Bank Account Verification – PD (Penny Drop - v5)",
|
||
docType: "BANK_ACCOUNT",
|
||
fields: [
|
||
{ id: "account_number", label: "Bank Account Number", placeholder: "e.g. 123456787270", type: "text", required: true, isCode: true },
|
||
{ id: "ifsc", label: "IFSC Code", placeholder: "e.g. UTIB0000373", type: "text", required: true, isCode: true, maxLength: 11 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Penny Drop v5 Sample (UTIB0000373)", data: { account_number: "123456787270", ifsc: "UTIB0000373" } },
|
||
{ label: "HDFC Bank Sample", data: { account_number: "50100234567890", ifsc: "HDFC0000001" } }
|
||
]
|
||
},
|
||
bank_account: {
|
||
title: "Bank Account Verification (Full Details / Auto Waterfall)",
|
||
docType: "BANK_ACCOUNT",
|
||
fields: [
|
||
{ id: "account_number", label: "Bank Account Number", placeholder: "e.g. 123456787270", type: "text", required: true, isCode: true },
|
||
{ id: "ifsc", label: "IFSC Code", placeholder: "e.g. UTIB0000373", type: "text", required: true, isCode: true, maxLength: 11 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Penny Drop v5 Sample", data: { account_number: "123456787270", ifsc: "UTIB0000373" } },
|
||
{ label: "HDFC Bank Sample", data: { account_number: "50100234567890", ifsc: "HDFC0000001" } }
|
||
]
|
||
},
|
||
gst_verify: {
|
||
title: "GST Verification (v5)",
|
||
docType: "GSTIN",
|
||
fields: [
|
||
{ id: "gst_number", label: "GST Identification Number (GSTIN)", placeholder: "e.g. 27ABCDE1234F1Z5", type: "text", required: true, isCode: true, maxLength: 15 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Valid GSTIN Sample", data: { gst_number: "27ABCDE1234F1Z5" } },
|
||
{ label: "TN Artisan GSTIN", data: { gst_number: "33AABCT1234F1Z9" } }
|
||
]
|
||
},
|
||
fetch_gst: {
|
||
title: "Fetch GST Records by PAN",
|
||
docType: "GST_BY_PAN",
|
||
fields: [
|
||
{ id: "pan", label: "PAN Number", placeholder: "e.g. ABCDE1234F", type: "text", required: true, isCode: true, maxLength: 10 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Corporate PAN Sample", data: { pan: "ABCDE1234F" } }
|
||
]
|
||
},
|
||
msme_by_pan: {
|
||
title: "Fetch MSME / Udyam by PAN",
|
||
docType: "MSME_BY_PAN",
|
||
fields: [
|
||
{ id: "pan", label: "PAN Number", placeholder: "e.g. ABCDE1234F", type: "text", required: true, isCode: true, maxLength: 10 }
|
||
],
|
||
quickFill: [
|
||
{ label: "Artisan Enterprise PAN", data: { pan: "ABCDE1234F" } }
|
||
]
|
||
}
|
||
};
|
||
|
||
function selectService(serviceKey) {
|
||
currentService = serviceKey;
|
||
document.querySelectorAll('.service-tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.doctype === serviceKey);
|
||
});
|
||
renderFormFields();
|
||
}
|
||
|
||
function renderFormFields() {
|
||
const config = serviceConfigs[currentService];
|
||
const container = document.getElementById('dynamicFormFields');
|
||
const chipsContainer = document.getElementById('quickFillChips');
|
||
|
||
container.innerHTML = config.fields.map(field => `
|
||
<div class="form-group">
|
||
<label class="form-label" for="field_${field.id}">
|
||
<span>${field.label}</span>
|
||
<span class="form-hint">${field.isCode ? 'UPPERCASE FORMAT' : ''}</span>
|
||
</label>
|
||
<input
|
||
type="${field.type || 'text'}"
|
||
id="field_${field.id}"
|
||
name="${field.id}"
|
||
placeholder="${field.placeholder || ''}"
|
||
class="form-input ${field.isCode ? 'code-font' : ''}"
|
||
${field.required ? 'required' : ''}
|
||
${field.maxLength ? `maxlength="${field.maxLength}"` : ''}
|
||
oninput="${field.isCode ? 'this.value = this.value.toUpperCase()' : ''}"
|
||
/>
|
||
</div>
|
||
`).join('');
|
||
|
||
chipsContainer.innerHTML = (config.quickFill || []).map((chip, idx) => `
|
||
<button type="button" class="chip-btn" onclick="applyQuickFill(${idx})">${chip.label}</button>
|
||
`).join('');
|
||
}
|
||
|
||
function applyQuickFill(idx) {
|
||
const config = serviceConfigs[currentService];
|
||
const sample = config.quickFill[idx].data;
|
||
Object.keys(sample).forEach(key => {
|
||
const input = document.getElementById(`field_${key}`);
|
||
if (input) input.value = sample[key];
|
||
});
|
||
}
|
||
|
||
function clearInputs() {
|
||
document.querySelectorAll('#dynamicFormFields input').forEach(i => i.value = '');
|
||
}
|
||
|
||
async function handleVerification(e) {
|
||
e.preventDefault();
|
||
const config = serviceConfigs[currentService];
|
||
const submitBtn = document.getElementById('submitBtn');
|
||
const btnText = document.getElementById('btnText');
|
||
const btnIcon = document.getElementById('btnIcon');
|
||
|
||
// Collect form data
|
||
const dataObj = {};
|
||
config.fields.forEach(f => {
|
||
const el = document.getElementById(`field_${f.id}`);
|
||
if (el) dataObj[f.id] = el.value.trim();
|
||
});
|
||
|
||
const vendorPref = document.getElementById('vendorPreference').value;
|
||
const payload = {
|
||
document_type: config.docType,
|
||
data: dataObj,
|
||
preferred_vendor: vendorPref || null
|
||
};
|
||
|
||
// Loading UI state
|
||
submitBtn.disabled = true;
|
||
btnIcon.innerHTML = '<div class="spinner"></div>';
|
||
btnText.innerText = 'Verifying via Live Waterfall...';
|
||
|
||
const startTime = performance.now();
|
||
try {
|
||
const response = await fetch('/api/verify/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
const result = await response.json();
|
||
const clientLatency = Math.round(performance.now() - startTime);
|
||
|
||
renderResults(result, clientLatency);
|
||
fetchTransactions();
|
||
} catch (err) {
|
||
renderError(err);
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
btnIcon.innerHTML = '🚀';
|
||
btnText.innerText = 'Execute Live Verification';
|
||
}
|
||
}
|
||
|
||
function renderResults(result, clientLatency) {
|
||
latestFullResponse = result;
|
||
latestCleanData = result.data || {};
|
||
|
||
const hero = document.getElementById('statusHero');
|
||
const heroIcon = document.getElementById('statusIcon');
|
||
const heroHeadline = document.getElementById('statusHeadline');
|
||
const heroMeta = document.getElementById('statusMeta');
|
||
const heroLatency = document.getElementById('heroLatency');
|
||
const entityGrid = document.getElementById('entityGrid');
|
||
const waterfallTrace = document.getElementById('waterfallTrace');
|
||
const attemptItemsList = document.getElementById('attemptItemsList');
|
||
const attemptCountBadge = document.getElementById('attemptCountBadge');
|
||
|
||
heroLatency.style.display = 'block';
|
||
heroLatency.innerText = `${result.total_latency_ms || clientLatency} ms`;
|
||
|
||
if (result.is_success) {
|
||
hero.className = 'status-hero success';
|
||
heroIcon.innerText = '✅';
|
||
heroHeadline.innerText = `Verified Successfully via ${result.successful_vendor || 'Engine'}`;
|
||
heroMeta.innerText = `Transaction Ref: ${result.reference_id || 'N/A'} • Status: ${result.status}`;
|
||
} else {
|
||
hero.className = 'status-hero failed';
|
||
heroIcon.innerText = '❌';
|
||
heroHeadline.innerText = `Verification Failed: ${result.status || 'UNVERIFIED'}`;
|
||
heroMeta.innerText = result.error_message || 'The vendor rejected the details or credentials.';
|
||
}
|
||
|
||
// Render extracted entity grid
|
||
const keyAttributes = extractKeyAttributes(latestCleanData);
|
||
if (Object.keys(keyAttributes).length > 0) {
|
||
entityGrid.style.display = 'grid';
|
||
entityGrid.innerHTML = Object.entries(keyAttributes).map(([label, val]) => `
|
||
<div class="entity-card">
|
||
<div class="entity-label">${label}</div>
|
||
<div class="entity-value">${val || '—'}</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
entityGrid.style.display = 'none';
|
||
}
|
||
|
||
// Render waterfall attempts
|
||
if (result.attempts && result.attempts.length > 0) {
|
||
waterfallTrace.style.display = 'block';
|
||
attemptCountBadge.innerText = `${result.attempts.length} Attempt(s)`;
|
||
attemptItemsList.innerHTML = result.attempts.map((att, i) => `
|
||
<div class="attempt-item ${att.is_success ? 'success' : 'failed'}">
|
||
<div>
|
||
<strong>Attempt #${att.attempt_number || (i+1)}:</strong>
|
||
<span style="color: #A78BFA; font-weight: 700; margin-left: 6px;">${att.vendor_name || 'Vendor'}</span>
|
||
<span style="color: var(--text-dim); margin-left: 8px;">(${att.status})</span>
|
||
</div>
|
||
<div style="font-family: 'JetBrains Mono', monospace; font-size: 0.76rem;">
|
||
<span>${att.latency_ms ? att.latency_ms.toFixed(1) + 'ms' : ''}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
waterfallTrace.style.display = 'none';
|
||
}
|
||
|
||
renderJsonDisplay();
|
||
}
|
||
|
||
function extractKeyAttributes(data) {
|
||
const attrs = {};
|
||
if (data.name) attrs['Full Name'] = data.name;
|
||
if (data.legal_name || data.legalName) attrs['Legal Business Name'] = data.legal_name || data.legalName;
|
||
if (data.trade_name || data.tradeName) attrs['Trade Name'] = data.trade_name || data.tradeName;
|
||
if (data.account_holder_name) attrs['Bank Account Holder'] = data.account_holder_name;
|
||
if (data.bank_name) attrs['Bank Name'] = data.bank_name;
|
||
if (data.ifsc) attrs['IFSC Code'] = data.ifsc;
|
||
if (data.category) attrs['Category'] = data.category;
|
||
if (data.status) attrs['Registration Status'] = data.status;
|
||
if (data.aadhaar_seeding_status) attrs['Aadhaar Seeding'] = data.aadhaar_seeding_status;
|
||
if (data.masked_aadhaar_number) attrs['Masked Aadhaar'] = data.masked_aadhaar_number;
|
||
if (data.date_of_birth || data.dob) attrs['Date of Birth'] = data.date_of_birth || data.dob;
|
||
return attrs;
|
||
}
|
||
|
||
function renderError(err) {
|
||
const hero = document.getElementById('statusHero');
|
||
hero.className = 'status-hero failed';
|
||
document.getElementById('statusIcon').innerText = '⚠️';
|
||
document.getElementById('statusHeadline').innerText = 'Network / Request Error';
|
||
document.getElementById('statusMeta').innerText = err.message || 'Unable to connect to verification backend.';
|
||
document.getElementById('jsonDisplay').innerText = JSON.stringify({ error: err.toString() }, null, 2);
|
||
}
|
||
|
||
function switchJsonTab(tab) {
|
||
currentJsonTab = tab;
|
||
document.getElementById('tabClean').classList.toggle('active', tab === 'clean');
|
||
document.getElementById('tabRaw').classList.toggle('active', tab === 'raw');
|
||
renderJsonDisplay();
|
||
}
|
||
|
||
function renderJsonDisplay() {
|
||
const target = currentJsonTab === 'clean' ? latestCleanData : latestFullResponse;
|
||
document.getElementById('jsonDisplay').innerText = JSON.stringify(target, null, 2);
|
||
}
|
||
|
||
function copyCurrentJson() {
|
||
const text = document.getElementById('jsonDisplay').innerText;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const btn = document.querySelector('.copy-btn');
|
||
const prev = btn.innerText;
|
||
btn.innerText = 'Copied!';
|
||
setTimeout(() => btn.innerText = prev, 1500);
|
||
});
|
||
}
|
||
|
||
async function fetchTransactions() {
|
||
try {
|
||
const res = await fetch('/api/transactions/?limit=10');
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const tbody = document.getElementById('transactionsTbody');
|
||
if (!data.results || data.results.length === 0) return;
|
||
|
||
tbody.innerHTML = data.results.map(tx => `
|
||
<tr onclick="loadTransaction('${tx.reference_id}')">
|
||
<td class="mono" style="color: var(--text-muted);">${tx.created_at ? tx.created_at.replace('T', ' ').substring(0, 19) : ''}</td>
|
||
<td class="mono" style="color: var(--info);">${tx.reference_id ? tx.reference_id.substring(0, 8) + '...' : ''}</td>
|
||
<td><strong>${(tx.document_type || '').toUpperCase()}</strong></td>
|
||
<td><span style="color: #A78BFA; font-weight: 600;">${tx.successful_vendor || 'exhausted'}</span></td>
|
||
<td>
|
||
<span class="status-pill ${tx.status === 'SUCCESS' || tx.status === 'VERIFIED' ? 'success' : 'failed'}">
|
||
${tx.status}
|
||
</span>
|
||
</td>
|
||
<td class="mono">${tx.total_latency_ms ? tx.total_latency_ms.toFixed(1) + ' ms' : '0 ms'}</td>
|
||
<td><button class="chip-btn" onclick="event.stopPropagation(); loadTransaction('${tx.reference_id}')">Inspect</button></td>
|
||
</tr>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error("Failed to refresh transactions", e);
|
||
}
|
||
}
|
||
|
||
async function loadTransaction(refId) {
|
||
try {
|
||
const res = await fetch(`/api/transactions/${refId}/`);
|
||
if (!res.ok) return;
|
||
const tx = await res.json();
|
||
|
||
// Synthesize standard response structure
|
||
const result = {
|
||
reference_id: tx.reference_id,
|
||
is_success: tx.status === 'SUCCESS' || tx.status === 'VERIFIED',
|
||
status: tx.status,
|
||
document_type: tx.document_type,
|
||
successful_vendor: tx.successful_vendor,
|
||
data: tx.final_data || {},
|
||
attempts: tx.attempts || [],
|
||
total_latency_ms: tx.total_latency_ms,
|
||
error_message: tx.status !== 'SUCCESS' ? 'Retrieved from transaction audit log.' : ''
|
||
};
|
||
renderResults(result, tx.total_latency_ms || 0);
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
} catch (e) {
|
||
console.error("Failed to load transaction details", e);
|
||
}
|
||
}
|
||
|
||
// Initialize default form
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
renderFormFields();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|