516 lines
18 KiB
JavaScript
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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} |