linh-hk's picture
fix socketIO connection and Log placeholder
046aaba
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mtDNA Location • Output</title>
<!-- Optional: keep the same favicon / fonts as your existing site -->
<link rel="icon" href="{{ url_for('static', filename='images/favicon.png') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='Output.css') }}" media="screen">
</head>
<body
data-is-vip="{{ 'true' if isvip else 'false' }}"
data-ws-url="{{ ws_url|default('') }}"
>
<!-- data-job-id="{{ job_id|default('') }}" -->
<div class="container">
<!-- Header -->
<header class="row">
<div>
<h1>mtDNA Location • Output</h1>
<div class="muted small">
Job ID: <span id="job-id">{{ job_id }}
</span> • Started: <span id="job-started-at">{{ started_at or '' }}
</span> • Permitted #run: <span id="job-total-queries">{{ total_queries or '' }}</span></div>
</div>
<div class="toolbar">
<span id="ws-status" class="badge connecting" aria-live="polite" role="status">
<span class="badge-dot" aria-hidden="true"></span>
<span class="badge-text">Connecting…</span>
</span>
<a class="btn btn-ghost" href="{{ url_for('home') }}">← Back</a>
</div>
</header>
<div class="spacer"></div>
<section class="row grid-2">
<!-- Left column: run status + logs -->
<div class="card">
<h2>Run status</h2>
<div class="kvs" aria-live="polite">
<div class="kv">
<div class="label">Overall status</div>
<div id="overall-status" class="value">{{ status or 'queued' }}</div>
</div>
<div class="kv">
<div class="label">Progress</div>
<div class="value"><span id="progress-value">0</span>%</div>
</div>
<div class="kv">
<div class="label">Processed</div>
<div class="value"><span id="processed-count">0</span>/<span id="total-count">0</span></div>
</div>
<div class="kv">
<div class="label">Tier</div>
<div class="value" id="user-tier">{{ 'Premium' if isvip else 'Freemium' }}</div>
</div>
</div>
<div class="spacer"></div>
<div class="progress" aria-hidden="true"><span id="progress-bar" style="width:0%"></span></div>
<div class="spacer"></div>
<div class="toolbar">
<!-- Wire these later to your WebSocket/SSE/REST actions -->
<button id="btn-cancel" class="btn btn-danger" data-action="cancel">Cancel job</button>
<button id="btn-pause" class="btn" data-action="pause">Pause</button>
<button id="btn-resume" class="btn" data-action="resume">Resume</button>
</div>
<div class="spacer"></div>
<h3>Live log</h3>
<pre id="live-log" class="log mono" aria-live="polite" aria-atomic="false">
<span id="log-placeholder" class="logline muted">Waiting for messages…</span>
</pre>
</div>
<!-- Right column: summary + downloads + alerts -->
<div class="card">
<h2>Summary</h2>
<ul class="muted summary">
<li>Accession IDs received: <strong id="sum-accessions">0</strong></li>
<li>Unique isolates resolved: <strong id="sum-isolates">0</strong></li>
<li>Errors / warnings: <strong id="sum-errors">0</strong></li>
</ul>
<div id="alerts"></div>
<div class="spacer"></div>
<h3>Downloads</h3>
<div class="row download">
<a id="dl-csv" class="btn" href="#" download>Results (.csv)</a>
<a id="dl-json" class="btn" href="#" download>Results (.json)</a>
</div>
<div class="spacer"></div>
<h3>Share with</h3>
<div class="row share">
<input id="share-link" class="mono" value="" readonly />
<button id="copy-share" class="btn">Send</button>
</div>
</div>
</section>
<div class="spacer"></div>
<!-- Results table -->
<section class="card">
<h2>Results</h2>
<div class="muted small" id="results-help">Rows will stream in as they complete.</div>
<div class="spacer"></div>
<div style="overflow:auto;">
<table id="results-table" style="table-layout:fixed;width:100%" aria-describedby="results-help">
<colgroup>
<col width="3%">
<col width="8%">
<col width="26%">
<col width="8%">
<col width="25%">
<col width="8%">
<col width="15%">
<col width="7%">
</colgroup>
<thead>
<tr>
<th scope="col" class="col-idx">#</th>
<th scope="col" class="col-id">Sample ID</th>
<th scope="col" class="col-country">Sources</th>
<th scope="col" class="col-sampletype">Predicted Country</th>
<th scope="col" class="col-countryex">Country Explanation</th>
<th scope="col" class="col-sampleex">Predicted Type</th>
<th scope="col" class="col-sources">Type Explanation</th>
<th scope="col" class="col-time">Time</th>
</tr>
</thead>
<tbody id="results-body">
<tr id="results-placeholder">
<td colspan="8" class="muted">No results yet.</td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="spacer"></div>
<!-- Visuals / future widgets area -->
<section class="row visualisation">
<div class="card">
<h3>Geography preview</h3>
<div id="map-mount">Map placeholder</div>
</div>
<div class="card">
<h3>Progress over time</h3>
<div id="chart-mount">Chart placeholder</div>
</div>
</section>
<div class="spacer"></div>
<footer class="muted small">
<span id="tier-text">{{ 'We love you ♥ Premium' if isvip else 'Freemium' }}</span> • mtDNA Location
</footer>
</div>
<!-- TEMPLATES (for client-side rendering) -->
<template id="tpl-log-line"><span class="logline"></span>\n</template>
<template id="tpl-result-row">
<tr>
<td class="mono"></td>
<td></td>
<td class="mono small muted"></td>
<td></td>
<td class="small"></td>
</tr>
</template>
<!-- Load a v3 client that matches Flask-SocketIO (python-socketio 5.x) -->
<script src="https://cdn.socket.io/3.1.3/socket.io.min.js"></script>
<script>
(function () {
// ---- your existing JS (kept) ----
const els = {
wsStatus: document.getElementById('ws-status'),
wsBadgeText: document.querySelector('#ws-status .badge-text'),
progressBar: document.getElementById('progress-bar'),
progressValue: document.getElementById('progress-value'),
processed: document.getElementById('processed-count'),
total: document.getElementById('total-count'),
overallStatus: document.getElementById('overall-status'),
log: document.getElementById('live-log'),
tbody: document.getElementById('results-body'),
placeholder: document.getElementById('results-placeholder'),
dlCSV: document.getElementById('dl-csv'),
dlJSON: document.getElementById('dl-json'),
};
const jobId = "{{ job_id }}"; // provided by Flask template (✓)
function setConnection(state, text) {
els.wsStatus?.classList.remove('connecting','connected','disconnected');
els.wsStatus?.classList.add(state);
if (els.wsBadgeText) els.wsBadgeText.textContent = text;
}
function appendLog(line) {
if (!els.log) return;
const ph = document.getElementById('log-placeholder');
if (ph) ph.remove(); // drop the placeholder once
const span = document.createElement('span');
span.className = 'logline';
span.textContent = line;
els.log.appendChild(span);
els.log.appendChild(document.createTextNode('\n'));
els.log.scrollTop = els.log.scrollHeight;
}
function appendRow(r) {
if (els.placeholder) { els.placeholder.remove(); els.placeholder = null; }
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="mono">${r.idx ?? ''}</td>
<td>${r.sample_id ?? ''}</td>
<td class="mono small muted">${r.sources ?? ''}</td>
<td>${r.predicted_country ?? ''}</td>
<td class="small">${r.country_explanation ?? ''}</td>
<td>${r.predicted_sample_type ?? ''}</td>
<td class="small">${r.sample_type_explanation ?? ''}</td>
<td>${(String(r.time_cost ?? r['Time cost'] ?? '').match(/\d+(?:\.\d+)?/) || [''])[0]}</td>`;
els.tbody?.appendChild(tr);
// increment processed + progress % (your existing logic)
if (els.processed && els.total) {
const done = (parseInt(els.processed.textContent || '0', 10) || 0) + 1;
els.processed.textContent = String(done);
const total = parseInt(els.total.textContent || '0', 10) || 0;
if (total > 0) {
const pct = Math.min(100, Math.round((done / total) * 100));
els.progressValue.textContent = pct;
els.progressBar.style.width = pct + '%';
}
}
const sumIso = document.getElementById('sum-isolates');
if (sumIso) sumIso.textContent = String((parseInt(sumIso.textContent || '0', 10) || 0) + 1);
}
setConnection('connecting', 'Connecting…');
// Create the socket (v3 client)
// const socket = io({ transports: ['websocket', 'polling'], reconnectionAttempts: 5, timeout: 10000 });
// ✅ replace WITH THIS:
const qs = window.location.search; // includes ?__sign=...
const endpoint = qs ? ('/?' + qs.slice(1)) : '/'; // build "/?__sign=..."
const socket = io(endpoint, {
path: '/socket.io',
transports: ['websocket','polling'], // allow fallback, then upgrade
withCredentials: true,
reconnectionAttempts: 5,
timeout: 10000
});
socket.on('connect', () => {
setConnection('connected', 'Connected');
appendLog('[WS] connected');
socket.emit('join', { job_id: jobId }); // your page already emits join (✓)
});
socket.on('connect_error', (err) => appendLog('[WS] connect_error: ' + (err?.message || err)));
socket.on('disconnect', (reason) => {
setConnection('disconnected', 'Disconnected');
appendLog('[WS] disconnected: ' + reason);
});
socket.on('status', (data) => {
if (els.overallStatus) els.overallStatus.textContent = data.state || 'unknown';
if (data.total != null) {
if (els.total) els.total.textContent = data.total;
const sumAcc = document.getElementById('sum-accessions');
if (sumAcc) sumAcc.textContent = data.total; // update “Accession IDs received” counter
}
appendLog(`[STATUS] ${data.state}${data.elapsed ? ' in ' + data.elapsed : ''}`);
});
socket.on('log', (data) => appendLog(`[LOG] ${data.msg}`));
socket.on('row', appendRow);
window.addEventListener('beforeunload', () => {
socket.emit('leave', { job_id: jobId });
});
})();
document.addEventListener('click', function (e) {
const a = e.target.closest('a[href]');
if (!a) return;
const href = a.getAttribute('href');
// Only adjust internal links (not http/https or mailto/etc.)
if (!href || /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith('#')) return;
// Preserve the current ?__sign=... and any other params
const qs = window.location.search; // includes "?__sign=..."
// If the link already has a query, merge them
const joined = href + (href.includes('?') ? '&' + qs.slice(1) : qs);
e.preventDefault();
window.location.href = joined;
});
</script>
</body>
</html>