BC_praca/Frontend/JS/upload_choosing_fetch.js

516 lines
18 KiB
JavaScript

const btnIF = document.getElementById("btnIF");
const btnAE = document.getElementById("btnAE");
const startBtn = document.getElementById("startBtn");
let chosenAlgorithm = null;
let chosenAlgorithmName = null;
let startTime = null;
let lastResults = null;
let progressInterval = null;
let backendErrorShown = false;
const JOB_STORAGE_KEY = "custom_active_job";
function selectAlgorithm(selected, other) {
selected.classList.add("scale-105", "border-2", "border-black");
selected.classList.remove("opacity-50");
other.classList.remove("scale-105", "border-2", "border-black");
other.classList.add("opacity-50");
}
function enableStartButton() {
startBtn.classList.remove("bg-gray-600", "text-gray-300", "cursor-not-allowed");
startBtn.classList.add("bg-orange-500", "hover:bg-orange-400", "text-white", "cursor-pointer");
}
btnIF.addEventListener("click", () => {
selectAlgorithm(btnIF, btnAE);
chosenAlgorithm = "iforest_custom";
chosenAlgorithmName = "Isolation Forest (Custom)";
enableStartButton();
});
btnAE.addEventListener("click", () => {
selectAlgorithm(btnAE, btnIF);
chosenAlgorithm = "autoencoder_custom";
chosenAlgorithmName = "Autoencoder (Custom)";
enableStartButton();
});
function getBackendJobName() {
return chosenAlgorithm;
}
function getResultURL() {
if (chosenAlgorithm === "iforest_custom") {
return "/get-isolation-forest-result-custom";
}
return "/get-autoencoder-result-custom";
}
function saveActiveJob() {
sessionStorage.setItem(JOB_STORAGE_KEY, JSON.stringify({
algorithm: chosenAlgorithm,
algorithmName: chosenAlgorithmName,
backendJob: getBackendJobName(),
startedAt: Date.now()
}));
}
function loadActiveJob() {
const raw = sessionStorage.getItem(JOB_STORAGE_KEY);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
sessionStorage.removeItem(JOB_STORAGE_KEY);
return null;
}
}
function clearActiveJob() {
sessionStorage.removeItem(JOB_STORAGE_KEY);
}
function applySavedAlgorithm(saved) {
chosenAlgorithm = saved.algorithm;
chosenAlgorithmName = saved.algorithmName;
startTime = saved.startedAt;
if (chosenAlgorithm === "iforest_custom") {
selectAlgorithm(btnIF, btnAE);
} else if (chosenAlgorithm === "autoencoder_custom") {
selectAlgorithm(btnAE, btnIF);
}
enableStartButton();
}
function restoreCustomRunLayout() {
const step1 = document.getElementById("step-1");
const step2 = document.getElementById("step-2");
const step3 = document.getElementById("step-3");
const step4 = document.getElementById("step-4");
const step5 = document.getElementById("step-5");
if (step1) step1.classList.add("hidden");
if (step2) step2.classList.add("hidden");
if (step3) step3.classList.add("hidden");
if (step4) step4.classList.add("hidden");
if (step5) step5.classList.remove("hidden");
}
function updateProgressUI(progress) {
document.getElementById("progressContainer").classList.remove("hidden");
document.getElementById("progressBar").style.width = progress + "%";
document.getElementById("progressLabel").textContent = progress + "%";
}
function showLoading(customText = null) {
document.getElementById("loadingOverlay").classList.remove("hidden");
document.getElementById("progressBar").style.width = "0%";
document.getElementById("progressLabel").textContent = "0%";
document.getElementById("progressContainer").classList.add("hidden");
const loadingText = document.getElementById("loadingText");
if (loadingText) {
loadingText.innerHTML = customText ?? `Starting ${chosenAlgorithmName}...<br>This may take a while.`;
}
}
function hideLoading() {
document.getElementById("loadingOverlay").classList.add("hidden");
}
function showErrorPopup(message) {
const popup = document.createElement("div");
popup.innerHTML = `
<div class="fixed inset-0 flex justify-center items-center bg-black bg-opacity-70 z-50">
<div class="bg-red-600 p-6 rounded-xl shadow-xl text-center max-w-md">
<h2 class="text-2xl font-bold text-white mb-4">Error</h2>
<p class="text-white mb-6">${message}</p>
<button id="closeError"
class="bg-white text-red-600 px-6 py-2 rounded-lg font-semibold hover:bg-gray-200 transition">
Close
</button>
</div>
</div>
`;
document.body.appendChild(popup);
document.getElementById("closeError").onclick = () => popup.remove();
}
const originalFetch = window.fetch.bind(window);
window.fetch = async function (...args) {
const res = await originalFetch(...args);
let url = "";
const req = args[0];
if (typeof req === "string") {
url = req;
} else if (req && typeof req.url === "string") {
url = req.url;
}
if (res.status === 409) {
let message = "Another operation is already running.";
try {
const data = await res.clone().json();
if (data?.error) {
message = data.error;
} else if (data?.message) {
message = data.message;
}
} catch (_) {}
if (url.includes("/upload-dataset")) {
showErrorPopup(message || "Cannot upload a new dataset while an algorithm is running.");
} else if (url.includes("/save-config")) {
showErrorPopup(message || "Cannot save config while an algorithm is running.");
}
}
return res;
};
function fetchFinalResults(retryCount = 0) {
fetch(getResultURL())
.then(res => res.json())
.then(json => {
if (!json.ready) {
if (retryCount < 10) {
setTimeout(() => fetchFinalResults(retryCount + 1), 1000);
return;
}
hideLoading();
clearActiveJob();
showErrorPopup("Final results are not ready.");
return;
}
hideLoading();
clearActiveJob();
restoreCustomRunLayout();
lastResults = json.data;
if (json.data?.error) {
showErrorPopup(`Algorithm failed.<br>${json.data.message}`);
return;
}
displayResults(json.data);
})
.catch(() => {
hideLoading();
clearActiveJob();
showErrorPopup("Error fetching final results.");
});
}
function startProgressPolling() {
if (progressInterval) {
clearInterval(progressInterval);
}
document.getElementById("progressContainer").classList.remove("hidden");
progressInterval = setInterval(async () => {
try {
const res = await fetch("/get-status");
if (!res.ok) {
throw new Error("Backend status fetch failed");
}
const data = await res.json();
updateProgressUI(data.progress || 0);
if (!data.running) {
clearInterval(progressInterval);
progressInterval = null;
fetchFinalResults();
}
} catch (err) {
clearInterval(progressInterval);
progressInterval = null;
hideLoading();
clearActiveJob();
if (!backendErrorShown) {
backendErrorShown = true;
showErrorPopup("Connection to backend was lost.<br>Backend may have stopped.");
}
}
}, 1500);
}
async function restoreRunningJob() {
const saved = loadActiveJob();
if (!saved) return;
try {
const res = await fetch("/get-status");
const status = await res.json();
if (status.current_job !== saved.backendJob) {
clearActiveJob();
return;
}
applySavedAlgorithm(saved);
restoreCustomRunLayout();
showLoading(`Resuming ${chosenAlgorithmName}...<br>This may take a while.`);
updateProgressUI(status.progress || 0);
if (status.running) {
startProgressPolling();
} else {
fetchFinalResults();
}
} catch (err) {
hideLoading();
clearActiveJob();
if (!backendErrorShown) {
backendErrorShown = true;
showErrorPopup("Connection to backend was lost.<br>Please start the backend and try again.");
}
}
}
startBtn.addEventListener("click", () => {
if (!chosenAlgorithm) return;
showLoading();
backendErrorShown = false;
saveActiveJob();
let url;
if (chosenAlgorithm === "iforest_custom") {
url = "/start-iforest-custom";
} else if (chosenAlgorithm === "autoencoder_custom") {
url = "/start-autoencoder-custom";
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60000);
fetch(url, { method: "POST", signal: controller.signal })
.then(async res => {
clearTimeout(timeout);
if (res.status === 409) {
hideLoading();
clearActiveJob();
showErrorPopup("Another algorithm is already running.<br>Please wait until it finishes.");
return;
}
if (!res.ok) {
throw new Error("Server error");
}
const saved = loadActiveJob();
startTime = saved?.startedAt ?? Date.now();
startProgressPolling();
})
.catch(err => {
hideLoading();
clearActiveJob();
console.error(err);
showErrorPopup("Backend server is not running.<br>Start app.py and try again.");
});
});
window.addEventListener("load", restoreRunningJob);
function displayResults(data) {
const container = document.getElementById("resultContainer");
container.classList.remove("hidden");
const runtime = ((Date.now() - startTime) / 1000).toFixed(2);
let topAnomaliesHTML = '';
if (data.top_anomalies && data.top_anomalies.length > 0) {
const headers = Object.keys(data.top_anomalies[0]);
topAnomaliesHTML = `
<div class="mt-12">
<h3 class="text-2xl font-bold text-orange-500 mb-6 text-center">
Top 5 detected anomalies
</h3>
<div class="overflow-x-auto">
<table class="min-w-full bg-gray-800 rounded-xl shadow-xl">
<thead class="bg-gray-700">
<tr>
${headers.map(h => `<th class="px-4 py-3 text-orange-500">${h}</th>`).join('')}
</tr>
</thead>
<tbody>
${data.top_anomalies.map(row => `
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
${headers.map(key => {
const val = row[key];
return `<td class="px-4 py-3 text-center">${typeof val === 'number' ? val.toFixed(4) : val}</td>`;
}).join('')}
</tr>`).join('')}
</tbody>
</table>
</div>
</div>`;
}
container.innerHTML = `
<h2 class="text-3xl font-bold text-orange-500 mb-8 text-center">
Results
</h2>
<div class="overflow-x-auto mb-10">
<table class="min-w-full bg-gray-800 rounded-xl shadow-xl text-center">
<thead class="bg-gray-700">
<tr>
<th class="px-6 py-3 text-orange-500">Class</th>
<th class="px-6 py-3 text-orange-500">Precision</th>
<th class="px-6 py-3 text-orange-500">Recall</th>
<th class="px-6 py-3 text-orange-500">F1-Score</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700">
<td class="py-4 px-6 font-semibold">Normal traffic</td>
<td class="py-4 px-6">${data.precision_normal?.toFixed(4) ?? '-'}</td>
<td class="py-4 px-6">${data.recall_normal?.toFixed(4) ?? '-'}</td>
<td class="py-4 px-6">${data.f1_normal?.toFixed(4) ?? '-'}</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-4 px-6 font-semibold">Anomaly</td>
<td class="py-4 px-6">${data.precision_attack?.toFixed(4) ?? '-'}</td>
<td class="py-4 px-6">${data.recall_attack?.toFixed(4) ?? '-'}</td>
<td class="py-4 px-6">${data.f1_attack?.toFixed(4) ?? '-'}</td>
</tr>
<tr class="bg-gray-700 text-white font-bold">
<td class="py-4 px-6">Overall accuracy</td>
<td class="py-4 px-6">-</td>
<td class="py-4 px-6">-</td>
<td class="py-4 px-6">${data.accuracy?.toFixed(4) ?? '-'}</td>
</tr>
</tbody>
</table>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 mb-10">
<div class="flex flex-col items-center">
<h3 class="text-2xl text-orange-5oo font-semibold mb-4">Class distribution</h3>
<canvas id="pieChart" class="w-80 h-80"></canvas>
</div>
<div class="bg-gray-800/60 rounded-xl p-8 border border-gray-700">
<h3 class="text-2xl text-orange-500 font-semibold mb-6">Performance</h3>
<div class="space-y-4 text-lg">
<div class="flex justify-between"><span class="text-gray-400">Normal traffic:</span> <span class="font-bold text-white">${Math.round(data.normal_count || 0)}</span></div>
<div class="flex justify-between"><span class="text-gray-400">Anomalies:</span> <span class="font-bold text-white">${Math.round(data.attack_count || 0)}</span></div>
<div class="flex justify-between"><span class="text-gray-400">Runtime:</span> <span class="font-bold text-white">${runtime} s</span></div>
<div class="flex justify-between"><span class="text-gray-400">RAM before:</span> <span class="font-bold text-white">${data.ram_before || 0} MB</span></div>
<div class="flex justify-between"><span class="text-gray-400">RAM peak:</span> <span class="font-bold text-white">${data.ram_peak || 0} MB</span></div>
<div class="flex justify-between"><span class="text-gray-400">RAM after:</span> <span class="font-bold text-white">${data.ram_after || 0} MB</span></div>
<div class="flex justify-between"><span class="text-red-400 font-semibold">RAM increase:</span> <span class="font-bold text-red-300">+${data.ram_increase || 0} MB</span></div>
</div>
</div>
</div>
${topAnomaliesHTML}
<div class="w-full flex justify-center gap-10 mt-12">
<button id="downloadPDF"
class="px-10 py-5 bg-teal-500 hover:bg-teal-400 text-white text-xl font-bold rounded-xl transition shadow-xl cursor-pointer transform hover:scale-105">
Download PDF
</button>
<button id="downloadJSON"
class="px-10 py-5 bg-yellow-500 hover:bg-yellow-400 text-white text-xl font-bold rounded-xl transition shadow-xl cursor-pointer transform hover:scale-105">
Download JSON
</button>
</div>
`;
loadPieChart(data);
document.getElementById("downloadPDF").addEventListener("click", () => {
downloadPDF(data, runtime);
});
document.getElementById("downloadJSON").addEventListener("click", () => {
downloadJSON(data, runtime);
});
}
function downloadPDF(results, runtime) {
fetch("/download-pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
algorithm: chosenAlgorithmName,
results: results,
runtime: runtime
})
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "report.pdf";
link.click();
});
}
function downloadJSON(results, runtime) {
fetch("/download-json", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
algorithm: chosenAlgorithmName,
results: results,
runtime: runtime
})
})
.then(res => res.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "report.json";
link.click();
});
}
function loadPieChart(data) {
const ctx = document.getElementById("pieChart");
new Chart(ctx, {
type: "pie",
data: {
labels: ["Normal Traffic", "Attacks"],
datasets: [{
data: [data.normal_count || 0, data.attack_count || 0],
backgroundColor: ["#14b8a6", "#dc2626"],
}]
},
options: {
plugins: {
legend: {
labels: {
color: "#ffffff"
}
}
}
}
});
}