Skip to content

Commit b300b12

Browse files
Fixed backend file reading errors
1 parent 819f9dd commit b300b12

4 files changed

Lines changed: 374 additions & 71 deletions

File tree

src/libunicorn/graphengine/graphengine.js

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const state = {
55
counts: new Map(),
66
names: new Map(),
77
series: [],
8+
clientLog: [],
89
remote: {
910
user: "",
1011
host: "",
@@ -49,6 +50,8 @@ const els = {
4950
optionsSectionBody: document.getElementById("optionsSectionBody"),
5051
toggleReportsSection: document.getElementById("toggleReportsSection"),
5152
reportsSectionBody: document.getElementById("reportsSectionBody"),
53+
toggleLogSection: document.getElementById("toggleLogSection"),
54+
logSectionBody: document.getElementById("logSectionBody"),
5255
uncollapseBtn: document.getElementById("uncollapseBtn"),
5356
uncollapseTipsBtn: document.getElementById("uncollapseTipsBtn"),
5457
subtreeReportBtn: document.getElementById("subtreeReportBtn"),
@@ -105,6 +108,9 @@ const els = {
105108
sourceLegend: document.getElementById("sourceLegend"),
106109
topTableWrap: document.getElementById("topTableWrap"),
107110
topTable: document.getElementById("topTable"),
111+
clientLogMeta: document.getElementById("clientLogMeta"),
112+
clientLog: document.getElementById("clientLog"),
113+
clearClientLogBtn: document.getElementById("clearClientLogBtn"),
108114
};
109115

110116
els.renderBtn.addEventListener("click", loadAndRender);
@@ -156,6 +162,9 @@ els.clearSelectionBtn.addEventListener("click", clearSelection);
156162
if (els.exportMatrixBtn) {
157163
els.exportMatrixBtn.addEventListener("click", exportCurrentSubtreeMatrix);
158164
}
165+
if (els.clearClientLogBtn) {
166+
els.clearClientLogBtn.addEventListener("click", clearClientLog);
167+
}
159168
initControlsResize();
160169
initSummaryResize();
161170
initTablePanel();
@@ -190,6 +199,7 @@ function initRemotePanel() {
190199
updateConnectionState(false, "Not connected");
191200
renderRemoteDatasets();
192201
updateRenderAvailability();
202+
renderClientLog();
193203
}
194204

195205
function initFileInputs() {
@@ -340,6 +350,7 @@ function initSidebarPanel() {
340350
initSectionToggle("filesSection", els.toggleFilesSection, els.filesSectionBody);
341351
initSectionToggle("optionsSection", els.toggleOptionsSection, els.optionsSectionBody);
342352
initSectionToggle("reportsSection", els.toggleReportsSection, els.reportsSectionBody);
353+
initSectionToggle("logSection", els.toggleLogSection, els.logSectionBody);
343354
}
344355

345356
function initSectionToggle(key, button, body) {
@@ -396,6 +407,7 @@ async function connectRemote() {
396407

397408
updateConnectionState(false, "Connecting...");
398409
setStatus(`Testing local tunnel endpoint for ${user}@${host}...`);
410+
addClientLog("info", "tunnel", `Testing tunnel endpoint for ${user}@${host}.`, "GET http://localhost:8000/ping");
399411
try {
400412
const controller = new AbortController();
401413
const timeoutId = setTimeout(() => controller.abort(), 2500);
@@ -410,10 +422,17 @@ async function connectRemote() {
410422
const ping = await response.json();
411423
updateRemoteServerStatus(ping);
412424
updateConnectionState(true, "Connected");
425+
addClientLog(
426+
"success",
427+
"tunnel",
428+
`Tunnel check succeeded for ${user}@${host}.`,
429+
`backend taxonomy: nodes=${ping?.taxonomy?.nodes_file || "missing"}, names=${ping?.taxonomy?.names_file || "missing"}`,
430+
);
413431
try {
414432
await refreshRemoteDatasets({ selectAll: true });
415433
setStatus(remoteConnectionReadyMessage(user, host));
416434
} catch (error) {
435+
addClientLog("error", "datasets", `Remote dataset refresh failed after tunnel check.`, errorToDetail(error));
417436
setStatus(`Tunnel check succeeded for ${user}@${host}, but the remote dataset list could not be loaded yet. ${error.message || error}`);
418437
}
419438
} catch (error) {
@@ -422,6 +441,7 @@ async function connectRemote() {
422441
state.remote.selectedDatasets.clear();
423442
updateRemoteServerStatus(null);
424443
renderRemoteDatasets();
444+
addClientLog("error", "tunnel", `Tunnel check failed for ${user}@${host}.`, errorToDetail(error));
425445
setStatus(`Tunnel check failed for ${user}@${host}. Make sure the SSH tunnel is open and the remote HTTP server is running on port 8000.`);
426446
}
427447
}
@@ -466,12 +486,20 @@ async function uploadLoadedFiles() {
466486
}
467487

468488
els.uploadBtn.disabled = true;
489+
addClientLog(
490+
"info",
491+
"upload",
492+
`Uploading ${localFiles.length.toLocaleString()} local file${localFiles.length === 1 ? "" : "s"} to the backend.`,
493+
localFiles.map((file) => file.name).join("\n"),
494+
);
469495
try {
470496
const uploaded = await uploadFilesToRemote(localFiles);
471497
await refreshRemoteServerStatus();
472498
await refreshRemoteDatasets();
499+
addClientLog("success", "upload", `Uploaded ${uploaded.toLocaleString()} file(s) successfully.`);
473500
setStatus(`Uploaded ${uploaded.toLocaleString()} file(s) to the remote server for ${user}@${host}.`);
474501
} catch (error) {
502+
addClientLog("error", "upload", "Upload stopped.", errorToDetail(error));
475503
setStatus(`Upload stopped. ${error.message || error}`);
476504
} finally {
477505
els.uploadBtn.disabled = false;
@@ -531,6 +559,7 @@ async function loadAndRender() {
531559
redraw();
532560
} catch (error) {
533561
console.error(error);
562+
addClientLog("error", state.remote.connected ? "tree" : "local", "Could not render tree.", errorToDetail(error));
534563
setStatus(`Could not render tree: ${error.message || error}`);
535564
}
536565
}
@@ -553,13 +582,22 @@ async function uploadFilesToRemote(files) {
553582
for (const file of files) {
554583
const form = new FormData();
555584
form.append("file", file, file.name);
556-
const response = await fetch("http://localhost:8000/upload", {
557-
method: "POST",
558-
body: form,
559-
});
585+
addClientLog("info", "upload", `Uploading file ${file.name}.`);
586+
let response;
587+
try {
588+
response = await fetch("http://localhost:8000/upload", {
589+
method: "POST",
590+
body: form,
591+
});
592+
} catch (error) {
593+
addClientLog("error", "upload", `Upload fetch failed for ${file.name}.`, errorToDetail(error));
594+
throw error;
595+
}
560596
if (!response.ok) {
597+
addClientLog("error", "upload", `Upload failed for ${file.name}.`, `HTTP ${response.status}`);
561598
throw new Error(`Upload failed for ${file.name} with HTTP ${response.status}`);
562599
}
600+
addClientLog("success", "upload", `Upload finished for ${file.name}.`);
563601
uploaded++;
564602
}
565603
return uploaded;
@@ -586,14 +624,22 @@ async function loadAndRenderRemote() {
586624
}
587625

588626
state.remote.expandedTaxids.clear();
627+
addClientLog(
628+
"info",
629+
"tree",
630+
`Requesting remote root tree view for ${files.length.toLocaleString()} dataset${files.length === 1 ? "" : "s"}.`,
631+
files.join("\n"),
632+
);
589633
const payload = await fetchRemoteVisibleTree();
590634
if (!payload.tree) {
635+
addClientLog("error", "tree", "The remote backend returned no tree payload.");
591636
setStatus("The remote backend returned no tree to render.");
592637
return;
593638
}
594639

595640
applyRemoteVisiblePayload(payload);
596641
state.centerOnNextRender = true;
642+
addClientLog("success", "tree", `Loaded backend tree with ${Number(payload.direct_taxa || 0).toLocaleString()} direct taxa.`);
597643
setStatus(`Loaded backend tree for ${state.series.length.toLocaleString()} remote dataset${state.series.length === 1 ? "" : "s"} and ${state.remote.directTaxa.toLocaleString()} direct taxa.`);
598644
redraw();
599645
}
@@ -760,13 +806,23 @@ function buildRemoteContextUrl(path, options = {}) {
760806

761807
async function fetchRemoteVisibleTree(options = {}) {
762808
const endpoint = options.taxid != null ? "expand-node" : "root-view";
763-
const response = await fetch(buildRemoteContextUrl(endpoint, {
809+
const url = buildRemoteContextUrl(endpoint, {
764810
expandedTaxids: options.expandedTaxids || Array.from(state.remote.expandedTaxids),
765811
query: options.taxid != null ? { taxid: options.taxid } : {},
766-
}).toString(), { method: "GET" });
812+
}).toString();
813+
addClientLog("info", "tree", `GET /${endpoint}`, url);
814+
let response;
815+
try {
816+
response = await fetch(url, { method: "GET" });
817+
} catch (error) {
818+
addClientLog("error", "tree", `Remote ${endpoint} fetch failed.`, errorToDetail(error));
819+
throw error;
820+
}
767821
if (!response.ok) {
822+
addClientLog("error", "tree", `Remote ${endpoint} request failed.`, `HTTP ${response.status}`);
768823
throw new Error(`Remote ${endpoint} request failed with HTTP ${response.status}`);
769824
}
825+
addClientLog("success", "tree", `Remote ${endpoint} request succeeded.`);
770826
return response.json();
771827
}
772828

@@ -826,11 +882,20 @@ async function fetchRemoteRankReport(taxids) {
826882
}
827883

828884
async function fetchRemoteFullTreeModel() {
829-
const response = await fetch(buildRemoteContextUrl("tree-model").toString(), { method: "GET" });
885+
addClientLog("info", "tree", "GET /tree-model");
886+
let response;
887+
try {
888+
response = await fetch(buildRemoteContextUrl("tree-model").toString(), { method: "GET" });
889+
} catch (error) {
890+
addClientLog("error", "tree", "Remote tree-model fetch failed.", errorToDetail(error));
891+
throw error;
892+
}
830893
const payload = await response.json().catch(() => ({}));
831894
if (!response.ok) {
895+
addClientLog("error", "tree", "Remote tree-model request failed.", payload?.detail?.message || `HTTP ${response.status}`);
832896
throw new Error(payload?.detail?.message || `Remote tree-model request failed with HTTP ${response.status}`);
833897
}
898+
addClientLog("success", "tree", "Remote tree-model request succeeded.");
834899
return payload;
835900
}
836901

@@ -865,10 +930,18 @@ async function refreshRemoteDatasets(options = {}) {
865930
}
866931

867932
const { selectAll = false } = options;
868-
const response = await fetch("http://localhost:8000/datasets", {
869-
method: "GET",
870-
});
933+
addClientLog("info", "datasets", "Refreshing remote dataset list.", "GET http://localhost:8000/datasets");
934+
let response;
935+
try {
936+
response = await fetch("http://localhost:8000/datasets", {
937+
method: "GET",
938+
});
939+
} catch (error) {
940+
addClientLog("error", "datasets", "Remote datasets fetch failed.", errorToDetail(error));
941+
throw error;
942+
}
871943
if (!response.ok) {
944+
addClientLog("error", "datasets", "Remote datasets request failed.", `HTTP ${response.status}`);
872945
throw new Error(`Remote datasets request failed with HTTP ${response.status}`);
873946
}
874947

@@ -896,6 +969,7 @@ async function refreshRemoteDatasets(options = {}) {
896969
}
897970

898971
renderRemoteDatasets();
972+
addClientLog("success", "datasets", `Loaded ${state.remote.datasets.length.toLocaleString()} remote dataset${state.remote.datasets.length === 1 ? "" : "s"}.`);
899973
}
900974

901975
function renderRemoteDatasets() {
@@ -2363,6 +2437,70 @@ function setStatus(message) {
23632437
els.status.textContent = message;
23642438
}
23652439

2440+
function addClientLog(level, stage, message, detail = "") {
2441+
const entry = {
2442+
timestamp: new Date().toISOString(),
2443+
level: String(level || "info"),
2444+
stage: String(stage || "general"),
2445+
message: String(message || ""),
2446+
detail: String(detail || ""),
2447+
};
2448+
state.clientLog.unshift(entry);
2449+
if (state.clientLog.length > 200) {
2450+
state.clientLog.length = 200;
2451+
}
2452+
renderClientLog();
2453+
}
2454+
2455+
function clearClientLog() {
2456+
state.clientLog = [];
2457+
renderClientLog();
2458+
setStatus("Cleared client log.");
2459+
}
2460+
2461+
function renderClientLog() {
2462+
if (!els.clientLog || !els.clientLogMeta) return;
2463+
const entries = state.clientLog;
2464+
els.clientLogMeta.textContent = entries.length
2465+
? `${entries.length.toLocaleString()} recent client event${entries.length === 1 ? "" : "s"}`
2466+
: "No client log entries yet";
2467+
if (!entries.length) {
2468+
els.clientLog.innerHTML = `<div class="remote-datasets-empty">Client-side upload, dataset, and tree requests will appear here.</div>`;
2469+
return;
2470+
}
2471+
els.clientLog.innerHTML = entries.map((entry) => `
2472+
<div class="client-log-entry">
2473+
<div class="client-log-entry-head">
2474+
<div class="client-log-badges">
2475+
<span class="client-log-badge stage-${escapeHtml(entry.stage)}">${escapeHtml(entry.stage)}</span>
2476+
<span class="client-log-badge level-${escapeHtml(entry.level)}">${escapeHtml(entry.level)}</span>
2477+
</div>
2478+
<span class="client-log-time">${escapeHtml(formatLogTime(entry.timestamp))}</span>
2479+
</div>
2480+
<div class="client-log-message">${escapeHtml(entry.message)}</div>
2481+
${entry.detail ? `<pre class="client-log-detail">${escapeHtml(entry.detail)}</pre>` : ""}
2482+
</div>
2483+
`).join("");
2484+
}
2485+
2486+
function formatLogTime(timestamp) {
2487+
const parsed = new Date(timestamp);
2488+
if (Number.isNaN(parsed.getTime())) return timestamp;
2489+
return parsed.toLocaleTimeString([], {
2490+
hour: "2-digit",
2491+
minute: "2-digit",
2492+
second: "2-digit",
2493+
});
2494+
}
2495+
2496+
function errorToDetail(error) {
2497+
if (!error) return "";
2498+
if (typeof error === "string") return error;
2499+
const message = error.message || String(error);
2500+
const stack = typeof error.stack === "string" ? error.stack : "";
2501+
return stack && !stack.startsWith(message) ? `${message}\n${stack}` : message;
2502+
}
2503+
23662504
function initChartPan() {
23672505
let pointerId = null;
23682506
let startX = 0;

src/libunicorn/graphengine/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ <h2>Reports</h2>
169169
</div>
170170
</div>
171171
</section>
172+
173+
<section class="panel-section" aria-label="Client log section">
174+
<div class="panel-section-head">
175+
<h2>Client Log</h2>
176+
<button id="toggleLogSection" class="secondary compact-btn section-toggle" type="button" aria-expanded="false">Show</button>
177+
</div>
178+
<div id="logSectionBody" class="panel-section-body" hidden>
179+
<div class="client-log-head">
180+
<div id="clientLogMeta" class="client-log-meta">No client log entries yet</div>
181+
<button id="clearClientLogBtn" class="secondary compact-btn" type="button">Clear Log</button>
182+
</div>
183+
<div id="clientLog" class="client-log" aria-live="polite"></div>
184+
</div>
185+
</section>
172186
</section>
173187
<div id="controlsResize" class="controls-resize" role="separator" aria-orientation="vertical" title="Drag to resize controls"></div>
174188
<button id="sidebarToggle" class="sidebar-tab" type="button" aria-expanded="true" aria-controls="controls">Hide Panel</button>

0 commit comments

Comments
 (0)