simulations / remote_journaling.html
jnm-itb
feat: update remote journaling link and enhance simulation description in index.html
f746138
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulasi 3D Remote Journaling (Laporan Kualitas)</title> <script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background-color: #1a202c; }
canvas { display: block; }
/* --- UI Panel Styling --- */
#ui-panel {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(42, 50, 65, 0.9);
color: white;
padding: 15px;
border-radius: 8px;
width: 320px;
font-size: 0.8rem;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
max-height: calc(100vh - 20px);
overflow-y: auto;
z-index: 10;
}
#ui-panel::-webkit-scrollbar { width: 6px; }
#ui-panel::-webkit-scrollbar-track { background: #2d3748; border-radius: 3px;}
#ui-panel::-webkit-scrollbar-thumb { background: #4a5568; border-radius: 3px;}
#ui-panel::-webkit-scrollbar-thumb:hover { background: #718096; }
.control-group { margin-bottom: 12px; }
.control-label { display: block; margin-bottom: 3px; font-weight: 500; color: #a0aec0; }
.control-value { font-weight: bold; color: #63b3ed; min-width: 45px; display: inline-block; text-align: right; margin-left: 5px; }
input[type=range] { margin-top: 3px; width: 100%; cursor: pointer; appearance: none; height: 8px; background: #4a5568; border-radius: 4px; }
input[type=range]::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; background: #63b3ed; border-radius: 50%; cursor: pointer; }
input[type=range]::-moz-range-thumb { width: 16px; height: 16px; background: #63b3ed; border-radius: 50%; cursor: pointer; border: none; }
button { background-color: #4299e1; border: none; padding: 8px 15px; border-radius: 4px; transition: background-color 0.2s; width: 100%; margin-top: 10px; font-weight: 600; }
button:hover { background-color: #2b6cb0; }
button:disabled { background-color: #4a5568; cursor: not-allowed; }
button.active { background-color: #f56565; }
button.active:hover { background-color: #c53030; }
button.secondary { background-color: #718096; }
button.secondary:hover { background-color: #4a5568; }
.status-ok { color: #68d391; } /* Hijau */
.status-error { color: #f56565; } /* Merah */
.status-warn { color: #faf089; } /* Kuning */
.info-grid { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: center;}
hr.divider { margin: 15px 0; border-color: #4a5568; }
/* --- Info Box Styling --- */
#info-box { display: none; position: absolute; background-color: rgba(10, 20, 35, 0.9); color: #e2e8f0; padding: 12px; border-radius: 6px; border: 1px solid #4a5568; font-size: 0.75rem; max-width: 280px; z-index: 20; pointer-events: none; }
#info-box h4 { font-weight: bold; color: #90cdf4; margin-bottom: 8px; border-bottom: 1px solid #4a5568; padding-bottom: 4px;}
#info-box p { margin: 3px 0; color: #cbd5e0;}
#info-box strong { color: #e2e8f0; min-width: 70px; display: inline-block;}
/* --- Modal Styling (Umum) --- */
.modal { display: none; position: fixed; z-index: 50; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(5px); justify-content: center; align-items: center; }
.modal-content { background-color: #2d3748; margin: auto; padding: 25px; border: 1px solid #4a5568; width: 80%; max-width: 700px; border-radius: 8px; position: relative; color: white; font-size: 0.9rem; }
.modal-close-btn { color: #a0aec0; position: absolute; top: 10px; right: 15px; font-size: 24px; font-weight: bold; cursor: pointer; line-height: 1; }
.modal-close-btn:hover, .modal-close-btn:focus { color: #e2e8f0; text-decoration: none; }
.modal h3 { margin-top: 0; margin-bottom: 20px; color: #90cdf4; text-align: center; font-size: 1.2rem; }
/* --- Styling Khusus Modal Diagram & Laporan --- */
#dr-diagram-image { width: 100%; height: auto; max-height: 70vh; object-fit: contain; background-color: #1a202c; border-radius: 4px; border: 1px solid #4a5568; }
#report-content { line-height: 1.6; }
#report-content strong { color: #90cdf4; min-width: 180px; display: inline-block; }
#report-content span { color: #e2e8f0; }
/* Styling untuk Kualitas */
#report-quality.status-ok { color: #68d391; font-weight: bold; }
#report-quality.status-warn { color: #faf089; font-weight: bold; }
#report-quality.status-error { color: #f56565; font-weight: bold; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="ui-panel" class="shadow-lg">
<h2 class="text-lg font-bold mb-3 text-center text-gray-200">Simulasi Remote Journaling</h2>
<div class="control-group info-grid">
<span class="control-label">Status Sistem:</span> <span id="system-status" class="control-value status-ok">Idle</span>
<span class="control-label">Jaringan:</span> <span id="network-status" class="control-value status-ok">OK</span>
<span class="control-label">Server Sekunder:</span><span id="remote-server-status" class="control-value status-ok">Aktif</span>
</div>
<hr class="divider">
<h3 class="text-md font-semibold mb-2 text-center text-gray-300">Statistik Sesi</h3>
<div class="control-group info-grid">
<span class="control-label">Jurnal Dibuat:</span><span id="total-journals-created" class="control-value">0</span>
<span class="control-label">Jurnal Diterapkan:</span><span id="total-journals-applied" class="control-value">0</span>
<span class="control-label">Durasi Berjalan:</span> <span id="current-duration" class="control-value">0s</span>
<span class="control-label">Antrian Jurnal:</span><span id="queue-journal-length" class="control-value">0</span>
</div>
<hr class="divider">
<h3 class="text-md font-semibold mb-2 text-center text-gray-300">Kontrol</h3>
<div class="control-group">
<label for="transaction-rate" class="control-label">Target Transaksi/detik: <span id="transaction-rate-value" class="control-value">1.0</span></label>
<input type="range" id="transaction-rate" min="0.1" max="10" step="0.1" value="1">
</div>
<div class="control-group">
<label for="network-latency" class="control-label">Latensi Jaringan (ms): <span id="network-latency-value" class="control-value">50</span></label>
<input type="range" id="network-latency" min="10" max="1000" step="10" value="50">
</div>
<hr class="divider">
<button id="toggle-transactions-btn">Mulai Transaksi</button>
<button id="toggle-network-btn" class="mt-2">Putuskan Jaringan</button>
<button id="toggle-remote-server-btn" class="mt-2">Matikan Server Sekunder</button>
<button id="show-dr-diagram-btn" class="mt-2 secondary">Tampilkan Prosedur DR</button>
<p class="text-xs mt-4 text-gray-400 text-center">Klik Server untuk Info. Mouse untuk Navigasi.</p>
</div>
<div id="scene-container"></div>
<div id="info-box"></div>
<div id="dr-diagram-modal" class="modal">
<div class="modal-content">
<span class="modal-close-btn" id="close-dr-modal-btn">&times;</span>
<h3>Diagram Aktivitas Prosedur Disaster Recovery</h3>
<img id="dr-diagram-image"
src="file://uploaded:dg_dr.png-004f7c38-308b-4b49-9892-fb7819bb3295"
alt="Diagram Aktivitas Persiapan Disaster Recovery">
<p class="text-xs text-center mt-2 text-gray-400">
Diagram ini mengilustrasikan langkah-langkah persiapan implementasi remote journaling untuk disaster recovery.
</p>
</div>
</div>
<div id="report-modal" class="modal">
<div class="modal-content">
<span class="modal-close-btn" id="close-report-modal-btn">&times;</span>
<h3>Laporan Akhir Simulasi Remote Journaling</h3>
<div id="report-content">
<p><strong>Periode Simulasi:</strong> <span id="report-period">-</span></p>
<p><strong>Durasi Total:</strong> <span id="report-duration">-</span> detik</p>
<hr class="my-2 border-gray-600">
<p><strong>Total Jurnal Dibuat:</strong> <span id="report-created">-</span></p>
<p><strong>Total Jurnal Diterapkan:</strong> <span id="report-applied">-</span></p>
<p><strong>Tingkat Keberhasilan:</strong> <span id="report-success-rate">-</span> %</p>
<p><strong>Antrian Maksimal:</strong> <span id="report-max-queue">-</span> jurnal</p>
<hr class="my-2 border-gray-600">
<p><strong>Pengaturan Tx Rate (Final):</strong> <span id="report-final-tx-rate">-</span> /detik</p>
<p><strong>Pengaturan Latensi (Final):</strong> <span id="report-final-latency">-</span> ms</p>
<p><strong>Jumlah Gangguan Jaringan:</strong> <span id="report-network-downtime">-</span> kali</p>
<p><strong>Jumlah Gangguan Server Sekunder:</strong> <span id="report-server-downtime">-</span> kali</p>
<hr class="my-2 border-gray-600">
<p><strong>Status Akhir Sistem:</strong> <span id="report-final-status">-</span></p>
<p><strong>Kualitas Sistem (Simulasi):</strong> <span id="report-quality">-</span></p>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- Konfigurasi Dasar ---
let scene, camera, renderer, controls, clock, raycaster, mouse;
let primaryServer, remoteServer, networkPathTube;
const journalPackets = [];
const journalQueueData = [];
let totalJournalsCreated = 0;
let totalJournalsApplied = 0;
let intersectedObject = null;
let infoBoxTimeout = null;
// --- Status & Parameter Simulasi ---
const SystemState = { IDLE: 'Idle', GENERATING: 'Generating Journals', NETWORK_DOWN: 'Jaringan Putus', SERVER_DOWN: 'Server Sekunder Mati' };
let currentSystemState = SystemState.IDLE;
let transactionRate = 1.0;
let networkLatencyMs = 50;
let isNetworkOk = true;
let isRemoteServerOk = true;
let isGeneratingTransactions = false;
let transactionIntervalId = null;
let journalSequence = 0;
// --- Statistik untuk Laporan ---
let simulationStartTime = null;
let simulationEndTime = null;
let maxQueueLength = 0;
let networkDowntimeCount = 0;
let serverDowntimeCount = 0;
let sessionUpdateIntervalId = null;
// --- Elemen UI ---
const ui = {};
// --- Fungsi Inisialisasi ---
function init() {
// Assign elemen UI ke objek ui setelah DOM siap
Object.assign(ui, {
systemStatus: document.getElementById('system-status'),
networkStatus: document.getElementById('network-status'),
remoteServerStatus: document.getElementById('remote-server-status'),
queueLength: document.getElementById('queue-journal-length'),
totalCreated: document.getElementById('total-journals-created'),
totalApplied: document.getElementById('total-journals-applied'),
currentDuration: document.getElementById('current-duration'),
transactionRateSlider: document.getElementById('transaction-rate'),
transactionRateValue: document.getElementById('transaction-rate-value'),
latencySlider: document.getElementById('network-latency'),
latencyValue: document.getElementById('network-latency-value'),
toggleTransactionsBtn: document.getElementById('toggle-transactions-btn'),
toggleNetworkBtn: document.getElementById('toggle-network-btn'),
toggleRemoteServerBtn: document.getElementById('toggle-remote-server-btn'),
showDrDiagramBtn: document.getElementById('show-dr-diagram-btn'),
infoBox: document.getElementById('info-box'),
drDiagramModal: document.getElementById('dr-diagram-modal'),
closeDrModalBtn: document.getElementById('close-dr-modal-btn'),
reportModal: document.getElementById('report-modal'),
closeReportModalBtn: document.getElementById('close-report-modal-btn'),
reportPeriod: document.getElementById('report-period'),
reportDuration: document.getElementById('report-duration'),
reportCreated: document.getElementById('report-created'),
reportApplied: document.getElementById('report-applied'),
reportSuccessRate: document.getElementById('report-success-rate'),
reportMaxQueue: document.getElementById('report-max-queue'),
reportFinalTxRate: document.getElementById('report-final-tx-rate'),
reportFinalLatency: document.getElementById('report-final-latency'),
reportNetworkDowntime: document.getElementById('report-network-downtime'),
reportServerDowntime: document.getElementById('report-server-downtime'),
reportFinalStatus: document.getElementById('report-final-status'),
reportQuality: document.getElementById('report-quality'), // Elemen Kualitas Baru
uiPanel: document.getElementById('ui-panel')
});
clock = new THREE.Clock();
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a202c);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 35);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('scene-container').appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xcccccc, 0.8));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 10;
controls.maxDistance = 100;
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createEnvironment();
setupUIListeners();
setupInteractionListeners();
resetStatistics();
updateUI();
animate();
}
// --- Membuat Objek 3D ---
function createEnvironment() { /* ... sama ... */
const floorGeometry = new THREE.PlaneGeometry(100, 100); const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x3a475c, side: THREE.DoubleSide }); const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.position.y = -1; scene.add(floor);
const serverWidth = 2.5; const serverHeight = 5; const serverDepth = 5; const serverGeometry = new THREE.BoxGeometry(serverWidth, serverHeight, serverDepth);
const primaryMaterial = new THREE.MeshStandardMaterial({ color: 0x3b82f6, metalness: 0.7, roughness: 0.4 }); primaryServer = new THREE.Mesh(serverGeometry, primaryMaterial); primaryServer.position.set(-18, serverHeight / 2 - 1, 0); primaryServer.userData = { name: "Server Primer (Database)", specs: { CPU: "16 Cores Xeon", RAM: "64 GB DDR4 ECC", Storage: "10 TB SSD (RAID 10)", Network: "10 Gbps Ethernet", OS: "Linux (Ubuntu Server 22.04)", Database: "PostgreSQL 15", Journaling: "Streaming Replication (Primary)", Monitoring: "Zabbix Agent", Function: "Menjalankan database utama, mencatat & mengirim jurnal." } }; scene.add(primaryServer); addServerDetails(primaryServer);
const remoteMaterial = new THREE.MeshStandardMaterial({ color: 0x10b981, metalness: 0.7, roughness: 0.4, transparent: true, opacity: 1.0 }); remoteServer = new THREE.Mesh(serverGeometry, remoteMaterial); remoteServer.position.set(18, serverHeight / 2 - 1, 0); remoteServer.userData = { name: "Server Sekunder (DRC)", specs: { CPU: "12 Cores Xeon", RAM: "48 GB DDR4 ECC", Storage: "10 TB SSD (RAID 5)", Network: "1 Gbps WAN Link", OS: "Linux (Ubuntu Server 22.04)", Database: "PostgreSQL 15", Journaling: "Streaming Replication (Standby)", Monitoring: "Zabbix Agent", Function: "Menerima & menerapkan jurnal, siap mengambil alih." } }; scene.add(remoteServer); addServerDetails(remoteServer);
const startPoint = primaryServer.position.clone().setY(serverHeight * 0.8 - 1); const endPoint = remoteServer.position.clone().setY(serverHeight * 0.8 - 1); const curve = new THREE.CatmullRomCurve3([ startPoint, new THREE.Vector3(0, 5, 0), endPoint ]); const tubeRadius = 0.15; const tubeSegments = 32; const radiusSegments = 8; const tubeGeometry = new THREE.TubeGeometry(curve, tubeSegments, tubeRadius, radiusSegments, false); const tubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ffff }); networkPathTube = new THREE.Mesh(tubeGeometry, tubeMaterial); scene.add(networkPathTube);
}
function addServerDetails(serverObject) { /* ... sama ... */
const ledGeometry = new THREE.SphereGeometry(0.1, 8, 8); const ledMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const led = new THREE.Mesh(ledGeometry, ledMaterial); const serverDepth = serverObject.geometry.parameters.depth; const serverHeight = serverObject.geometry.parameters.height; led.position.set(0, serverHeight * 0.4, serverDepth / 2 + 0.01); serverObject.add(led);
}
// --- Logika Remote Journaling ---
function toggleTransactionGeneration() { /* ... sama ... */
isGeneratingTransactions = !isGeneratingTransactions; if (isGeneratingTransactions) { if (!simulationStartTime) { resetStatistics(); simulationStartTime = new Date(); } changeSystemState(SystemState.GENERATING); startJournalInterval(); startSessionUpdate(); ui.toggleTransactionsBtn.textContent = "Hentikan Transaksi"; ui.toggleTransactionsBtn.classList.add('active'); } else { simulationEndTime = new Date(); changeSystemState(SystemState.IDLE); stopJournalInterval(); stopSessionUpdate(); generateAndShowReport(); ui.toggleTransactionsBtn.textContent = "Mulai Transaksi"; ui.toggleTransactionsBtn.classList.remove('active'); } updateUI();
}
function startJournalInterval() { /* ... sama ... */
if (transactionIntervalId) clearInterval(transactionIntervalId); const interval = 1000 / transactionRate; transactionIntervalId = setInterval(createJournalEntry, interval);
}
function stopJournalInterval() { /* ... sama ... */
if (transactionIntervalId) { clearInterval(transactionIntervalId); transactionIntervalId = null; }
}
function createJournalEntry() { /* ... sama ... */
if (!isGeneratingTransactions) return; journalSequence++; totalJournalsCreated++; const journalData = { id: journalSequence, timestamp: Date.now() }; if (isNetworkOk && isRemoteServerOk) { sendJournalEntryVisual(journalData); } else { journalQueueData.push(journalData); maxQueueLength = Math.max(maxQueueLength, journalQueueData.length); if (isNetworkOk && !isRemoteServerOk && currentSystemState !== SystemState.SERVER_DOWN) changeSystemState(SystemState.SERVER_DOWN); else if (!isNetworkOk && currentSystemState !== SystemState.NETWORK_DOWN) changeSystemState(SystemState.NETWORK_DOWN); } updateUI();
}
function sendJournalEntryVisual(journalData) { /* ... sama ... */
const packetGeometry = new THREE.SphereGeometry(0.25, 10, 10); const packetMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); const packetMesh = new THREE.Mesh(packetGeometry, packetMaterial); packetMesh.userData = { ...journalData, progress: 0 }; const curve = networkPathTube.geometry.parameters.path; packetMesh.position.copy(curve.getPointAt(0)); journalPackets.push(packetMesh); scene.add(packetMesh);
}
function processJournalQueue() { /* ... sama ... */
while (isNetworkOk && isRemoteServerOk && journalQueueData.length > 0) { const journalData = journalQueueData.shift(); sendJournalEntryVisual(journalData); } updateUI(); if (journalQueueData.length === 0 && isNetworkOk && isRemoteServerOk) { changeSystemState(isGeneratingTransactions ? SystemState.GENERATING : SystemState.IDLE); }
}
function updateJournalPacketPositions(deltaTime) { /* ... sama ... */
const packetsToRemove = []; const baseSpeed = 25; const latencyEffect = 1 / (1 + networkLatencyMs / 150); const curve = networkPathTube.geometry.parameters.path; journalPackets.forEach((packet, index) => { const packetData = packet.userData; packetData.progress += baseSpeed * latencyEffect * deltaTime; if (packetData.progress >= 1) { if (isRemoteServerOk) applyJournalEntry(packet); else console.warn("Packet reached down server, discarding:", packetData.id); packetsToRemove.push(index); } else { packet.position.copy(curve.getPointAt(packetData.progress)); } }); for (let i = packetsToRemove.length - 1; i >= 0; i--) { const indexToRemove = packetsToRemove[i]; scene.remove(journalPackets[indexToRemove]); journalPackets.splice(indexToRemove, 1); }
}
function applyJournalEntry(packet) { /* ... sama ... */
totalJournalsApplied++; updateUI(); visualEffect(remoteServer, 0xffffff, 100);
}
// --- Statistik & Laporan ---
function resetStatistics() {
totalJournalsCreated = 0;
totalJournalsApplied = 0;
journalSequence = 0;
journalQueueData.length = 0;
maxQueueLength = 0;
networkDowntimeCount = 0;
serverDowntimeCount = 0;
simulationStartTime = null; // Reset waktu mulai
simulationEndTime = null;
// Reset tampilan statistik sesi di UI Panel
if(ui.totalCreated) ui.totalCreated.textContent = '0';
if(ui.totalApplied) ui.totalApplied.textContent = '0';
if(ui.queueLength) ui.queueLength.textContent = '0';
if(ui.currentDuration) ui.currentDuration.textContent = '0s';
}
function startSessionUpdate() { /* ... sama ... */
if (sessionUpdateIntervalId) clearInterval(sessionUpdateIntervalId); sessionUpdateIntervalId = setInterval(() => { if (simulationStartTime && isGeneratingTransactions && ui.currentDuration) { const currentDuration = ((new Date() - simulationStartTime) / 1000); ui.currentDuration.textContent = `${currentDuration.toFixed(1)}s`; } }, 1000);
}
function stopSessionUpdate() { /* ... sama ... */
if (sessionUpdateIntervalId) clearInterval(sessionUpdateIntervalId); sessionUpdateIntervalId = null; if(ui.currentDuration) ui.currentDuration.textContent = "0s";
}
// *** Fungsi Laporan Diperbarui dengan Penilaian Kualitas ***
function generateAndShowReport() {
const endTime = simulationEndTime || new Date();
const startTime = simulationStartTime || endTime;
const durationSeconds = startTime ? (endTime - startTime) / 1000 : 0;
const successRate = totalJournalsCreated > 0 ? (totalJournalsApplied / totalJournalsCreated * 100) : 0;
const finalTxRate = transactionRate;
const finalLatency = networkLatencyMs;
// --- Logika Penilaian Kualitas (Contoh Sederhana) ---
let qualityStatus = "-";
let qualityClass = "";
// Thresholds (contoh, bisa disesuaikan)
const goodSuccessRate = 98;
const warningSuccessRate = 90;
const highQueueThreshold = 20; // Antrian dianggap tinggi jika > 20
const criticalQueueThreshold = 100; // Antrian dianggap kritis jika > 100
if (successRate >= goodSuccessRate && maxQueueLength <= highQueueThreshold && networkDowntimeCount === 0 && serverDowntimeCount === 0) {
qualityStatus = "Stabil";
qualityClass = "status-ok";
} else if (successRate >= goodSuccessRate && maxQueueLength <= highQueueThreshold) {
qualityStatus = "Baik";
qualityClass = "status-ok";
} else if (successRate >= warningSuccessRate && maxQueueLength <= criticalQueueThreshold) {
qualityStatus = "Perlu Perhatian";
qualityClass = "status-warn";
} else {
qualityStatus = "Buruk";
qualityClass = "status-error";
}
// Beri catatan jika ada downtime meskipun status baik/stabil
if ((qualityStatus === "Stabil" || qualityStatus === "Baik") && (networkDowntimeCount > 0 || serverDowntimeCount > 0)) {
qualityStatus += " (dengan gangguan)";
if (qualityStatus === "Stabil (dengan gangguan)") qualityClass = "status-warn"; // Downgrade ke warning jika ada gangguan
}
// --- Akhir Logika Penilaian Kualitas ---
const formatTime = (date) => date ? date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-';
const formatDate = (date) => date ? date.toLocaleDateString('id-ID') : '-';
// Isi elemen modal laporan
ui.reportPeriod.textContent = `${formatDate(startTime)} ${formatTime(startTime)} - ${formatDate(endTime)} ${formatTime(endTime)}`;
ui.reportDuration.textContent = durationSeconds.toFixed(1);
ui.reportCreated.textContent = totalJournalsCreated;
ui.reportApplied.textContent = totalJournalsApplied;
ui.reportSuccessRate.textContent = successRate.toFixed(1);
ui.reportMaxQueue.textContent = maxQueueLength;
ui.reportFinalTxRate.textContent = finalTxRate.toFixed(1);
ui.reportFinalLatency.textContent = finalLatency.toFixed(0);
ui.reportNetworkDowntime.textContent = networkDowntimeCount;
ui.reportServerDowntime.textContent = serverDowntimeCount;
ui.reportFinalStatus.textContent = currentSystemState;
ui.reportQuality.textContent = qualityStatus; // Tampilkan Kualitas
ui.reportQuality.className = `control-value ${qualityClass}`; // Beri warna pada Kualitas
ui.reportModal.style.display = 'flex'; // Tampilkan modal
}
// --- Update UI & State ---
function changeSystemState(newState) { /* ... sama ... */
if (currentSystemState === newState) return; currentSystemState = newState; ui.systemStatus.textContent = newState; ui.systemStatus.className = 'control-value '; if (newState === SystemState.IDLE || newState === SystemState.GENERATING) ui.systemStatus.classList.add('status-ok'); else if (newState === SystemState.NETWORK_DOWN || newState === SystemState.SERVER_DOWN) ui.systemStatus.classList.add('status-error'); else ui.systemStatus.classList.add('status-warn'); ui.toggleTransactionsBtn.disabled = false; updateUI();
}
function updateUI() { // Update UI utama
if (!ui.networkStatus) return;
ui.networkStatus.textContent = isNetworkOk ? 'OK' : 'Terputus';
ui.networkStatus.className = `control-value ${isNetworkOk ? 'status-ok' : 'status-error'}`;
if (networkPathTube && networkPathTube.material) {
networkPathTube.material.color.set(isNetworkOk ? 0x00ffff : 0xf56565);
}
ui.remoteServerStatus.textContent = isRemoteServerOk ? 'Aktif' : 'Mati';
ui.remoteServerStatus.className = `control-value ${isRemoteServerOk ? 'status-ok' : 'status-error'}`;
remoteServer.material.opacity = isRemoteServerOk ? 1.0 : 0.4;
primaryServer.material.opacity = 1.0;
ui.transactionRateValue.textContent = transactionRate.toFixed(1);
ui.latencyValue.textContent = networkLatencyMs;
ui.toggleNetworkBtn.textContent = isNetworkOk ? 'Putuskan Jaringan' : 'Pulihkan Jaringan';
ui.toggleNetworkBtn.classList.toggle('active', !isNetworkOk);
ui.toggleRemoteServerBtn.textContent = isRemoteServerOk ? 'Matikan Server Sekunder' : 'Nyalakan Server Sekunder';
ui.toggleRemoteServerBtn.classList.toggle('active', !isRemoteServerOk);
// Update statistik sesi di panel utama
ui.queueLength.textContent = journalQueueData.length;
ui.totalCreated.textContent = totalJournalsCreated;
ui.totalApplied.textContent = totalJournalsApplied;
if(ui.currentDuration) {
let currentDurationVal = 0;
if (simulationStartTime && isGeneratingTransactions) {
currentDurationVal = ((new Date() - simulationStartTime) / 1000);
}
ui.currentDuration.textContent = `${currentDurationVal.toFixed(1)}s`;
}
}
function visualEffect(object, color, duration) { /* ... sama ... */
if (!object || !object.material || typeof object.material.color?.getHex !== 'function') return; const originalColorHex = object.material.color.getHex(); object.material.color.set(color); setTimeout(() => { if (object && object.material && typeof object.material.color?.setHex === 'function') { object.material.color.setHex(originalColorHex); } }, duration);
}
// --- Event Listeners UI ---
function setupUIListeners() {
ui.transactionRateSlider.addEventListener('input', (e) => {
transactionRate = parseFloat(e.target.value);
ui.transactionRateValue.textContent = transactionRate.toFixed(1);
if (isGeneratingTransactions) startJournalInterval();
});
ui.latencySlider.addEventListener('input', (e) => {
networkLatencyMs = parseInt(e.target.value);
updateUI();
});
ui.toggleTransactionsBtn.addEventListener('click', toggleTransactionGeneration);
ui.toggleNetworkBtn.addEventListener('click', () => {
isNetworkOk = !isNetworkOk;
if (!isNetworkOk) networkDowntimeCount++; // Hitung saat putus
if (isNetworkOk) {
processJournalQueue();
if (journalQueueData.length === 0 && isRemoteServerOk) changeSystemState(isGeneratingTransactions ? SystemState.GENERATING : SystemState.IDLE);
} else {
if (currentSystemState !== SystemState.SERVER_DOWN) changeSystemState(SystemState.NETWORK_DOWN);
}
updateUI();
});
ui.toggleRemoteServerBtn.addEventListener('click', () => {
isRemoteServerOk = !isRemoteServerOk;
if(!isRemoteServerOk) serverDowntimeCount++; // Hitung saat mati
if (isRemoteServerOk) {
processJournalQueue();
if (journalQueueData.length === 0 && isNetworkOk) changeSystemState(isGeneratingTransactions ? SystemState.GENERATING : SystemState.IDLE);
} else {
if (currentSystemState !== SystemState.NETWORK_DOWN) changeSystemState(SystemState.SERVER_DOWN);
}
updateUI();
});
// Listener modal diagram DR
ui.showDrDiagramBtn.addEventListener('click', () => { ui.drDiagramModal.style.display = 'flex'; });
ui.closeDrModalBtn.addEventListener('click', () => { ui.drDiagramModal.style.display = 'none'; });
ui.drDiagramModal.addEventListener('click', (event) => { if (event.target === ui.drDiagramModal) ui.drDiagramModal.style.display = 'none'; });
// Listener modal laporan
ui.closeReportModalBtn.addEventListener('click', () => { ui.reportModal.style.display = 'none'; });
ui.reportModal.addEventListener('click', (event) => { if (event.target === ui.reportModal) ui.reportModal.style.display = 'none'; });
window.addEventListener('resize', onWindowResize, false);
}
// --- Event Listeners Interaksi 3D ---
function setupInteractionListeners() { /* ... sama ... */
window.addEventListener('click', onMouseClick, false);
}
function onMouseClick(event) { /* ... sama ... */
if (ui.uiPanel && ui.uiPanel.contains(event.target)) { hideInfoBox(); return; } if (ui.drDiagramModal && ui.drDiagramModal.style.display !== 'none' && ui.drDiagramModal.contains(event.target)) { return; } if (ui.reportModal && ui.reportModal.style.display !== 'none' && ui.reportModal.contains(event.target)) { return; } mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects([primaryServer, remoteServer]); if (intersects.length > 0) { const clickedObject = intersects[0].object; showInfoBox(clickedObject, event.clientX, event.clientY); } else { hideInfoBox(); }
}
function showInfoBox(object, mouseX, mouseY) { /* ... sama ... */
if (!object.userData || !object.userData.specs) return; const specs = object.userData.specs; let infoHTML = `<h4>${object.userData.name}</h4>`; for (const key in specs) { let label = key.replace(/([A-Z])/g, ' $1').trim(); label = label.charAt(0).toUpperCase() + label.slice(1); infoHTML += `<p><strong>${label}:</strong> ${specs[key]}</p>`; } ui.infoBox.innerHTML = infoHTML; ui.infoBox.style.display = 'block'; const boxWidth = ui.infoBox.offsetWidth; const boxHeight = ui.infoBox.offsetHeight; let left = mouseX + 15; let top = mouseY + 15; if (left + boxWidth > window.innerWidth - 10) { left = mouseX - boxWidth - 15; } if (left < 10) { left = 10; } if (top + boxHeight > window.innerHeight - 10) { top = mouseY - boxHeight - 15; } if (top < 10) { top = 10; } ui.infoBox.style.left = `${left}px`; ui.infoBox.style.top = `${top}px`; clearTimeout(infoBoxTimeout); infoBoxTimeout = setTimeout(hideInfoBox, 6000);
}
function hideInfoBox() { /* ... sama ... */
ui.infoBox.style.display = 'none'; clearTimeout(infoBoxTimeout);
}
// --- Handle Resize ---
function onWindowResize() { /* ... sama ... */
camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
try {
updateJournalPacketPositions(deltaTime);
controls.update();
renderer.render(scene, camera);
} catch (error) {
console.error("Error in animation loop:", error);
}
}
// --- Mulai Simulasi ---
init(); // Panggil init untuk memulai
</script>
</body>
</html>