documentverification/templates/verifier/portal.html
vickytechkey 0291bd6195
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s
feat: add core verifier engine, serializers, tests, vendor client, and test portal template
2026-10-17 13:34:43 +05:30

1320 lines
49 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>