Spaces:
Running
Running
jnm-itb
feat: update remote journaling link and enhance simulation description in index.html
f746138
<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">×</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">×</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> | |