CoolFace
Apppublic

jatnikonm/simulations

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
remote_journaling.html506 linesDownload Raw Back to root
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">&times;</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">&times;</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