All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 23s
1956 lines
66 KiB
HTML
1956 lines
66 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Seller Central — Developer Studio & Log Viewer</title>
|
||
<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=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-main: #0b0f19;
|
||
--bg-card: #111827;
|
||
--bg-card-hover: #162032;
|
||
--bg-input: #1e293b;
|
||
--border-color: #243247;
|
||
--border-highlight: #3b82f6;
|
||
--text-main: #f8fafc;
|
||
--text-muted: #94a3b8;
|
||
--text-faint: #64748b;
|
||
--primary: #3b82f6;
|
||
--primary-hover: #2563eb;
|
||
--primary-glow: rgba(59, 130, 246, 0.25);
|
||
--success: #10b981;
|
||
--success-glow: rgba(16, 185, 129, 0.2);
|
||
--warning: #f59e0b;
|
||
--warning-glow: rgba(245, 158, 11, 0.2);
|
||
--danger: #ef4444;
|
||
--danger-glow: rgba(239, 68, 68, 0.2);
|
||
--info: #06b6d4;
|
||
--radius-sm: 6px;
|
||
--radius-md: 10px;
|
||
--radius-lg: 14px;
|
||
--font-mono: 'JetBrains Mono', Consolas, monospace;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
background-color: var(--bg-main);
|
||
color: var(--text-main);
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* Top Navbar */
|
||
header {
|
||
background: rgba(17, 24, 39, 0.85);
|
||
backdrop-filter: blur(12px);
|
||
border-bottom: 1px solid var(--border-color);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
padding: 0.75rem 1.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.brand-logo {
|
||
width: 34px;
|
||
height: 34px;
|
||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||
border-radius: var(--radius-sm);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
color: white;
|
||
font-size: 1.1rem;
|
||
box-shadow: 0 0 15px var(--primary-glow);
|
||
}
|
||
|
||
.brand-text h1 {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: var(--text-main);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.brand-text p {
|
||
font-size: 0.75rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.nav-tabs {
|
||
display: flex;
|
||
background: var(--bg-input);
|
||
padding: 3px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-color);
|
||
gap: 2px;
|
||
}
|
||
|
||
.nav-tab-btn {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
padding: 0.45rem 1rem;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.nav-tab-btn:hover {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.nav-tab-btn.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
box-shadow: 0 2px 8px var(--primary-glow);
|
||
}
|
||
|
||
.nav-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.status-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
background: rgba(16, 185, 129, 0.12);
|
||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||
color: #34d399;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
padding: 0.3rem 0.65rem;
|
||
border-radius: 9999px;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
background-color: #10b981;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 8px #10b981;
|
||
animation: pulse-dot 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.4; transform: scale(0.85); }
|
||
}
|
||
|
||
.auth-badge {
|
||
font-size: 0.75rem;
|
||
padding: 0.3rem 0.65rem;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--text-muted);
|
||
max-width: 220px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.auth-badge strong {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
/* Main Container */
|
||
main {
|
||
flex: 1;
|
||
padding: 1.5rem;
|
||
max-width: 1560px;
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.tab-pane {
|
||
display: none;
|
||
}
|
||
|
||
.tab-pane.active {
|
||
display: block;
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(4px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Grid Layouts */
|
||
.grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.25rem;
|
||
}
|
||
|
||
.grid-3 {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1.25rem;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.grid-2, .grid-3 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Card Component */
|
||
.card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-lg);
|
||
padding: 1.25rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding-bottom: 0.75rem;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
color: var(--text-main);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.card-title-tag {
|
||
font-size: 0.7rem;
|
||
background: rgba(59, 130, 246, 0.15);
|
||
color: #60a5fa;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Form Inputs */
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
label {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
input, select, textarea {
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-main);
|
||
padding: 0.55rem 0.75rem;
|
||
font-size: 0.85rem;
|
||
font-family: inherit;
|
||
outline: none;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||
width: 100%;
|
||
}
|
||
|
||
input:focus, select:focus, textarea:focus {
|
||
border-color: var(--border-highlight);
|
||
box-shadow: 0 0 0 2px var(--primary-glow);
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.45rem;
|
||
padding: 0.55rem 1rem;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
border-radius: var(--radius-sm);
|
||
border: none;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--primary-hover);
|
||
box-shadow: 0 2px 10px var(--primary-glow);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--bg-input);
|
||
color: var(--text-main);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #059669;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #10b981;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc2626;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #ef4444;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.35rem 0.65rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
/* Log Viewer Dashboard Styles */
|
||
.log-dashboard-layout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.stats-bar {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-md);
|
||
padding: 0.85rem 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.stat-card .stat-info p {
|
||
font-size: 0.72rem;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stat-card .stat-info h3 {
|
||
font-size: 1.4rem;
|
||
font-weight: 700;
|
||
color: var(--text-main);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.stat-card.error-card { border-left: 4px solid var(--danger); }
|
||
.stat-card.error-card .stat-info h3 { color: #f87171; }
|
||
.stat-card.warn-card { border-left: 4px solid var(--warning); }
|
||
.stat-card.warn-card .stat-info h3 { color: #fbbf24; }
|
||
.stat-card.info-card { border-left: 4px solid var(--info); }
|
||
.stat-card.info-card .stat-info h3 { color: #38bdf8; }
|
||
.stat-card.total-card { border-left: 4px solid var(--primary); }
|
||
|
||
/* Filter Toolbar */
|
||
.filter-toolbar {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-lg);
|
||
padding: 1rem;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 0.6rem;
|
||
}
|
||
|
||
.pill-btn-group {
|
||
display: flex;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pill-btn {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
padding: 0.35rem 0.75rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.pill-btn:hover {
|
||
color: var(--text-main);
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.pill-btn.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.pill-btn.pill-error.active { background: var(--danger); }
|
||
.pill-btn.pill-warn.active { background: var(--warning); color: black; }
|
||
.pill-btn.pill-info.active { background: var(--info); color: black; }
|
||
.pill-btn.pill-debug.active { background: #8b5cf6; }
|
||
|
||
.search-box-wrap {
|
||
position: relative;
|
||
min-width: 260px;
|
||
}
|
||
|
||
.search-box-wrap input {
|
||
padding-left: 2rem;
|
||
}
|
||
|
||
.search-icon {
|
||
position: absolute;
|
||
left: 0.7rem;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: var(--text-muted);
|
||
font-size: 0.85rem;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Terminal Console */
|
||
.terminal-container {
|
||
background: #070a12;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-lg);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 620px;
|
||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
.terminal-header {
|
||
background: #0d121f;
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding: 0.6rem 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.terminal-title {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
}
|
||
|
||
.terminal-dots {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
.dot-red { background: #ff5f56; }
|
||
.dot-yellow { background: #ffbd2e; }
|
||
.dot-green { background: #27c93f; }
|
||
|
||
.terminal-body {
|
||
flex: 1;
|
||
padding: 0.85rem 1rem;
|
||
overflow-y: auto;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.82rem;
|
||
line-height: 1.55;
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.terminal-body::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
.terminal-body::-webkit-scrollbar-track {
|
||
background: #070a12;
|
||
}
|
||
.terminal-body::-webkit-scrollbar-thumb {
|
||
background: #1e293b;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.log-line {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 2px 4px;
|
||
border-radius: 4px;
|
||
transition: background 0.1s ease;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.log-line:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.log-num {
|
||
color: #475569;
|
||
user-select: none;
|
||
min-width: 38px;
|
||
text-align: right;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.log-ts {
|
||
color: #64748b;
|
||
font-size: 0.75rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.log-badge {
|
||
font-size: 0.68rem;
|
||
font-weight: 700;
|
||
padding: 1px 6px;
|
||
border-radius: 3px;
|
||
letter-spacing: 0.04em;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.badge-error { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.4); }
|
||
.badge-warn, .badge-warning { background: rgba(245, 158, 11, 0.2); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.4); }
|
||
.badge-info { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.4); }
|
||
.badge-debug { background: rgba(139, 92, 246, 0.2); color: #c084fc; border: 1px solid rgba(139, 92, 246, 0.4); }
|
||
|
||
.log-msg {
|
||
flex: 1;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.log-line.is-error .log-msg { color: #fca5a5; }
|
||
.log-line.is-warning .log-msg { color: #fde68a; }
|
||
|
||
.log-highlight {
|
||
background: #eab308;
|
||
color: #000;
|
||
padding: 0 2px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.terminal-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: var(--text-faint);
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
/* Response Inspector */
|
||
.response-inspector {
|
||
background: #070a12;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
margin-top: 0.75rem;
|
||
}
|
||
|
||
.response-header {
|
||
background: #0d121f;
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding: 0.45rem 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.status-code {
|
||
font-weight: 700;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.status-2xx { background: rgba(16, 185, 129, 0.2); color: #34d399; }
|
||
.status-4xx { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
|
||
.status-5xx { background: rgba(239, 68, 68, 0.2); color: #f87171; }
|
||
|
||
.response-body {
|
||
padding: 0.75rem;
|
||
max-height: 280px;
|
||
overflow-y: auto;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.78rem;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
/* Table */
|
||
.data-table-wrap {
|
||
overflow-x: auto;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
th {
|
||
background: var(--bg-input);
|
||
text-align: left;
|
||
padding: 0.6rem 0.75rem;
|
||
color: var(--text-muted);
|
||
font-size: 0.72rem;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
td {
|
||
padding: 0.6rem 0.75rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
color: var(--text-main);
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
/* Modals & Alerts */
|
||
.alert-toast {
|
||
position: fixed;
|
||
bottom: 1.5rem;
|
||
right: 1.5rem;
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-color);
|
||
padding: 0.85rem 1.25rem;
|
||
border-radius: var(--radius-md);
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
font-size: 0.85rem;
|
||
z-index: 1000;
|
||
animation: slideUp 0.25s ease;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from { transform: translateY(20px); opacity: 0; }
|
||
to { transform: translateY(0); opacity: 1; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Header -->
|
||
<header>
|
||
<div class="brand">
|
||
<div class="brand-logo">⚡</div>
|
||
<div class="brand-text">
|
||
<h1>Seller Central Backend</h1>
|
||
<p>Developer Testing Studio & Real-time Log Dashboard</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation Tabs -->
|
||
<div class="nav-tabs">
|
||
<button class="nav-tab-btn active" onclick="switchTab('logs-tab')">
|
||
<span>📜</span> Log Dashboard
|
||
</button>
|
||
<button class="nav-tab-btn" onclick="switchTab('tester-tab')">
|
||
<span>🧪</span> API Test Suite
|
||
</button>
|
||
<button class="nav-tab-btn" onclick="switchTab('sandbox-tab')">
|
||
<span>⚡</span> API Sandbox
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Header Meta & Auth Status -->
|
||
<div class="nav-meta">
|
||
<div class="status-pill">
|
||
<span class="status-dot"></span>
|
||
<span>Online ({{ environment }})</span>
|
||
</div>
|
||
<div class="auth-badge" id="headerAuthBadge" title="JWT Access Token Status">
|
||
Auth: <strong id="headerAuthUser">Dummy / Anonymous</strong>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main>
|
||
|
||
<!-- ================================================================= -->
|
||
<!-- TAB 1: LOG DASHBOARD (WITH RICH FILTERS) -->
|
||
<!-- ================================================================= -->
|
||
<div id="logs-tab" class="tab-pane active">
|
||
<div class="log-dashboard-layout">
|
||
|
||
<!-- Stats Overview Bar -->
|
||
<div class="stats-bar" id="logStatsBar">
|
||
<div class="stat-card total-card">
|
||
<div class="stat-info">
|
||
<p>Total Lines Displayed</p>
|
||
<h3 id="statTotalLines">0</h3>
|
||
</div>
|
||
<span style="font-size: 1.5rem;">📄</span>
|
||
</div>
|
||
<div class="stat-card error-card">
|
||
<div class="stat-info">
|
||
<p>Errors (Critical)</p>
|
||
<h3 id="statErrorCount">0</h3>
|
||
</div>
|
||
<span style="font-size: 1.5rem;">🔴</span>
|
||
</div>
|
||
<div class="stat-card warn-card">
|
||
<div class="stat-info">
|
||
<p>Warnings</p>
|
||
<h3 id="statWarnCount">0</h3>
|
||
</div>
|
||
<span style="font-size: 1.5rem;">🟡</span>
|
||
</div>
|
||
<div class="stat-card info-card">
|
||
<div class="stat-info">
|
||
<p>Info / Debug</p>
|
||
<h3 id="statInfoCount">0</h3>
|
||
</div>
|
||
<span style="font-size: 1.5rem;">🔵</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter & Control Toolbar -->
|
||
<div class="filter-toolbar">
|
||
<!-- Left: File Selector & Level Filter -->
|
||
<div class="filter-group">
|
||
<label style="margin-right: 0.25rem;">File:</label>
|
||
<div class="pill-btn-group" id="logFileSelector">
|
||
<button class="pill-btn active" onclick="setLogFile('app.log')">app.log</button>
|
||
<button class="pill-btn" onclick="setLogFile('error.log')">error.log</button>
|
||
<button class="pill-btn" onclick="setLogFile('requests.log')">requests.log</button>
|
||
<button class="pill-btn" onclick="setLogFile('all')">all</button>
|
||
</div>
|
||
|
||
<label style="margin-left: 0.5rem; margin-right: 0.25rem;">Level:</label>
|
||
<div class="pill-btn-group" id="logLevelSelector">
|
||
<button class="pill-btn active" onclick="setLogLevel('ALL')">ALL</button>
|
||
<button class="pill-btn pill-error" onclick="setLogLevel('ERROR')">ERROR</button>
|
||
<button class="pill-btn pill-warn" onclick="setLogLevel('WARNING')">WARN</button>
|
||
<button class="pill-btn pill-info" onclick="setLogLevel('INFO')">INFO</button>
|
||
<button class="pill-btn pill-debug" onclick="setLogLevel('DEBUG')">DEBUG</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Middle: Text Search Filter -->
|
||
<div class="filter-group">
|
||
<div class="search-box-wrap">
|
||
<span class="search-icon">🔍</span>
|
||
<input type="text" id="logSearchInput" placeholder="Filter logs (e.g. login, 404, OTP)..." onkeyup="handleSearchKeyUp(event)">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Limit, Auto-refresh & Action Buttons -->
|
||
<div class="filter-group">
|
||
<label>Limit:</label>
|
||
<select id="logLimitSelect" onchange="fetchLogs()" style="width: auto; padding: 0.35rem 0.6rem;">
|
||
<option value="50">50</option>
|
||
<option value="150" selected>150</option>
|
||
<option value="300">300</option>
|
||
<option value="600">600</option>
|
||
<option value="1200">1200</option>
|
||
</select>
|
||
|
||
<label>Auto-Refresh:</label>
|
||
<select id="logAutoRefreshSelect" onchange="changeAutoRefresh()" style="width: auto; padding: 0.35rem 0.6rem;">
|
||
<option value="0">Off</option>
|
||
<option value="2000">2s</option>
|
||
<option value="5000" selected>5s</option>
|
||
<option value="10000">10s</option>
|
||
</select>
|
||
|
||
<button class="btn btn-primary btn-sm" onclick="fetchLogs()" title="Refresh now">
|
||
🔄 Refresh
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="copyLogsToClipboard()" title="Copy to clipboard">
|
||
📋 Copy
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="downloadLogs()" title="Download raw log">
|
||
📥 Download
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="clearCurrentLog()" title="Clear/truncate log file">
|
||
🗑️ Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Terminal Log Viewer Console -->
|
||
<div class="terminal-container">
|
||
<div class="terminal-header">
|
||
<div class="terminal-title">
|
||
<div class="terminal-dots">
|
||
<span class="dot dot-red"></span>
|
||
<span class="dot dot-yellow"></span>
|
||
<span class="dot dot-green"></span>
|
||
</div>
|
||
<span id="terminalFileTitle">logs/app.log</span>
|
||
<span id="terminalActiveFilterNotice" style="color: #60a5fa; font-size: 0.72rem;"></span>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem; align-items: center; font-size: 0.72rem; color: var(--text-faint);">
|
||
<span id="terminalLastUpdated">Updated: Never</span>
|
||
<button class="btn btn-secondary btn-sm" style="padding: 2px 6px;" onclick="scrollToBottom()">Scroll to Bottom ⬇</button>
|
||
</div>
|
||
</div>
|
||
<div class="terminal-body" id="terminalBody">
|
||
<div class="terminal-empty">
|
||
<span>⏳ Loading logs...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ================================================================= -->
|
||
<!-- TAB 2: BACKEND API TEST SUITE -->
|
||
<!-- ================================================================= -->
|
||
<div id="tester-tab" class="tab-pane">
|
||
<div class="grid-2">
|
||
|
||
<!-- 1. Authentication & Session Module -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>🔐</span> 1. Authentication & User Accounts
|
||
</div>
|
||
<span class="card-title-tag">POST /api/auth/*</span>
|
||
</div>
|
||
|
||
<!-- Auth Sub-tabs -->
|
||
<div class="pill-btn-group" style="width: 100%; display: flex;">
|
||
<button id="tabBtnRegister" class="pill-btn active" style="flex: 1; text-align: center;" onclick="switchAuthSubTab('register')">
|
||
📝 Create Account
|
||
</button>
|
||
<button id="tabBtnLogin" class="pill-btn" style="flex: 1; text-align: center;" onclick="switchAuthSubTab('login')">
|
||
🔑 Sign In
|
||
</button>
|
||
<button id="tabBtnOtp" class="pill-btn" style="flex: 1; text-align: center;" onclick="switchAuthSubTab('otp')">
|
||
📱 Verify OTP
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Sub-tab 1: Create Account Form -->
|
||
<div id="authSubTabRegister">
|
||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
||
Create a brand new supplier account. Generates JWT tokens and sends mock verification OTPs.
|
||
</p>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Username</label>
|
||
<input type="text" id="regUsername" value="artisan_seller_01">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Email Address</label>
|
||
<input type="email" id="regEmail" value="artisan01@example.com">
|
||
</div>
|
||
</div>
|
||
<div class="form-row" style="margin-top: 0.5rem;">
|
||
<div class="form-group">
|
||
<label>Phone Number (10 Digits)</label>
|
||
<input type="text" id="regPhone" value="9876543210">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Password (Min 8 chars)</label>
|
||
<input type="password" id="regPassword" value="SecurePassword123!">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem; margin-top: 0.85rem; flex-wrap: wrap;">
|
||
<button class="btn btn-success btn-sm" onclick="runCreateAccount()">
|
||
✨ Create Account
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="randomizeRegisterFields()">
|
||
🎲 Randomize Fields
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sub-tab 2: Sign In Form -->
|
||
<div id="authSubTabLogin" style="display: none;">
|
||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
||
Authenticate an existing supplier with username or phone number.
|
||
</p>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Username or Phone</label>
|
||
<input type="text" id="authUsername" value="demo_seller">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Password</label>
|
||
<input type="password" id="authPassword" value="super_secure_pass_123">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem; margin-top: 0.85rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runLogin()">
|
||
🔑 Sign In
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="fillDemoSeller()">
|
||
⚡ Fill Demo Seller
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="runLogout()">
|
||
🚪 Logout
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sub-tab 3: Verify OTP Form -->
|
||
<div id="authSubTabOtp" style="display: none;">
|
||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
||
Validate simulated 2FA verification codes (default mock code: <code>123456</code>).
|
||
</p>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>OTP Verification Type</label>
|
||
<select id="otpType">
|
||
<option value="phone">Phone OTP</option>
|
||
<option value="email">Email OTP</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>OTP Code</label>
|
||
<input type="text" id="otpCode" value="123456">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem; margin-top: 0.85rem;">
|
||
<button class="btn btn-primary btn-sm" onclick="runVerifyOtp()">
|
||
✅ Verify OTP Code
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Session Info Card -->
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.65rem; display: flex; align-items: center; justify-content: space-between; font-size: 0.75rem; color: var(--text-muted);">
|
||
<span>Active: <strong id="sessionUserLabel" style="color: var(--text-main);">Anonymous / Dummy</strong></span>
|
||
<div style="display: flex; gap: 0.4rem;">
|
||
<button class="btn btn-secondary btn-sm" style="padding: 2px 7px; font-size: 0.7rem;" onclick="copyToken()">Copy JWT</button>
|
||
<button class="btn btn-secondary btn-sm" style="padding: 2px 7px; font-size: 0.7rem;" onclick="runLogout()">Logout</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="authResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="authStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="authResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 2. Supplier Profile & KYC Module -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>👤</span> 2. Supplier Profile & KYC
|
||
</div>
|
||
<span class="card-title-tag">/api/profile/*</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.8rem; color: var(--text-muted);">
|
||
Query verified seller profiles, update business coordinates, and submit KYC credentials (GSTIN / PAN).
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runGetProfile()">
|
||
🔍 Get Current Profile
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="runUpdateProfile()">
|
||
✏️ Update Profile Details
|
||
</button>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>15-Digit GSTIN</label>
|
||
<input type="text" id="profileGstin" value="29AAAAA1111A1Z1">
|
||
</div>
|
||
<div class="form-group" style="justify-content: flex-end;">
|
||
<button class="btn btn-secondary btn-sm" style="height: 36px;" onclick="runSubmitGstin()">
|
||
🏛️ Submit GSTIN
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>PAN Card Verification</label>
|
||
<input type="text" id="profilePan" value="ABCDE1234F">
|
||
</div>
|
||
<div class="form-group" style="justify-content: flex-end;">
|
||
<button class="btn btn-secondary btn-sm" style="height: 36px;" onclick="runVerifyPan()">
|
||
🆔 Verify PAN
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="profileResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="profileStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="profileResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 3. Product Catalog Management -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>📦</span> 3. Product Catalog
|
||
</div>
|
||
<span class="card-title-tag">/api/products/*</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.8rem; color: var(--text-muted);">
|
||
Manage products, query inventory by filters, create items, and execute bulk uploads.
|
||
</p>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Search Keyword</label>
|
||
<input type="text" id="prodSearch" placeholder="Search by title or SKU...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Low Stock Filter</label>
|
||
<select id="prodLowStock">
|
||
<option value="">All Stock Levels</option>
|
||
<option value="true">Low Stock (≤ 5)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runListProducts()">
|
||
📋 List Products
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="runCreateSampleProduct()">
|
||
➕ Create Sample Product
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="runBulkUploadDemo()">
|
||
📦 Bulk Upload Demo JSON
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="productResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="productStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="productResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 4. Order Management & Fulfillment -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>🚚</span> 4. Order Fulfillment
|
||
</div>
|
||
<span class="card-title-tag">/api/orders/*</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.8rem; color: var(--text-muted);">
|
||
Track incoming supplier orders and test state transitions (Accept ➡️ Ready to Ship with DHL tracking).
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runListOrders()">
|
||
📋 List Orders
|
||
</button>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Order ID (DB ID)</label>
|
||
<input type="text" id="targetOrderId" placeholder="e.g. 1">
|
||
</div>
|
||
<div class="form-group" style="justify-content: flex-end; display: flex; gap: 0.5rem; flex-direction: row; align-items: flex-end;">
|
||
<button class="btn btn-success btn-sm" onclick="runAcceptOrder()">
|
||
✅ Accept Order
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="runRejectOrder()">
|
||
❌ Reject Order
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="orderResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="orderStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="orderResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 5. Customer Returns -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>🔄</span> 5. Customer Returns
|
||
</div>
|
||
<span class="card-title-tag">/api/returns/*</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.8rem; color: var(--text-muted);">
|
||
Inspect customer return requests and test resolution (Approve ➡️ In Transit).
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runListReturns()">
|
||
📋 List Return Requests
|
||
</button>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Return Request ID (DB ID)</label>
|
||
<input type="text" id="targetReturnId" placeholder="e.g. 1">
|
||
</div>
|
||
<div class="form-group" style="justify-content: flex-end; display: flex; gap: 0.5rem; flex-direction: row; align-items: flex-end;">
|
||
<button class="btn btn-success btn-sm" onclick="runReturnAction('Approved')">
|
||
👍 Approve Return
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="runReturnAction('Rejected')">
|
||
👎 Reject Return
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="returnResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="returnStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="returnResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 6. Wallet & Payouts -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>💰</span> 6. Wallet & Payouts
|
||
</div>
|
||
<span class="card-title-tag">/api/wallet/*</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.8rem; color: var(--text-muted);">
|
||
View ledger balances and test withdrawal operations with generated audit transaction receipts.
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||
<button class="btn btn-primary btn-sm" onclick="runGetWallet()">
|
||
💳 Check Wallet Balance
|
||
</button>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid var(--border-color); padding-top: 0.75rem;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Withdraw Amount ($)</label>
|
||
<input type="number" id="withdrawAmount" value="50.00" step="10">
|
||
</div>
|
||
<div class="form-group" style="justify-content: flex-end;">
|
||
<button class="btn btn-secondary btn-sm" style="height: 36px;" onclick="runWithdraw()">
|
||
💸 Request Payout
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="walletResponseWrap" style="display: none;">
|
||
<div class="response-header">
|
||
<span>Response Inspector</span>
|
||
<span class="status-code" id="walletStatusCode">200 OK</span>
|
||
</div>
|
||
<div class="response-body" id="walletResponseBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ================================================================= -->
|
||
<!-- TAB 3: INTERACTIVE API SANDBOX -->
|
||
<!-- ================================================================= -->
|
||
<div id="sandbox-tab" class="tab-pane">
|
||
<div class="card" style="max-width: 1000px; margin: 0 auto;">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span>⚡</span> Interactive REST API Runner
|
||
</div>
|
||
<span class="card-title-tag">Custom HTTP Client</span>
|
||
</div>
|
||
|
||
<p style="font-size: 0.85rem; color: var(--text-muted);">
|
||
Send arbitrary HTTP requests to any backend endpoint with custom methods, headers, and request body.
|
||
</p>
|
||
|
||
<!-- Method & URL -->
|
||
<div style="display: flex; gap: 0.75rem;">
|
||
<select id="sandboxMethod" style="width: 120px; font-weight: 700;">
|
||
<option value="GET">GET</option>
|
||
<option value="POST">POST</option>
|
||
<option value="PUT">PUT</option>
|
||
<option value="DELETE">DELETE</option>
|
||
<option value="PATCH">PATCH</option>
|
||
</select>
|
||
<input type="text" id="sandboxUrl" value="/api/profile/" placeholder="/api/endpoint/">
|
||
<button class="btn btn-primary" onclick="sendSandboxRequest()" style="min-width: 130px;">
|
||
🚀 Send Request
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Quick Presets -->
|
||
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">Presets:</span>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadSandboxPreset('GET', '/api/profile/')">GET /api/profile/</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadSandboxPreset('GET', '/api/products/')">GET /api/products/</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadSandboxPreset('GET', '/api/orders/')">GET /api/orders/</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadSandboxPreset('GET', '/api/wallet/')">GET /api/wallet/</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadSandboxPreset('GET', '/api/logs/?limit=50')">GET /api/logs/</button>
|
||
</div>
|
||
|
||
<!-- Request Body -->
|
||
<div class="form-group">
|
||
<label>JSON Request Body (For POST / PUT / PATCH):</label>
|
||
<textarea id="sandboxBody" rows="7" placeholder='{"key": "value"}'></textarea>
|
||
</div>
|
||
|
||
<!-- Response Inspector -->
|
||
<div class="response-inspector" id="sandboxResponseWrap" style="display: block;">
|
||
<div class="response-header">
|
||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||
<span>Response:</span>
|
||
<span class="status-code" id="sandboxStatusCode">— Ready —</span>
|
||
<span id="sandboxLatency" style="color: var(--text-muted); font-size: 0.7rem;"></span>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm" style="padding: 2px 6px;" onclick="copySandboxResponse()">Copy JSON</button>
|
||
</div>
|
||
<div class="response-body" id="sandboxResponseBody" style="min-height: 180px;">// Click 'Send Request' to inspect backend output.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<!-- Notification Toast -->
|
||
<div id="toast" class="alert-toast" style="display: none;">
|
||
<span id="toastIcon">ℹ️</span>
|
||
<span id="toastMsg">Notification message</span>
|
||
</div>
|
||
|
||
<script>
|
||
// Global State
|
||
let activeToken = localStorage.getItem('seller_central_access_token') || '';
|
||
let currentLogFile = 'app.log';
|
||
let currentLogLevel = 'ALL';
|
||
let autoRefreshTimer = null;
|
||
|
||
// Initialize
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
updateAuthUI();
|
||
fetchLogs();
|
||
setupAutoRefresh();
|
||
});
|
||
|
||
// Toast Helper
|
||
function showToast(msg, icon = 'ℹ️') {
|
||
const toast = document.getElementById('toast');
|
||
document.getElementById('toastMsg').innerText = msg;
|
||
document.getElementById('toastIcon').innerText = icon;
|
||
toast.style.display = 'flex';
|
||
setTimeout(() => {
|
||
toast.style.display = 'none';
|
||
}, 3500);
|
||
}
|
||
|
||
// Tab Switching
|
||
function switchTab(tabId) {
|
||
document.querySelectorAll('.tab-pane').forEach(el => el.classList.remove('active'));
|
||
document.querySelectorAll('.nav-tab-btn').forEach(el => el.classList.remove('active'));
|
||
|
||
const targetPane = document.getElementById(tabId);
|
||
if (targetPane) targetPane.classList.add('active');
|
||
|
||
const activeBtn = Array.from(document.querySelectorAll('.nav-tab-btn')).find(b =>
|
||
(tabId === 'logs-tab' && b.innerText.includes('Log')) ||
|
||
(tabId === 'tester-tab' && b.innerText.includes('Test')) ||
|
||
(tabId === 'sandbox-tab' && b.innerText.includes('Sandbox'))
|
||
);
|
||
if (activeBtn) activeBtn.classList.add('active');
|
||
|
||
if (tabId === 'logs-tab') {
|
||
fetchLogs();
|
||
}
|
||
}
|
||
|
||
// Update Header Auth UI
|
||
function updateAuthUI() {
|
||
const authBadge = document.getElementById('headerAuthUser');
|
||
const sessionLabel = document.getElementById('sessionUserLabel');
|
||
const savedUser = localStorage.getItem('seller_central_active_user') || '';
|
||
|
||
if (activeToken) {
|
||
authBadge.innerText = savedUser ? `User: ${savedUser}` : 'Token Active (Bearer)';
|
||
authBadge.style.color = '#34d399';
|
||
if (sessionLabel) {
|
||
sessionLabel.innerText = savedUser ? `${savedUser} (Authenticated)` : 'Logged In (Bearer Token)';
|
||
sessionLabel.style.color = '#34d399';
|
||
}
|
||
} else {
|
||
authBadge.innerText = 'Dummy / Anonymous';
|
||
authBadge.style.color = '#94a3b8';
|
||
if (sessionLabel) {
|
||
sessionLabel.innerText = 'Anonymous / Dummy';
|
||
sessionLabel.style.color = '#94a3b8';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Base API Request Helper
|
||
async function apiRequest(endpoint, method = 'GET', body = null) {
|
||
const startTime = performance.now();
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
};
|
||
if (activeToken) {
|
||
headers['Authorization'] = `Bearer ${activeToken}`;
|
||
}
|
||
|
||
const options = {
|
||
method,
|
||
headers,
|
||
};
|
||
if (body && method !== 'GET') {
|
||
options.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(endpoint, options);
|
||
const duration = Math.round(performance.now() - startTime);
|
||
let data = null;
|
||
const text = await res.text();
|
||
try {
|
||
data = JSON.parse(text);
|
||
} catch {
|
||
data = text;
|
||
}
|
||
|
||
return {
|
||
status: res.status,
|
||
statusText: res.statusText,
|
||
ok: res.ok,
|
||
duration,
|
||
data
|
||
};
|
||
} catch (err) {
|
||
return {
|
||
status: 0,
|
||
statusText: 'Network Error',
|
||
ok: false,
|
||
duration: 0,
|
||
data: { error: err.message }
|
||
};
|
||
}
|
||
}
|
||
|
||
// Display formatted response in inspector box
|
||
function showInspector(wrapId, statusId, bodyId, res) {
|
||
const wrap = document.getElementById(wrapId);
|
||
const statusEl = document.getElementById(statusId);
|
||
const bodyEl = document.getElementById(bodyId);
|
||
|
||
wrap.style.display = 'block';
|
||
statusEl.innerText = `${res.status} ${res.statusText} (${res.duration}ms)`;
|
||
statusEl.className = 'status-code ' + (res.ok ? 'status-2xx' : (res.status >= 500 ? 'status-5xx' : 'status-4xx'));
|
||
|
||
bodyEl.innerText = typeof res.data === 'object' ? JSON.stringify(res.data, null, 2) : String(res.data);
|
||
}
|
||
|
||
// =========================================================================
|
||
// LOG VIEWER DASHBOARD LOGIC
|
||
// =========================================================================
|
||
|
||
function setLogFile(filename) {
|
||
currentLogFile = filename;
|
||
document.querySelectorAll('#logFileSelector .pill-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.innerText.trim() === filename);
|
||
});
|
||
document.getElementById('terminalFileTitle').innerText = filename === 'all' ? 'Combined (all logs)' : `logs/${filename}`;
|
||
fetchLogs();
|
||
}
|
||
|
||
function setLogLevel(level) {
|
||
currentLogLevel = level;
|
||
document.querySelectorAll('#logLevelSelector .pill-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.innerText.trim() === level);
|
||
});
|
||
fetchLogs();
|
||
}
|
||
|
||
function handleSearchKeyUp(e) {
|
||
if (e.key === 'Enter') {
|
||
fetchLogs();
|
||
}
|
||
}
|
||
|
||
function changeAutoRefresh() {
|
||
setupAutoRefresh();
|
||
fetchLogs();
|
||
}
|
||
|
||
function setupAutoRefresh() {
|
||
if (autoRefreshTimer) {
|
||
clearInterval(autoRefreshTimer);
|
||
autoRefreshTimer = null;
|
||
}
|
||
const val = parseInt(document.getElementById('logAutoRefreshSelect').value, 10);
|
||
if (val > 0) {
|
||
autoRefreshTimer = setInterval(() => {
|
||
fetchLogs(true);
|
||
}, val);
|
||
}
|
||
}
|
||
|
||
async function fetchLogs(isSilent = false) {
|
||
const limit = document.getElementById('logLimitSelect').value;
|
||
const search = document.getElementById('logSearchInput').value.trim();
|
||
|
||
const params = new URLSearchParams({
|
||
file: currentLogFile,
|
||
level: currentLogLevel,
|
||
limit: limit,
|
||
tail: 'true',
|
||
});
|
||
if (search) {
|
||
params.set('search', search);
|
||
}
|
||
|
||
if (!isSilent) {
|
||
document.getElementById('terminalActiveFilterNotice').innerText = 'Loading...';
|
||
}
|
||
|
||
const res = await apiRequest(`/api/logs/?${params.toString()}`);
|
||
if (!res.ok) {
|
||
document.getElementById('terminalBody').innerHTML = `<div class="terminal-empty" style="color: #f87171;">⚠️ Failed to load logs: ${res.data?.error || res.statusText}</div>`;
|
||
return;
|
||
}
|
||
|
||
const data = res.data;
|
||
renderLogs(data.logs || []);
|
||
|
||
// Update counters
|
||
document.getElementById('statTotalLines').innerText = data.returned_count || 0;
|
||
document.getElementById('statErrorCount').innerText = data.level_counts?.ERROR || 0;
|
||
document.getElementById('statWarnCount').innerText = data.level_counts?.WARNING || 0;
|
||
document.getElementById('statInfoCount').innerText = (data.level_counts?.INFO || 0) + (data.level_counts?.DEBUG || 0);
|
||
|
||
document.getElementById('terminalLastUpdated').innerText = `Updated: ${new Date().toLocaleTimeString()}`;
|
||
document.getElementById('terminalActiveFilterNotice').innerText = search ? `[Filter: "${search}"]` : '';
|
||
}
|
||
|
||
function renderLogs(logs) {
|
||
const body = document.getElementById('terminalBody');
|
||
const search = document.getElementById('logSearchInput').value.trim().toLowerCase();
|
||
|
||
if (!logs || logs.length === 0) {
|
||
body.innerHTML = `<div class="terminal-empty"><span>📭 No log entries match the current filters.</span></div>`;
|
||
return;
|
||
}
|
||
|
||
const rows = logs.map(entry => {
|
||
const lvl = (entry.level || 'INFO').toUpperCase();
|
||
let badgeClass = 'badge-info';
|
||
let lineClass = '';
|
||
|
||
if (lvl === 'ERROR' || lvl === 'CRITICAL') {
|
||
badgeClass = 'badge-error';
|
||
lineClass = 'is-error';
|
||
} else if (lvl === 'WARNING' || lvl === 'WARN') {
|
||
badgeClass = 'badge-warn';
|
||
lineClass = 'is-warning';
|
||
} else if (lvl === 'DEBUG') {
|
||
badgeClass = 'badge-debug';
|
||
}
|
||
|
||
let rawMsg = escapeHtml(entry.message || entry.raw || '');
|
||
if (search) {
|
||
const regex = new RegExp(`(${escapeRegExp(search)})`, 'gi');
|
||
rawMsg = rawMsg.replace(regex, '<span class="log-highlight">$1</span>');
|
||
}
|
||
|
||
const sourceTag = entry.source && currentLogFile === 'all' ? `<span style="color:#818cf8; font-size:0.7rem;">[${entry.source}]</span> ` : '';
|
||
|
||
return `
|
||
<div class="log-line ${lineClass}">
|
||
<span class="log-num">${entry.id || ''}</span>
|
||
<span class="log-ts">${entry.timestamp || ''}</span>
|
||
<span class="log-badge ${badgeClass}">${lvl}</span>
|
||
<span class="log-msg">${sourceTag}${rawMsg}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
body.innerHTML = rows;
|
||
}
|
||
|
||
function scrollToBottom() {
|
||
const body = document.getElementById('terminalBody');
|
||
body.scrollTop = body.scrollHeight;
|
||
}
|
||
|
||
function copyLogsToClipboard() {
|
||
const body = document.getElementById('terminalBody');
|
||
const text = body.innerText;
|
||
navigator.clipboard.writeText(text);
|
||
showToast('Logs copied to clipboard!', '📋');
|
||
}
|
||
|
||
function downloadLogs() {
|
||
const search = document.getElementById('logSearchInput').value.trim();
|
||
const params = new URLSearchParams({
|
||
file: currentLogFile,
|
||
level: currentLogLevel,
|
||
limit: document.getElementById('logLimitSelect').value,
|
||
raw: 'true',
|
||
});
|
||
if (search) params.set('search', search);
|
||
|
||
window.open(`/api/logs/?${params.toString()}`, '_blank');
|
||
}
|
||
|
||
async function clearCurrentLog() {
|
||
if (!confirm(`Are you sure you want to truncate and clear ${currentLogFile}?`)) return;
|
||
|
||
const res = await apiRequest('/api/logs/clear/', 'POST', { file: currentLogFile });
|
||
if (res.ok) {
|
||
showToast(`Cleared ${currentLogFile} successfully!`, '🗑️');
|
||
fetchLogs();
|
||
} else {
|
||
alert('Failed to clear log: ' + (res.data?.error || res.statusText));
|
||
}
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return str.replace(/[&<>'"]/g,
|
||
tag => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
"'": ''',
|
||
'"': '"'
|
||
}[tag] || tag)
|
||
);
|
||
}
|
||
|
||
function escapeRegExp(string) {
|
||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
// =========================================================================
|
||
// AUTHENTICATION & ACCOUNT CREATION LOGIC
|
||
// =========================================================================
|
||
|
||
function switchAuthSubTab(tab) {
|
||
document.getElementById('authSubTabRegister').style.display = tab === 'register' ? 'block' : 'none';
|
||
document.getElementById('authSubTabLogin').style.display = tab === 'login' ? 'block' : 'none';
|
||
document.getElementById('authSubTabOtp').style.display = tab === 'otp' ? 'block' : 'none';
|
||
|
||
document.getElementById('tabBtnRegister').classList.toggle('active', tab === 'register');
|
||
document.getElementById('tabBtnLogin').classList.toggle('active', tab === 'login');
|
||
document.getElementById('tabBtnOtp').classList.toggle('active', tab === 'otp');
|
||
}
|
||
|
||
function randomizeRegisterFields() {
|
||
const rand = Math.floor(1000 + Math.random() * 9000);
|
||
document.getElementById('regUsername').value = `seller_${rand}`;
|
||
document.getElementById('regEmail').value = `seller_${rand}@example.com`;
|
||
document.getElementById('regPhone').value = `9${Math.floor(100000000 + Math.random() * 900000000)}`;
|
||
document.getElementById('regPassword').value = 'SecurePassword123!';
|
||
showToast('Generated fresh random credentials!', '🎲');
|
||
}
|
||
|
||
function fillDemoSeller() {
|
||
document.getElementById('authUsername').value = 'demo_seller';
|
||
document.getElementById('authPassword').value = 'super_secure_pass_123';
|
||
showToast('Pre-filled demo seller credentials', '⚡');
|
||
}
|
||
|
||
function copyToken() {
|
||
if (!activeToken) {
|
||
showToast('No active JWT token found. Please sign in.', '⚠️');
|
||
return;
|
||
}
|
||
navigator.clipboard.writeText(activeToken);
|
||
showToast('JWT Access Token copied to clipboard!', '📋');
|
||
}
|
||
|
||
async function runCreateAccount() {
|
||
const username = document.getElementById('regUsername').value.trim();
|
||
const email = document.getElementById('regEmail').value.trim();
|
||
const phone = document.getElementById('regPhone').value.trim();
|
||
const password = document.getElementById('regPassword').value;
|
||
|
||
if (!username || !email || !phone || !password) {
|
||
alert('All fields (username, email, phone, password) are required to create an account.');
|
||
return;
|
||
}
|
||
|
||
const res = await apiRequest('/api/auth/register/', 'POST', {
|
||
username,
|
||
email,
|
||
phone,
|
||
password,
|
||
confirm_password: password
|
||
});
|
||
|
||
showInspector('authResponseWrap', 'authStatusCode', 'authResponseBody', res);
|
||
if (res.ok && res.data?.access_token) {
|
||
activeToken = res.data.access_token;
|
||
localStorage.setItem('seller_central_access_token', activeToken);
|
||
localStorage.setItem('seller_central_active_user', username);
|
||
document.getElementById('authUsername').value = username;
|
||
updateAuthUI();
|
||
showToast(`Account created for ${username}! Verification OTPs sent.`, '🎉');
|
||
|
||
// Automatically switch to OTP tab and pre-fill default test OTP
|
||
document.getElementById('otpCode').value = '123456';
|
||
switchAuthSubTab('otp');
|
||
fetchLogs();
|
||
} else {
|
||
const errMsg = typeof res.data === 'object' ? Object.entries(res.data).map(([k,v]) => `${k}: ${v}`).join(' ') : (res.data || res.statusText);
|
||
showToast(`Registration failed: ${errMsg}`, '❌');
|
||
}
|
||
}
|
||
|
||
async function runLogin() {
|
||
const u = document.getElementById('authUsername').value.trim();
|
||
const p = document.getElementById('authPassword').value;
|
||
|
||
const res = await apiRequest('/api/auth/login/', 'POST', {
|
||
username: u,
|
||
password: p
|
||
});
|
||
|
||
showInspector('authResponseWrap', 'authStatusCode', 'authResponseBody', res);
|
||
if (res.ok && res.data?.access_token) {
|
||
activeToken = res.data.access_token;
|
||
const loggedUser = res.data?.user?.username || u;
|
||
localStorage.setItem('seller_central_access_token', activeToken);
|
||
localStorage.setItem('seller_central_active_user', loggedUser);
|
||
updateAuthUI();
|
||
showToast(`Logged in successfully as ${loggedUser}!`, '🔑');
|
||
fetchLogs();
|
||
}
|
||
}
|
||
|
||
async function runLogout() {
|
||
const res = await apiRequest('/api/auth/logout/', 'POST');
|
||
activeToken = '';
|
||
localStorage.removeItem('seller_central_access_token');
|
||
localStorage.removeItem('seller_central_active_user');
|
||
updateAuthUI();
|
||
showInspector('authResponseWrap', 'authStatusCode', 'authResponseBody', res);
|
||
showToast('Logged out. Cleared local token.', '🚪');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runVerifyOtp() {
|
||
const type = document.getElementById('otpType').value;
|
||
const otp = document.getElementById('otpCode').value;
|
||
|
||
const res = await apiRequest('/api/auth/verify-otp/', 'POST', { type, otp });
|
||
showInspector('authResponseWrap', 'authStatusCode', 'authResponseBody', res);
|
||
if (res.ok) {
|
||
showToast(`${type.toUpperCase()} OTP Verified!`, '✅');
|
||
fetchLogs();
|
||
}
|
||
}
|
||
|
||
async function runGetProfile() {
|
||
const res = await apiRequest('/api/profile/');
|
||
showInspector('profileResponseWrap', 'profileStatusCode', 'profileResponseBody', res);
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runUpdateProfile() {
|
||
const res = await apiRequest('/api/profile/', 'PUT', {
|
||
store_name: 'Artisan Decor & Handlooms Studio',
|
||
business_bio: 'Authentic handcrafted heritage products curated directly from local master artisans.',
|
||
street: '45 Craftsmen Boulevard',
|
||
city: 'Bangalore',
|
||
state: 'Karnataka',
|
||
pincode: '560001',
|
||
latitude: 12.9716,
|
||
longitude: 77.5946
|
||
});
|
||
showInspector('profileResponseWrap', 'profileStatusCode', 'profileResponseBody', res);
|
||
if (res.ok) showToast('Profile details updated!', '✨');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runSubmitGstin() {
|
||
const gstin = document.getElementById('profileGstin').value;
|
||
const res = await apiRequest('/api/profile/submit-gstin/', 'POST', { gstin });
|
||
showInspector('profileResponseWrap', 'profileStatusCode', 'profileResponseBody', res);
|
||
if (res.ok) showToast('GSTIN Verified successfully!', '🏛️');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runVerifyPan() {
|
||
const pan = document.getElementById('profilePan').value;
|
||
const res = await apiRequest('/api/profile/kyc/verify-pan/', 'POST', { pan_number: pan });
|
||
showInspector('profileResponseWrap', 'profileStatusCode', 'profileResponseBody', res);
|
||
if (res.ok) showToast('PAN verification completed!', '🆔');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runListProducts() {
|
||
const s = document.getElementById('prodSearch').value.trim();
|
||
const low = document.getElementById('prodLowStock').value;
|
||
const params = new URLSearchParams();
|
||
if (s) params.set('search', s);
|
||
if (low) params.set('low_stock', low);
|
||
|
||
const res = await apiRequest(`/api/products/?${params.toString()}`);
|
||
showInspector('productResponseWrap', 'productStatusCode', 'productResponseBody', res);
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runCreateSampleProduct() {
|
||
const rand = Math.floor(100 + Math.random() * 900);
|
||
const res = await apiRequest('/api/products/', 'POST', {
|
||
title: `Handcrafted Teak Bowl - Special Edition #${rand}`,
|
||
category: 'Home Decor',
|
||
price: 49.99,
|
||
stock: 25,
|
||
sku: `TEAK-BWL-${rand}`,
|
||
image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100'
|
||
});
|
||
showInspector('productResponseWrap', 'productStatusCode', 'productResponseBody', res);
|
||
if (res.ok) showToast('Product created successfully!', '📦');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runBulkUploadDemo() {
|
||
const demoProducts = [
|
||
{ title: 'Handmade Brass Lamp', category: 'Lighting', price: 65.00, stock: 12, sku: `BRASS-${Date.now()}-1` },
|
||
{ title: 'Pure Cotton Pashmina Shawl', category: 'Apparel', price: 110.00, stock: 8, sku: `PASH-${Date.now()}-2` }
|
||
];
|
||
const res = await apiRequest('/api/products/bulk-upload/', 'POST', { products: demoProducts });
|
||
showInspector('productResponseWrap', 'productStatusCode', 'productResponseBody', res);
|
||
if (res.ok) showToast('Bulk upload completed!', '🚀');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runListOrders() {
|
||
const res = await apiRequest('/api/orders/');
|
||
showInspector('orderResponseWrap', 'orderStatusCode', 'orderResponseBody', res);
|
||
if (res.ok && Array.isArray(res.data?.results || res.data) && (res.data?.results || res.data).length > 0) {
|
||
const firstOrder = (res.data?.results || res.data)[0];
|
||
document.getElementById('targetOrderId').value = firstOrder.id;
|
||
}
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runAcceptOrder() {
|
||
const id = document.getElementById('targetOrderId').value || '1';
|
||
const res = await apiRequest(`/api/orders/${id}/accept/`, 'POST');
|
||
showInspector('orderResponseWrap', 'orderStatusCode', 'orderResponseBody', res);
|
||
if (res.ok) showToast(`Order #${id} accepted & carrier assigned!`, '✅');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runRejectOrder() {
|
||
const id = document.getElementById('targetOrderId').value || '1';
|
||
const res = await apiRequest(`/api/orders/${id}/reject/`, 'POST');
|
||
showInspector('orderResponseWrap', 'orderStatusCode', 'orderResponseBody', res);
|
||
if (res.ok) showToast(`Order #${id} rejected!`, '❌');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runListReturns() {
|
||
const res = await apiRequest('/api/returns/');
|
||
showInspector('returnResponseWrap', 'returnStatusCode', 'returnResponseBody', res);
|
||
if (res.ok && Array.isArray(res.data?.results || res.data) && (res.data?.results || res.data).length > 0) {
|
||
const firstRet = (res.data?.results || res.data)[0];
|
||
document.getElementById('targetReturnId').value = firstRet.id;
|
||
}
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runReturnAction(action) {
|
||
const id = document.getElementById('targetReturnId').value || '1';
|
||
const res = await apiRequest(`/api/returns/${id}/action/`, 'POST', { action });
|
||
showInspector('returnResponseWrap', 'returnStatusCode', 'returnResponseBody', res);
|
||
if (res.ok) showToast(`Return #${id} set to ${action}!`, '🔄');
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runGetWallet() {
|
||
const res = await apiRequest('/api/wallet/');
|
||
showInspector('walletResponseWrap', 'walletStatusCode', 'walletResponseBody', res);
|
||
fetchLogs();
|
||
}
|
||
|
||
async function runWithdraw() {
|
||
const amount = document.getElementById('withdrawAmount').value;
|
||
const res = await apiRequest('/api/wallet/withdraw/', 'POST', { amount });
|
||
showInspector('walletResponseWrap', 'walletStatusCode', 'walletResponseBody', res);
|
||
if (res.ok) showToast(`Payout request of $${amount} submitted!`, '💸');
|
||
fetchLogs();
|
||
}
|
||
|
||
// =========================================================================
|
||
// API SANDBOX LOGIC
|
||
// =========================================================================
|
||
|
||
function loadSandboxPreset(method, url) {
|
||
document.getElementById('sandboxMethod').value = method;
|
||
document.getElementById('sandboxUrl').value = url;
|
||
}
|
||
|
||
async function sendSandboxRequest() {
|
||
const method = document.getElementById('sandboxMethod').value;
|
||
const url = document.getElementById('sandboxUrl').value.trim();
|
||
const bodyRaw = document.getElementById('sandboxBody').value.trim();
|
||
|
||
let body = null;
|
||
if (bodyRaw && method !== 'GET') {
|
||
try {
|
||
body = JSON.parse(bodyRaw);
|
||
} catch {
|
||
body = bodyRaw;
|
||
}
|
||
}
|
||
|
||
const res = await apiRequest(url, method, body);
|
||
const statusEl = document.getElementById('sandboxStatusCode');
|
||
const bodyEl = document.getElementById('sandboxResponseBody');
|
||
const latencyEl = document.getElementById('sandboxLatency');
|
||
|
||
statusEl.innerText = `${res.status} ${res.statusText}`;
|
||
statusEl.className = 'status-code ' + (res.ok ? 'status-2xx' : (res.status >= 500 ? 'status-5xx' : 'status-4xx'));
|
||
latencyEl.innerText = `${res.duration} ms`;
|
||
|
||
bodyEl.innerText = typeof res.data === 'object' ? JSON.stringify(res.data, null, 2) : String(res.data);
|
||
fetchLogs();
|
||
}
|
||
|
||
function copySandboxResponse() {
|
||
const bodyEl = document.getElementById('sandboxResponseBody');
|
||
navigator.clipboard.writeText(bodyEl.innerText);
|
||
showToast('Sandbox response copied!', '📋');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|