The dashboard actually.
This commit is contained in:
1 parent
d9a2e6a923
commit
911745fda6
2 files changed
+557
No files matched your search
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lightweight test server and transparent proxy for the Records API.
|
||||
Serves index.html on http://127.0.0.1:8080 and adds CORS headers to Axum requests.
|
||||
"""
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
PORT = 8080
|
||||
|
||||
class ProxyHandler(SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', '*')
|
||||
super().end_headers()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"🚀 Dashboard running at http://127.0.0.1:{PORT}")
|
||||
print("Press Ctrl+C to stop.")
|
||||
server = HTTPServer(('127.0.0.1', PORT), ProxyHandler)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping...")
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Records API & Replication Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--card-bg: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #c9d1d9;
|
||||
--text-muted: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--success: #3fb950;
|
||||
--warning: #d29922;
|
||||
--danger: #f85149;
|
||||
--primary-color: #79c0ff;
|
||||
--replica-color: #bc8cff;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
padding: 20px;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
h1 { font-size: 1.4rem; color: #fff; display: flex; align-items: center; gap: 10px; }
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge-primary { background: rgba(121, 192, 255, 0.15); color: var(--primary-color); }
|
||||
.badge-replica { background: rgba(188, 140, 255, 0.15); color: var(--replica-color); }
|
||||
.badge-ok { background: rgba(63, 185, 80, 0.2); color: var(--success); }
|
||||
.badge-warn { background: rgba(210, 153, 34, 0.2); color: var(--warning); }
|
||||
.badge-err { background: rgba(248, 81, 73, 0.2); color: var(--danger); }
|
||||
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
|
||||
@media (max-width: 900px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Topology Bar */
|
||||
.topology {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 16px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.node-box {
|
||||
text-align: center;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
min-width: 180px;
|
||||
}
|
||||
.pipe {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.arrow { font-size: 1.5rem; color: var(--accent); letter-spacing: 2px; }
|
||||
|
||||
/* Inputs & Buttons */
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
background: #0d1117;
|
||||
border: 1px solid var(--border);
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
button {
|
||||
background: #21262d;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
button:hover { background: #30363d; border-color: #8b949e; color: #fff; }
|
||||
button.btn-primary { background: #238636; border-color: #2ea043; color: #fff; }
|
||||
button.btn-primary:hover { background: #2ea043; }
|
||||
button.btn-test { background: #1f6feb; border-color: #388bfd; color: #fff; }
|
||||
button.btn-test:hover { background: #388bfd; }
|
||||
|
||||
/* Tables */
|
||||
.table-container { max-height: 380px; overflow-y: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
||||
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
||||
th { position: sticky; top: 0; background: var(--card-bg); color: var(--text-muted); font-size: 0.75rem; }
|
||||
tr.synced { background: rgba(63, 185, 80, 0.05); }
|
||||
tr.lagging { background: rgba(248, 81, 73, 0.1); }
|
||||
|
||||
/* Logs */
|
||||
.log-box {
|
||||
height: 180px;
|
||||
overflow-y: auto;
|
||||
background: #090d13;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 0.78rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
.log-line { margin-bottom: 4px; border-bottom: 1px dotted rgba(255,255,255,0.05); padding-bottom: 2px; }
|
||||
.log-info { color: var(--accent); }
|
||||
.log-pass { color: var(--success); }
|
||||
.log-fail { color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1>⚡ Records API & Replication Monitor</h1>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 4px;">
|
||||
Interactive validation and real-time visualization of SQLite append-only journals
|
||||
</p>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap: 10px;">
|
||||
<label style="font-size:0.8rem; display:flex; align-items:center; gap:6px; cursor:pointer;">
|
||||
<input type="checkbox" id="autoPoll" checked style="width:auto; margin:0;" /> Live Auto-Poll (1.5s)
|
||||
</label>
|
||||
<button onclick="fetchAll()">🔄 Refresh Now</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Node Addresses & Endpoints Setup -->
|
||||
<div class="card" style="padding: 10px 16px;">
|
||||
<div style="display:flex; gap: 20px; align-items:center; flex-wrap:wrap; font-size:0.85rem;">
|
||||
<div style="flex:1; min-width:240px;">
|
||||
<label>Primary Base URL:</label>
|
||||
<input type="text" id="primaryUrl" value="http://127.0.0.1:3000" style="margin-bottom:0;" />
|
||||
</div>
|
||||
<div style="flex:1; min-width:240px;">
|
||||
<label>Replica Base URL:</label>
|
||||
<input type="text" id="replicaUrl" value="http://127.0.0.1:3001" style="margin-bottom:0;" />
|
||||
</div>
|
||||
<div style="flex:1; min-width:180px;">
|
||||
<label>Replication Bearer Token:</label>
|
||||
<input type="password" id="replToken" value="change-me" style="margin-bottom:0;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Replication Pipeline Banner -->
|
||||
<div class="topology">
|
||||
<div class="node-box" id="nodePrimaryBox">
|
||||
<span class="badge badge-primary">Primary Node</span>
|
||||
<div style="font-size: 1.2rem; font-weight: bold; margin-top: 5px;" id="primaryHead">Seq: --</div>
|
||||
<div style="font-size: 0.72rem; color: var(--text-muted);" id="primaryId">ID: --</div>
|
||||
</div>
|
||||
|
||||
<div class="pipe">
|
||||
<span id="pipeState" class="badge badge-ok">Connected</span>
|
||||
<div class="arrow">━━━━ ◄─pull── ━━━━</div>
|
||||
<div id="pipeDetails">Lag: <span id="lagRecords">0</span> records</div>
|
||||
</div>
|
||||
|
||||
<div class="node-box" id="nodeReplicaBox">
|
||||
<span class="badge badge-replica">Replica Node</span>
|
||||
<div style="font-size: 1.2rem; font-weight: bold; margin-top: 5px;" id="replicaHead">Seq: --</div>
|
||||
<div style="font-size: 0.72rem; color: var(--text-muted);" id="replicaId">ID: --</div>
|
||||
<div style="font-size: 0.72rem; color: var(--text-muted);" id="replicaSyncState">State: --</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Side-by-Side Journal Tables -->
|
||||
<div class="grid-2">
|
||||
<!-- Primary Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>📖 Primary Records (`/api/records`)</span>
|
||||
<span id="primaryCount" class="badge badge-primary">0 records</span>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table id="primaryTable">
|
||||
<thead><tr><th>Seq</th><th>UUID</th><th>Date (UTC)</th><th>Payload</th></tr></thead>
|
||||
<tbody><tr><td colspan="4" style="text-align:center; color:var(--text-muted)">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replica Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>📖 Replica Records (`/api/records`)</span>
|
||||
<span id="replicaCount" class="badge badge-replica">0 records</span>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table id="replicaTable">
|
||||
<thead><tr><th>Seq</th><th>UUID</th><th>Date (UTC)</th><th>Payload</th></tr></thead>
|
||||
<tbody><tr><td colspan="4" style="text-align:center; color:var(--text-muted)">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action & Validation Panel -->
|
||||
<div class="grid-3">
|
||||
<!-- Insert Record Form -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span>✍️ Write Record (Primary)</span></div>
|
||||
<label style="font-size:0.8rem;">JSON Payload (`data` field):</label>
|
||||
<textarea id="recordData" rows="4">{
|
||||
"version": 1,
|
||||
"message": "Testing replication!"
|
||||
}</textarea>
|
||||
<button class="btn-primary" style="width:100%;" onclick="submitRecord()">Send to Primary (POST /api/records)</button>
|
||||
</div>
|
||||
|
||||
<!-- Automated Validation Tests -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span>🧪 Automated Tests</span></div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||
<button class="btn-test" onclick="runReplicaWriteGuardTest()">🛡️ Test Replica Read-Only Guard (Expect 403)</button>
|
||||
<button class="btn-test" onclick="runSchemaValidationTest()">⚠️ Test Invalid Schema Payload (Expect 400)</button>
|
||||
<button class="btn-test" onclick="runSyncLatencyTest()">⏱️ Test Insert-to-Replica Sync Latency</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Schema & Admin -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>⚙️ Schema Management</span>
|
||||
<button onclick="fetchSchema()" style="padding:2px 8px; font-size:0.75rem;">Inspect</button>
|
||||
</div>
|
||||
<pre id="schemaView" style="font-size:0.75rem; background:#0d1117; padding:8px; border-radius:4px; max-height:100px; overflow:auto;">Click Inspect to view</pre>
|
||||
<button onclick="reloadSchema()" style="margin-top:8px; width:100%;">Trigger Hot-Reload (`/reload-config`)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Console Log -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>🖥️ Live Verification Console</span>
|
||||
<button onclick="document.getElementById('consoleLog').innerHTML = '';" style="font-size:0.75rem;">Clear</button>
|
||||
</div>
|
||||
<div class="log-box" id="consoleLog"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let primaryHeadSeq = 0;
|
||||
let replicaHeadSeq = 0;
|
||||
|
||||
function log(msg, type = 'info') {
|
||||
const box = document.getElementById('consoleLog');
|
||||
const time = new Date().toISOString().split('T')[1].replace('Z','');
|
||||
const div = document.createElement('div');
|
||||
div.className = `log-line log-${type}`;
|
||||
div.innerText = `[${time}] ${msg}`;
|
||||
box.prepend(div);
|
||||
}
|
||||
|
||||
function getUrls() {
|
||||
return {
|
||||
primary: document.getElementById('primaryUrl').value.replace(/\/+$/, ''),
|
||||
replica: document.getElementById('replicaUrl').value.replace(/\/+$/, ''),
|
||||
token: document.getElementById('replToken').value
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAll() {
|
||||
const { primary, replica } = getUrls();
|
||||
|
||||
// 1. Fetch Primary Records
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/records`);
|
||||
if (res.ok) {
|
||||
const records = await res.json();
|
||||
renderTable('primaryTable', records);
|
||||
document.getElementById('primaryCount').innerText = `${records.length} records`;
|
||||
primaryHeadSeq = records.length > 0 ? records[0].seq : 0;
|
||||
document.getElementById('primaryHead').innerText = `Seq: ${primaryHeadSeq}`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('primaryHead').innerText = `Offline`;
|
||||
}
|
||||
|
||||
// 2. Fetch Replica Replication Status
|
||||
try {
|
||||
const res = await fetch(`${replica}/api/replication/status`);
|
||||
if (res.ok) {
|
||||
const status = await res.json();
|
||||
replicaHeadSeq = status.local_head_seq ?? 0;
|
||||
document.getElementById('replicaHead').innerText = `Seq: ${replicaHeadSeq}`;
|
||||
document.getElementById('replicaId').innerText = `ID: ${status.instance_id.slice(0, 8)}...`;
|
||||
document.getElementById('primaryId').innerText = `Source: ${(status.sync?.source_instance_id || 'unlinked').slice(0, 8)}...`;
|
||||
|
||||
const state = status.sync?.state || 'unknown';
|
||||
const badge = document.getElementById('pipeState');
|
||||
badge.innerText = state.toUpperCase();
|
||||
badge.className = `badge ${state === 'idle' ? 'badge-ok' : state === 'syncing' ? 'badge-warn' : 'badge-err'}`;
|
||||
|
||||
const lag = status.lag_records ?? Math.max(0, primaryHeadSeq - replicaHeadSeq);
|
||||
document.getElementById('lagRecords').innerText = lag;
|
||||
document.getElementById('replicaSyncState').innerText = `State: ${state}`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('replicaHead').innerText = `Offline`;
|
||||
document.getElementById('pipeState').innerText = 'DISCONNECTED';
|
||||
document.getElementById('pipeState').className = 'badge badge-err';
|
||||
}
|
||||
|
||||
// 3. Fetch Replica Records
|
||||
try {
|
||||
const res = await fetch(`${replica}/api/records`);
|
||||
if (res.ok) {
|
||||
const records = await res.json();
|
||||
renderTable('replicaTable', records);
|
||||
document.getElementById('replicaCount').innerText = `${records.length} records`;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function renderTable(tableId, records) {
|
||||
const tbody = document.querySelector(`#${tableId} tbody`);
|
||||
if (!records || records.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="4" style="text-align:center; color:var(--text-muted)">Database is empty</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = records.map(r => `
|
||||
<tr class="${r.seq === primaryHeadSeq ? 'synced' : ''}">
|
||||
<td><strong>#${r.seq}</strong></td>
|
||||
<td style="font-family:monospace; font-size:0.75rem;" title="${r.uuid}">${r.uuid.slice(0, 8)}…</td>
|
||||
<td style="font-size:0.75rem;">${r.date.replace('T', ' ').replace('Z', '')}</td>
|
||||
<td><code style="font-size:0.75rem;">${JSON.stringify(r.data)}</code></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function submitRecord() {
|
||||
const { primary } = getUrls();
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(document.getElementById('recordData').value);
|
||||
} catch (err) {
|
||||
log(`Invalid JSON in editor: ${err.message}`, 'fail');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: payload })
|
||||
});
|
||||
const text = await res.text();
|
||||
if (res.status === 201) {
|
||||
const data = JSON.parse(text);
|
||||
log(`Write successful on Primary! Created seq #${data.seq} (UUID: ${data.uuid.slice(0, 8)})`, 'pass');
|
||||
fetchAll();
|
||||
} else {
|
||||
log(`Primary rejected write [HTTP ${res.status}]: ${text}`, 'fail');
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Network failure writing to Primary: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
// TEST 1: Replica read-only safety guard
|
||||
async function runReplicaWriteGuardTest() {
|
||||
const { replica } = getUrls();
|
||||
log(`Starting Replica Write Guard test against ${replica}/api/records...`, 'info');
|
||||
try {
|
||||
const res = await fetch(`${replica}/api/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: { message: "Direct write forbidden test" } })
|
||||
});
|
||||
if (res.status === 403) {
|
||||
const msg = await res.text();
|
||||
log(`PASS: Replica blocked write with HTTP 403 Forbidden! Response: "${msg}"`, 'pass');
|
||||
} else {
|
||||
log(`FAIL: Replica answered with HTTP ${res.status} instead of expected 403!`, 'fail');
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Test failed due to network error: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
// TEST 2: Schema validation test
|
||||
async function runSchemaValidationTest() {
|
||||
const { primary } = getUrls();
|
||||
log(`Starting Schema Rejection test on Primary (sending wrong types & extra fields)...`, 'info');
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: { unknown_xyz_field: 999999, version: "not-an-integer" } })
|
||||
});
|
||||
const msg = await res.text();
|
||||
if (res.status === 400) {
|
||||
log(`PASS: Schema validation rejected bad payload [HTTP 400]: "${msg}"`, 'pass');
|
||||
} else {
|
||||
log(`FAIL: Expected HTTP 400, got ${res.status}: "${msg}"`, 'fail');
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Test failed: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
// TEST 3: Sync Latency propagation benchmark
|
||||
async function runSyncLatencyTest() {
|
||||
const { primary, replica } = getUrls();
|
||||
const testMsg = `probe-${Date.now()}`;
|
||||
log(`Triggering probe write on Primary with marker: ${testMsg}...`, 'info');
|
||||
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: { message: testMsg, msg: testMsg, version: 1 } })
|
||||
});
|
||||
if (!res.ok) throw new Error(`Primary write failed with HTTP ${res.status}`);
|
||||
const created = await res.json();
|
||||
const targetSeq = created.seq;
|
||||
log(`Record created with seq #${targetSeq}. Waiting for Replica sync poll...`, 'info');
|
||||
|
||||
let attempts = 0;
|
||||
const interval = setInterval(async () => {
|
||||
attempts++;
|
||||
try {
|
||||
const statusRes = await fetch(`${replica}/api/replication/status`);
|
||||
const status = await statusRes.json();
|
||||
if (status.local_head_seq >= targetSeq) {
|
||||
clearInterval(interval);
|
||||
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
|
||||
log(`PASS: Record #${targetSeq} successfully replicated in ${elapsed}s (${attempts} checks)!`, 'pass');
|
||||
fetchAll();
|
||||
} else if (attempts >= 40) {
|
||||
clearInterval(interval);
|
||||
log(`FAIL: Replica did not reach seq #${targetSeq} within 20 seconds.`, 'fail');
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(interval);
|
||||
log(`Replication polling error: ${e.message}`, 'fail');
|
||||
}
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
log(`Probe failed: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSchema() {
|
||||
const { primary } = getUrls();
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/admin/config`);
|
||||
const data = await res.json();
|
||||
document.getElementById('schemaView').innerText = JSON.stringify(data, null, 2);
|
||||
log(`Fetched active configuration schema.`, 'info');
|
||||
} catch (e) {
|
||||
log(`Failed to fetch schema: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadSchema() {
|
||||
const { primary } = getUrls();
|
||||
try {
|
||||
const res = await fetch(`${primary}/api/admin/reload-config`, { method: 'POST' });
|
||||
const text = await res.text();
|
||||
log(`Reload schema result [HTTP ${res.status}]: ${text}`, res.ok ? 'pass' : 'fail');
|
||||
fetchSchema();
|
||||
} catch (e) {
|
||||
log(`Reload failed: ${e.message}`, 'fail');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-polling cycle
|
||||
setInterval(() => {
|
||||
if (document.getElementById('autoPoll').checked) {
|
||||
fetchAll();
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
// Initial load
|
||||
fetchAll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user