[31188f42] einfügen
einfügen
This commit is contained in:
@@ -56,6 +56,10 @@ def stats():
|
||||
def get_jobs():
|
||||
return queue.get_recent_jobs(limit=100)
|
||||
|
||||
@app.get("/api/accounts")
|
||||
def get_accounts():
|
||||
return queue.get_account_summary(limit=500)
|
||||
|
||||
@app.get("/dashboard", response_class=HTMLResponse)
|
||||
def dashboard():
|
||||
html_content = """
|
||||
@@ -63,70 +67,188 @@ def dashboard():
|
||||
<html>
|
||||
<head>
|
||||
<title>Connector Dashboard</title>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<meta http-equiv="refresh" content="30">
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; background: #f0f2f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #ddd; font-size: 14px; }
|
||||
th { background-color: #f8f9fa; color: #666; font-weight: 600; }
|
||||
tr:hover { background-color: #f8f9fa; }
|
||||
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
||||
.status-PENDING { background: #e2e8f0; color: #475569; }
|
||||
.status-PROCESSING { background: #dbeafe; color: #1e40af; }
|
||||
.status-COMPLETED { background: #dcfce7; color: #166534; }
|
||||
.status-FAILED { background: #fee2e2; color: #991b1b; }
|
||||
.status-RETRY { background: #fef9c3; color: #854d0e; }
|
||||
.meta { color: #888; font-size: 12px; }
|
||||
pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #444; font-family: monospace; font-size: 11px; max-height: 60px; overflow-y: auto; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #0f172a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: #1e293b;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
h1 { margin: 0; font-size: 24px; color: #f8fafc; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid #334155; padding-bottom: 10px; }
|
||||
.tab { padding: 8px 16px; cursor: pointer; border-radius: 6px; font-weight: 500; font-size: 14px; color: #94a3b8; transition: all 0.2s; }
|
||||
.tab:hover { background: #334155; color: #f8fafc; }
|
||||
.tab.active { background: #3b82f6; color: white; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 14px; border-bottom: 1px solid #334155; font-size: 14px; }
|
||||
th { background-color: #1e293b; color: #94a3b8; font-weight: 600; text-transform: uppercase; font-size: 12px; letter-spacing: 0.5px; }
|
||||
tr:hover { background-color: #334155; }
|
||||
|
||||
.status { padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; }
|
||||
.status-PENDING { background: #334155; color: #cbd5e1; }
|
||||
.status-PROCESSING { background: #1e40af; color: #bfdbfe; }
|
||||
.status-COMPLETED { background: #064e3b; color: #a7f3d0; }
|
||||
.status-FAILED { background: #7f1d1d; color: #fecaca; }
|
||||
|
||||
.phases { display: flex; gap: 4px; align-items: center; }
|
||||
.phase { width: 12px; height: 12px; border-radius: 50%; background: #334155; border: 2px solid #1e293b; box-shadow: 0 0 0 1px #334155; }
|
||||
.phase.completed { background: #10b981; box-shadow: 0 0 0 1px #10b981; }
|
||||
.phase.processing { background: #f59e0b; box-shadow: 0 0 0 1px #f59e0b; animation: pulse 1.5s infinite; }
|
||||
.phase.failed { background: #ef4444; box-shadow: 0 0 0 1px #ef4444; }
|
||||
|
||||
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
|
||||
|
||||
.meta { color: #94a3b8; font-size: 12px; display: block; margin-top: 4px; }
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #cbd5e1;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 11px;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
background: #0f172a;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<header>
|
||||
<h1>🔌 SuperOffice Connector Dashboard</h1>
|
||||
<div id="stats"></div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" id="tab-accounts" onclick="switchTab('accounts')">Account View</div>
|
||||
<div class="tab" id="tab-events" onclick="switchTab('events')">Event Log</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">ID</th>
|
||||
<th width="120">Status</th>
|
||||
<th width="150">Updated</th>
|
||||
<th width="150">Event</th>
|
||||
<th>Payload / Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="job-table">
|
||||
<tr><td colspan="5" style="text-align:center;">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="view-accounts">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account / Person</th>
|
||||
<th width="120">ID</th>
|
||||
<th width="150">Process Progress</th>
|
||||
<th width="100">Duration</th>
|
||||
<th width="120">Status</th>
|
||||
<th width="150">Last Update</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="account-table">
|
||||
<tr><td colspan="6" style="text-align:center;">Loading Accounts...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="view-events" class="hidden">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">ID</th>
|
||||
<th width="120">Status</th>
|
||||
<th width="150">Updated</th>
|
||||
<th width="150">Event</th>
|
||||
<th>Payload / Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="event-table">
|
||||
<tr><td colspan="5" style="text-align:center;">Loading Events...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentTab = 'accounts';
|
||||
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
document.getElementById('tab-accounts').classList.toggle('active', tab === 'accounts');
|
||||
document.getElementById('tab-events').classList.toggle('active', tab === 'events');
|
||||
document.getElementById('view-accounts').classList.toggle('hidden', tab !== 'accounts');
|
||||
document.getElementById('view-events').classList.toggle('hidden', tab !== 'events');
|
||||
loadData();
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (currentTab === 'accounts') await loadAccounts();
|
||||
else await loadEvents();
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
// Use relative path to work behind Nginx /connector/ prefix
|
||||
const response = await fetch('api/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
const tbody = document.getElementById('job-table');
|
||||
const response = await fetch('api/accounts');
|
||||
const accounts = await response.json();
|
||||
const tbody = document.getElementById('account-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (jobs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;">No jobs found</td></tr>';
|
||||
if (accounts.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;">No accounts in process</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
accounts.sort((a,b) => new Date(b.updated_at) - new Date(a.updated_at));
|
||||
|
||||
accounts.forEach(acc => {
|
||||
const tr = document.createElement('tr');
|
||||
const phasesHtml = `
|
||||
<div class="phases">
|
||||
<div class="phase ${acc.phases.received}" title="Received"></div>
|
||||
<div class="phase ${acc.phases.enriching}" title="Enriching (CE)"></div>
|
||||
<div class="phase ${acc.phases.syncing}" title="Syncing (SO)"></div>
|
||||
<div class="phase ${acc.phases.completed}" title="Completed"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<strong>${acc.name}</strong>
|
||||
<span class="meta">${acc.last_event}</span>
|
||||
</td>
|
||||
<td>${acc.id}</td>
|
||||
<td>${phasesHtml}</td>
|
||||
<td><span class="meta">${acc.duration || '0s'}</span></td>
|
||||
<td><span class="status status-${acc.status}">${acc.status}</span></td>
|
||||
<td>${new Date(acc.updated_at + "Z").toLocaleTimeString()}</td>
|
||||
<td><pre>${acc.error_msg || 'No issues'}</pre></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (e) { console.error("Failed to load accounts", e); }
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
try {
|
||||
const response = await fetch('api/jobs');
|
||||
const jobs = await response.json();
|
||||
const tbody = document.getElementById('event-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
jobs.forEach(job => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
let details = JSON.stringify(job.payload, null, 2);
|
||||
if (job.error_msg) {
|
||||
details += "\\n\\n🔴 ERROR: " + job.error_msg;
|
||||
}
|
||||
if (job.error_msg) details += "\\n\\n🔴 ERROR: " + job.error_msg;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>#${job.id}</td>
|
||||
@@ -137,13 +259,11 @@ def dashboard():
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to load jobs", e);
|
||||
}
|
||||
} catch (e) { console.error("Failed to load events", e); }
|
||||
}
|
||||
|
||||
loadData();
|
||||
// Also handled by meta refresh, but JS refresh is smoother if we want to remove meta refresh
|
||||
setInterval(loadData, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user