BC_praca/Frontend/JS/frontend.js

467 lines
16 KiB
JavaScript

const btnIF = document.getElementById("btnIF");
const btnAE = document.getElementById("btnAE");
const startBtn = document.getElementById("startBtn");
let chosenAlgorithm = null;
let chosenAlgorithmName = "";
let startTime = null;
let lastResults = null;
let progressInterval = null;
let backendErrorShown = false;
const JOB_STORAGE_KEY = "default_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-700", "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 = "isolation_forest";
chosenAlgorithmName = "Isolation Forest";
enableStartButton();
});
btnAE.addEventListener("click", () => {
selectAlgorithm(btnAE, btnIF);
chosenAlgorithm = "autoencoder";
chosenAlgorithmName = "Autoencoder";
enableStartButton();
});
function getBackendJobName() {
return chosenAlgorithm === "isolation_forest" ? "iforest" : "autoencoder";
}
function getResultURL() {
return chosenAlgorithm === "isolation_forest"
? "/get-isolation-forest-result"
: "/get-autoencoder-result";
}
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 === "isolation_forest") {
selectAlgorithm(btnIF, btnAE);
} else if (chosenAlgorithm === "autoencoder") {
selectAlgorithm(btnAE, btnIF);
}
enableStartButton();
}
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.querySelector("#loadingOverlay p");
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();
}
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();
lastResults = json.data;
if (json.data?.error) {
showErrorPopup(`Algorithm failed.<br>${json.data.message}`);
return;
}
displayResults(json.data);
})
.catch(err => {
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);
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 =
chosenAlgorithm === "isolation_forest"
? "/start-iforest"
: "/start-autoencoder";
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
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);
// Dynamicky vytvoríme tabuľku pre Top 5 anomálie (ak existujú)
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>
<!-- Metriky tabuľka -->
<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>
<!-- Distribúcia + Systémové info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 mb-10">
<!-- Pie Chart -->
<div class="flex flex-col items-center">
<h3 class="text-2xl text-orange-500 font-semibold mb-4">Class distribution</h3>
<canvas id="pieChart" class="w-80 h-80"></canvas>
</div>
<!-- RAM a počty -->
<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)}</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)}</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} 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} 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} 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} MB</span></div>
</div>
</div>
</div>
${topAnomaliesHTML}
<!-- Download tlačidlá -->
<div class="w-full flex justify-center gap-10 mt-12">
<button id="downloadPDF"
class="px-10 py-5 bg-orange-500 hover:bg-orange-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"
}
}
}
}
});
}