jatnikonm/simulations
0
1<!DOCTYPE html>2<html lang="id">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Simulasi 3D Remote Journaling (Laporan Kualitas)</title> <script src="https://cdn.tailwindcss.com"></script>7 <style>8 body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background-color: #1a202c; }9 canvas { display: block; }10 /* --- UI Panel Styling --- */11 #ui-panel {12 position: absolute;13 top: 10px;14 left: 10px;15 background-color: rgba(42, 50, 65, 0.9);16 color: white;17 padding: 15px;18 border-radius: 8px;19 width: 320px;20 font-size: 0.8rem;21 backdrop-filter: blur(5px);22 border: 1px solid rgba(255, 255, 255, 0.1);23 max-height: calc(100vh - 20px);24 overflow-y: auto;25 z-index: 10;26 }27 #ui-panel::-webkit-scrollbar { width: 6px; }28 #ui-panel::-webkit-scrollbar-track { background: #2d3748; border-radius: 3px;}29 #ui-panel::-webkit-scrollbar-thumb { background: #4a5568; border-radius: 3px;}30 #ui-panel::-webkit-scrollbar-thumb:hover { background: #718096; }31 32 .control-group { margin-bottom: 12px; }33 .control-label { display: block; margin-bottom: 3px; font-weight: 500; color: #a0aec0; }34 .control-value { font-weight: bold; color: #63b3ed; min-width: 45px; display: inline-block; text-align: right; margin-left: 5px; }35 input[type=range] { margin-top: 3px; width: 100%; cursor: pointer; appearance: none; height: 8px; background: #4a5568; border-radius: 4px; }36 input[type=range]::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; background: #63b3ed; border-radius: 50%; cursor: pointer; }37 input[type=range]::-moz-range-thumb { width: 16px; height: 16px; background: #63b3ed; border-radius: 50%; cursor: pointer; border: none; }38 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; }39 button:hover { background-color: #2b6cb0; }40 button:disabled { background-color: #4a5568; cursor: not-allowed; }41 button.active { background-color: #f56565; }42 button.active:hover { background-color: #c53030; }43 button.secondary { background-color: #718096; }44 button.secondary:hover { background-color: #4a5568; }45 .status-ok { color: #68d391; } /* Hijau */46 .status-error { color: #f56565; } /* Merah */47 .status-warn { color: #faf089; } /* Kuning */48 .info-grid { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: center;}49 hr.divider { margin: 15px 0; border-color: #4a5568; }50 51 /* --- Info Box Styling --- */52 #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; }53 #info-box h4 { font-weight: bold; color: #90cdf4; margin-bottom: 8px; border-bottom: 1px solid #4a5568; padding-bottom: 4px;}54 #info-box p { margin: 3px 0; color: #cbd5e0;}55 #info-box strong { color: #e2e8f0; min-width: 70px; display: inline-block;}56 57 /* --- Modal Styling (Umum) --- */58 .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; }59 .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; }60 .modal-close-btn { color: #a0aec0; position: absolute; top: 10px; right: 15px; font-size: 24px; font-weight: bold; cursor: pointer; line-height: 1; }61 .modal-close-btn:hover, .modal-close-btn:focus { color: #e2e8f0; text-decoration: none; }62 .modal h3 { margin-top: 0; margin-bottom: 20px; color: #90cdf4; text-align: center; font-size: 1.2rem; }63 64 /* --- Styling Khusus Modal Diagram & Laporan --- */65 #dr-diagram-image { width: 100%; height: auto; max-height: 70vh; object-fit: contain; background-color: #1a202c; border-radius: 4px; border: 1px solid #4a5568; }66 #report-content { line-height: 1.6; }67 #report-content strong { color: #90cdf4; min-width: 180px; display: inline-block; }68 #report-content span { color: #e2e8f0; }69 /* Styling untuk Kualitas */70 #report-quality.status-ok { color: #68d391; font-weight: bold; }71 #report-quality.status-warn { color: #faf089; font-weight: bold; }72 #report-quality.status-error { color: #f56565; font-weight: bold; }73 </style>74 <script type="importmap">75 {76 "imports": {77 "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",78 "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"79 }80 }81 </script>82</head>83<body>84 <div id="ui-panel" class="shadow-lg">85 <h2 class="text-lg font-bold mb-3 text-center text-gray-200">Simulasi Remote Journaling</h2>86 <div class="control-group info-grid">87 <span class="control-label">Status Sistem:</span> <span id="system-status" class="control-value status-ok">Idle</span>88 <span class="control-label">Jaringan:</span> <span id="network-status" class="control-value status-ok">OK</span>89 <span class="control-label">Server Sekunder:</span><span id="remote-server-status" class="control-value status-ok">Aktif</span>90 </div>91 <hr class="divider">92 <h3 class="text-md font-semibold mb-2 text-center text-gray-300">Statistik Sesi</h3>93 <div class="control-group info-grid">94 <span class="control-label">Jurnal Dibuat:</span><span id="total-journals-created" class="control-value">0</span>95 <span class="control-label">Jurnal Diterapkan:</span><span id="total-journals-applied" class="control-value">0</span>96 <span class="control-label">Durasi Berjalan:</span> <span id="current-duration" class="control-value">0s</span>97 <span class="control-label">Antrian Jurnal:</span><span id="queue-journal-length" class="control-value">0</span>98 </div>99 <hr class="divider">100 <h3 class="text-md font-semibold mb-2 text-center text-gray-300">Kontrol</h3>101 <div class="control-group">102 <label for="transaction-rate" class="control-label">Target Transaksi/detik: <span id="transaction-rate-value" class="control-value">1.0</span></label>103 <input type="range" id="transaction-rate" min="0.1" max="10" step="0.1" value="1">104 </div>105 <div class="control-group">106 <label for="network-latency" class="control-label">Latensi Jaringan (ms): <span id="network-latency-value" class="control-value">50</span></label>107 <input type="range" id="network-latency" min="10" max="1000" step="10" value="50">108 </div>109 <hr class="divider">110 <button id="toggle-transactions-btn">Mulai Transaksi</button>111 <button id="toggle-network-btn" class="mt-2">Putuskan Jaringan</button>112 <button id="toggle-remote-server-btn" class="mt-2">Matikan Server Sekunder</button>113 <button id="show-dr-diagram-btn" class="mt-2 secondary">Tampilkan Prosedur DR</button>114 <p class="text-xs mt-4 text-gray-400 text-center">Klik Server untuk Info. Mouse untuk Navigasi.</p>115 </div>116 117 <div id="scene-container"></div>118 119 <div id="info-box"></div>120 121 <div id="dr-diagram-modal" class="modal">122 <div class="modal-content">123 <span class="modal-close-btn" id="close-dr-modal-btn">×</span>124 <h3>Diagram Aktivitas Prosedur Disaster Recovery</h3>125 <img id="dr-diagram-image"126 src="file://uploaded:dg_dr.png-004f7c38-308b-4b49-9892-fb7819bb3295"127 alt="Diagram Aktivitas Persiapan Disaster Recovery">128 <p class="text-xs text-center mt-2 text-gray-400">129 Diagram ini mengilustrasikan langkah-langkah persiapan implementasi remote journaling untuk disaster recovery.130 </p>131 </div>132 </div>133 134 <div id="report-modal" class="modal">135 <div class="modal-content">136 <span class="modal-close-btn" id="close-report-modal-btn">×</span>137 <h3>Laporan Akhir Simulasi Remote Journaling</h3>138 <div id="report-content">139 <p><strong>Periode Simulasi:</strong> <span id="report-period">-</span></p>140 <p><strong>Durasi Total:</strong> <span id="report-duration">-</span> detik</p>141 <hr class="my-2 border-gray-600">142 <p><strong>Total Jurnal Dibuat:</strong> <span id="report-created">-</span></p>143 <p><strong>Total Jurnal Diterapkan:</strong> <span id="report-applied">-</span></p>144 <p><strong>Tingkat Keberhasilan:</strong> <span id="report-success-rate">-</span> %</p>145 <p><strong>Antrian Maksimal:</strong> <span id="report-max-queue">-</span> jurnal</p>146 <hr class="my-2 border-gray-600">147 <p><strong>Pengaturan Tx Rate (Final):</strong> <span id="report-final-tx-rate">-</span> /detik</p>148 <p><strong>Pengaturan Latensi (Final):</strong> <span id="report-final-latency">-</span> ms</p>149 <p><strong>Jumlah Gangguan Jaringan:</strong> <span id="report-network-downtime">-</span> kali</p>150 <p><strong>Jumlah Gangguan Server Sekunder:</strong> <span id="report-server-downtime">-</span> kali</p>151 <hr class="my-2 border-gray-600">152 <p><strong>Status Akhir Sistem:</strong> <span id="report-final-status">-</span></p>153 <p><strong>Kualitas Sistem (Simulasi):</strong> <span id="report-quality">-</span></p>154 </div>155 </div>156 </div>157 158 159 <script type="module">160 import * as THREE from 'three';161 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';162 163 // --- Konfigurasi Dasar ---164 let scene, camera, renderer, controls, clock, raycaster, mouse;165 let primaryServer, remoteServer, networkPathTube;166 const journalPackets = [];167 const journalQueueData = [];168 let totalJournalsCreated = 0;169 let totalJournalsApplied = 0;170 let intersectedObject = null;171 let infoBoxTimeout = null;172 173 // --- Status & Parameter Simulasi ---174 const SystemState = { IDLE: 'Idle', GENERATING: 'Generating Journals', NETWORK_DOWN: 'Jaringan Putus', SERVER_DOWN: 'Server Sekunder Mati' };175 let currentSystemState = SystemState.IDLE;176 let transactionRate = 1.0;177 let networkLatencyMs = 50;178 let isNetworkOk = true;179 let isRemoteServerOk = true;180 let isGeneratingTransactions = false;181 let transactionIntervalId = null;182 let journalSequence = 0;183 184 // --- Statistik untuk Laporan ---185 let simulationStartTime = null;186 let simulationEndTime = null;187 let maxQueueLength = 0;188 let networkDowntimeCount = 0;189 let serverDowntimeCount = 0;190 let sessionUpdateIntervalId = null;191 192 // --- Elemen UI ---193 const ui = {};194 195 // --- Fungsi Inisialisasi ---196 function init() {197 // Assign elemen UI ke objek ui setelah DOM siap198 Object.assign(ui, {199 systemStatus: document.getElementById('system-status'),200 networkStatus: document.getElementById('network-status'),201 remoteServerStatus: document.getElementById('remote-server-status'),202 queueLength: document.getElementById('queue-journal-length'),203 totalCreated: document.getElementById('total-journals-created'),204 totalApplied: document.getElementById('total-journals-applied'),205 currentDuration: document.getElementById('current-duration'),206 transactionRateSlider: document.getElementById('transaction-rate'),207 transactionRateValue: document.getElementById('transaction-rate-value'),208 latencySlider: document.getElementById('network-latency'),209 latencyValue: document.getElementById('network-latency-value'),210 toggleTransactionsBtn: document.getElementById('toggle-transactions-btn'),211 toggleNetworkBtn: document.getElementById('toggle-network-btn'),212 toggleRemoteServerBtn: document.getElementById('toggle-remote-server-btn'),213 showDrDiagramBtn: document.getElementById('show-dr-diagram-btn'),214 infoBox: document.getElementById('info-box'),215 drDiagramModal: document.getElementById('dr-diagram-modal'),216 closeDrModalBtn: document.getElementById('close-dr-modal-btn'),217 reportModal: document.getElementById('report-modal'),218 closeReportModalBtn: document.getElementById('close-report-modal-btn'),219 reportPeriod: document.getElementById('report-period'),220 reportDuration: document.getElementById('report-duration'),221 reportCreated: document.getElementById('report-created'),222 reportApplied: document.getElementById('report-applied'),223 reportSuccessRate: document.getElementById('report-success-rate'),224 reportMaxQueue: document.getElementById('report-max-queue'),225 reportFinalTxRate: document.getElementById('report-final-tx-rate'),226 reportFinalLatency: document.getElementById('report-final-latency'),227 reportNetworkDowntime: document.getElementById('report-network-downtime'),228 reportServerDowntime: document.getElementById('report-server-downtime'),229 reportFinalStatus: document.getElementById('report-final-status'),230 reportQuality: document.getElementById('report-quality'), // Elemen Kualitas Baru231 uiPanel: document.getElementById('ui-panel')232 });233 234 clock = new THREE.Clock();235 scene = new THREE.Scene();236 scene.background = new THREE.Color(0x1a202c);237 camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);238 camera.position.set(0, 10, 35);239 renderer = new THREE.WebGLRenderer({ antialias: true });240 renderer.setSize(window.innerWidth, window.innerHeight);241 document.getElementById('scene-container').appendChild(renderer.domElement);242 scene.add(new THREE.AmbientLight(0xcccccc, 0.8));243 const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);244 dirLight.position.set(5, 10, 7.5);245 scene.add(dirLight);246 controls = new OrbitControls(camera, renderer.domElement);247 controls.enableDamping = true;248 controls.dampingFactor = 0.05;249 controls.minDistance = 10;250 controls.maxDistance = 100;251 raycaster = new THREE.Raycaster();252 mouse = new THREE.Vector2();253 254 createEnvironment();255 setupUIListeners();256 setupInteractionListeners();257 resetStatistics();258 updateUI();259 animate();260 }261 262 // --- Membuat Objek 3D ---263 function createEnvironment() { /* ... sama ... */264 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);265 const serverWidth = 2.5; const serverHeight = 5; const serverDepth = 5; const serverGeometry = new THREE.BoxGeometry(serverWidth, serverHeight, serverDepth);266 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);267 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);268 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);269 }270 function addServerDetails(serverObject) { /* ... sama ... */271 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);272 }273 274 // --- Logika Remote Journaling ---275 function toggleTransactionGeneration() { /* ... sama ... */276 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();277 }278 function startJournalInterval() { /* ... sama ... */279 if (transactionIntervalId) clearInterval(transactionIntervalId); const interval = 1000 / transactionRate; transactionIntervalId = setInterval(createJournalEntry, interval);280 }281 function stopJournalInterval() { /* ... sama ... */282 if (transactionIntervalId) { clearInterval(transactionIntervalId); transactionIntervalId = null; }283 }284 function createJournalEntry() { /* ... sama ... */285 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();286 }287 function sendJournalEntryVisual(journalData) { /* ... sama ... */288 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);289 }290 function processJournalQueue() { /* ... sama ... */291 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); }292 }293 function updateJournalPacketPositions(deltaTime) { /* ... sama ... */294 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); }295 }296 function applyJournalEntry(packet) { /* ... sama ... */297 totalJournalsApplied++; updateUI(); visualEffect(remoteServer, 0xffffff, 100);298 }299 300 // --- Statistik & Laporan ---301 function resetStatistics() {302 totalJournalsCreated = 0;303 totalJournalsApplied = 0;304 journalSequence = 0;305 journalQueueData.length = 0;306 maxQueueLength = 0;307 networkDowntimeCount = 0;308 serverDowntimeCount = 0;309 simulationStartTime = null; // Reset waktu mulai310 simulationEndTime = null;311 // Reset tampilan statistik sesi di UI Panel312 if(ui.totalCreated) ui.totalCreated.textContent = '0';313 if(ui.totalApplied) ui.totalApplied.textContent = '0';314 if(ui.queueLength) ui.queueLength.textContent = '0';315 if(ui.currentDuration) ui.currentDuration.textContent = '0s';316 }317 function startSessionUpdate() { /* ... sama ... */318 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);319 }320 function stopSessionUpdate() { /* ... sama ... */321 if (sessionUpdateIntervalId) clearInterval(sessionUpdateIntervalId); sessionUpdateIntervalId = null; if(ui.currentDuration) ui.currentDuration.textContent = "0s";322 }323 324 // *** Fungsi Laporan Diperbarui dengan Penilaian Kualitas ***325 function generateAndShowReport() {326 const endTime = simulationEndTime || new Date();327 const startTime = simulationStartTime || endTime;328 const durationSeconds = startTime ? (endTime - startTime) / 1000 : 0;329 const successRate = totalJournalsCreated > 0 ? (totalJournalsApplied / totalJournalsCreated * 100) : 0;330 const finalTxRate = transactionRate;331 const finalLatency = networkLatencyMs;332 333 // --- Logika Penilaian Kualitas (Contoh Sederhana) ---334 let qualityStatus = "-";335 let qualityClass = "";336 // Thresholds (contoh, bisa disesuaikan)337 const goodSuccessRate = 98;338 const warningSuccessRate = 90;339 const highQueueThreshold = 20; // Antrian dianggap tinggi jika > 20340 const criticalQueueThreshold = 100; // Antrian dianggap kritis jika > 100341 342 if (successRate >= goodSuccessRate && maxQueueLength <= highQueueThreshold && networkDowntimeCount === 0 && serverDowntimeCount === 0) {343 qualityStatus = "Stabil";344 qualityClass = "status-ok";345 } else if (successRate >= goodSuccessRate && maxQueueLength <= highQueueThreshold) {346 qualityStatus = "Baik";347 qualityClass = "status-ok";348 } else if (successRate >= warningSuccessRate && maxQueueLength <= criticalQueueThreshold) {349 qualityStatus = "Perlu Perhatian";350 qualityClass = "status-warn";351 } else {352 qualityStatus = "Buruk";353 qualityClass = "status-error";354 }355 // Beri catatan jika ada downtime meskipun status baik/stabil356 if ((qualityStatus === "Stabil" || qualityStatus === "Baik") && (networkDowntimeCount > 0 || serverDowntimeCount > 0)) {357 qualityStatus += " (dengan gangguan)";358 if (qualityStatus === "Stabil (dengan gangguan)") qualityClass = "status-warn"; // Downgrade ke warning jika ada gangguan359 }360 // --- Akhir Logika Penilaian Kualitas ---361 362 363 const formatTime = (date) => date ? date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-';364 const formatDate = (date) => date ? date.toLocaleDateString('id-ID') : '-';365 366 // Isi elemen modal laporan367 ui.reportPeriod.textContent = `${formatDate(startTime)} ${formatTime(startTime)} - ${formatDate(endTime)} ${formatTime(endTime)}`;368 ui.reportDuration.textContent = durationSeconds.toFixed(1);369 ui.reportCreated.textContent = totalJournalsCreated;370 ui.reportApplied.textContent = totalJournalsApplied;371 ui.reportSuccessRate.textContent = successRate.toFixed(1);372 ui.reportMaxQueue.textContent = maxQueueLength;373 ui.reportFinalTxRate.textContent = finalTxRate.toFixed(1);374 ui.reportFinalLatency.textContent = finalLatency.toFixed(0);375 ui.reportNetworkDowntime.textContent = networkDowntimeCount;376 ui.reportServerDowntime.textContent = serverDowntimeCount;377 ui.reportFinalStatus.textContent = currentSystemState;378 ui.reportQuality.textContent = qualityStatus; // Tampilkan Kualitas379 ui.reportQuality.className = `control-value ${qualityClass}`; // Beri warna pada Kualitas380 381 ui.reportModal.style.display = 'flex'; // Tampilkan modal382 }383 384 // --- Update UI & State ---385 function changeSystemState(newState) { /* ... sama ... */386 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();387 }388 function updateUI() { // Update UI utama389 if (!ui.networkStatus) return;390 ui.networkStatus.textContent = isNetworkOk ? 'OK' : 'Terputus';391 ui.networkStatus.className = `control-value ${isNetworkOk ? 'status-ok' : 'status-error'}`;392 if (networkPathTube && networkPathTube.material) {393 networkPathTube.material.color.set(isNetworkOk ? 0x00ffff : 0xf56565);394 }395 ui.remoteServerStatus.textContent = isRemoteServerOk ? 'Aktif' : 'Mati';396 ui.remoteServerStatus.className = `control-value ${isRemoteServerOk ? 'status-ok' : 'status-error'}`;397 remoteServer.material.opacity = isRemoteServerOk ? 1.0 : 0.4;398 primaryServer.material.opacity = 1.0;399 ui.transactionRateValue.textContent = transactionRate.toFixed(1);400 ui.latencyValue.textContent = networkLatencyMs;401 ui.toggleNetworkBtn.textContent = isNetworkOk ? 'Putuskan Jaringan' : 'Pulihkan Jaringan';402 ui.toggleNetworkBtn.classList.toggle('active', !isNetworkOk);403 ui.toggleRemoteServerBtn.textContent = isRemoteServerOk ? 'Matikan Server Sekunder' : 'Nyalakan Server Sekunder';404 ui.toggleRemoteServerBtn.classList.toggle('active', !isRemoteServerOk);405 // Update statistik sesi di panel utama406 ui.queueLength.textContent = journalQueueData.length;407 ui.totalCreated.textContent = totalJournalsCreated;408 ui.totalApplied.textContent = totalJournalsApplied;409 if(ui.currentDuration) {410 let currentDurationVal = 0;411 if (simulationStartTime && isGeneratingTransactions) {412 currentDurationVal = ((new Date() - simulationStartTime) / 1000);413 }414 ui.currentDuration.textContent = `${currentDurationVal.toFixed(1)}s`;415 }416 }417 function visualEffect(object, color, duration) { /* ... sama ... */418 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);419 }420 421 // --- Event Listeners UI ---422 function setupUIListeners() {423 ui.transactionRateSlider.addEventListener('input', (e) => {424 transactionRate = parseFloat(e.target.value);425 ui.transactionRateValue.textContent = transactionRate.toFixed(1);426 if (isGeneratingTransactions) startJournalInterval();427 });428 ui.latencySlider.addEventListener('input', (e) => {429 networkLatencyMs = parseInt(e.target.value);430 updateUI();431 });432 ui.toggleTransactionsBtn.addEventListener('click', toggleTransactionGeneration);433 ui.toggleNetworkBtn.addEventListener('click', () => {434 isNetworkOk = !isNetworkOk;435 if (!isNetworkOk) networkDowntimeCount++; // Hitung saat putus436 if (isNetworkOk) {437 processJournalQueue();438 if (journalQueueData.length === 0 && isRemoteServerOk) changeSystemState(isGeneratingTransactions ? SystemState.GENERATING : SystemState.IDLE);439 } else {440 if (currentSystemState !== SystemState.SERVER_DOWN) changeSystemState(SystemState.NETWORK_DOWN);441 }442 updateUI();443 });444 ui.toggleRemoteServerBtn.addEventListener('click', () => {445 isRemoteServerOk = !isRemoteServerOk;446 if(!isRemoteServerOk) serverDowntimeCount++; // Hitung saat mati447 if (isRemoteServerOk) {448 processJournalQueue();449 if (journalQueueData.length === 0 && isNetworkOk) changeSystemState(isGeneratingTransactions ? SystemState.GENERATING : SystemState.IDLE);450 } else {451 if (currentSystemState !== SystemState.NETWORK_DOWN) changeSystemState(SystemState.SERVER_DOWN);452 }453 updateUI();454 });455 456 // Listener modal diagram DR457 ui.showDrDiagramBtn.addEventListener('click', () => { ui.drDiagramModal.style.display = 'flex'; });458 ui.closeDrModalBtn.addEventListener('click', () => { ui.drDiagramModal.style.display = 'none'; });459 ui.drDiagramModal.addEventListener('click', (event) => { if (event.target === ui.drDiagramModal) ui.drDiagramModal.style.display = 'none'; });460 461 // Listener modal laporan462 ui.closeReportModalBtn.addEventListener('click', () => { ui.reportModal.style.display = 'none'; });463 ui.reportModal.addEventListener('click', (event) => { if (event.target === ui.reportModal) ui.reportModal.style.display = 'none'; });464 465 window.addEventListener('resize', onWindowResize, false);466 }467 468 // --- Event Listeners Interaksi 3D ---469 function setupInteractionListeners() { /* ... sama ... */470 window.addEventListener('click', onMouseClick, false);471 }472 function onMouseClick(event) { /* ... sama ... */473 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(); }474 }475 function showInfoBox(object, mouseX, mouseY) { /* ... sama ... */476 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);477 }478 function hideInfoBox() { /* ... sama ... */479 ui.infoBox.style.display = 'none'; clearTimeout(infoBoxTimeout);480 }481 482 // --- Handle Resize ---483 function onWindowResize() { /* ... sama ... */484 camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight);485 }486 487 // --- Animation Loop ---488 function animate() {489 requestAnimationFrame(animate);490 const deltaTime = clock.getDelta();491 try {492 updateJournalPacketPositions(deltaTime);493 controls.update();494 renderer.render(scene, camera);495 } catch (error) {496 console.error("Error in animation loop:", error);497 }498 }499 500 // --- Mulai Simulasi ---501 init(); // Panggil init untuk memulai502 503 </script>504</body>505</html>506 