NEED NOT TO KNOW
NEED NOT TO KNOW
<!DOCTYPE html>
<html>
<head>
<title>Real-time Sensor Monitoring - DHT11, HCSR04, Soil</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Chart.js untuk grafik -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Papa Parse untuk parsing CSV -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
<!-- Font Awesome untuk ikon -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {
transition: background-color 0.3s, color 0.3s, border-color 0.3s, box-shadow 0.3s;
}
body {
font-family: 'Segoe UI', Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
/* Tema Normal (Default) */
body.theme-normal {
background-color: #f5f5f5;
}
body.theme-normal .container {
background-color: white;
color: #333;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
/* Tema Glow in the Dark */
body.theme-glow {
background-color: #0a0a0a;
}
body.theme-glow .container {
background-color: #111;
color: #fff;
box-shadow: 0 0 30px rgba(0,255,255,0.3), 0 0 60px rgba(255,0,255,0.2);
border: 1px solid rgba(255,255,255,0.1);
}
body.theme-glow h1 {
color: #fff;
text-shadow: 0 0 10px cyan, 0 0 20px magenta;
}
body.theme-glow .sensor-grid {
background: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.1);
}
body.theme-glow .sensor-item {
background: rgba(0,0,0,0.5);
border-color: rgba(255,255,255,0.2);
box-shadow: 0 0 15px rgba(0,255,255,0.2);
}
body.theme-glow .sensor-value {
text-shadow: 0 0 10px currentColor;
}
/* Tema Hecker (Matrix Style) */
body.theme-hecker {
background-color: #0f0f0f;
}
body.theme-hecker .container {
background-color: #000;
color: #00ff00;
box-shadow: 0 0 30px #00ff00;
border: 2px solid #00ff00;
font-family: 'Courier New', monospace;
}
body.theme-hecker h1 {
color: #00ff00;
text-shadow: 0 0 10px #00ff00;
font-family: 'Courier New', monospace;
}
body.theme-hecker .sensor-grid {
background: #001100;
border-color: #00ff00;
}
body.theme-hecker .sensor-item {
background: #001100;
border-color: #00ff00;
box-shadow: 0 0 10px #00ff00;
}
body.theme-hecker .sensor-label {
color: #00ff00;
}
body.theme-hecker .sensor-value {
color: #00ff00;
font-weight: bold;
text-shadow: 0 0 5px #00ff00;
}
body.theme-hecker button {
background: #001100;
color: #00ff00;
border: 2px solid #00ff00;
}
body.theme-hecker button:hover {
background: #00ff00;
color: #000;
}
/* Tema Rainbow */
body.theme-rainbow {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
body.theme-rainbow .container {
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
border: 3px solid transparent;
border-image: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1;
animation: border-rainbow 3s linear infinite;
}
body.theme-rainbow h1 {
background: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 3s linear infinite;
}
body.theme-rainbow .sensor-item {
background: rgba(255,255,255,0.8);
border: 2px solid transparent;
border-image: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1;
animation: border-rainbow 3s linear infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes rainbow {
0% { filter: hue-rotate(0deg); }
100% { filter: hue-rotate(360deg); }
}
@keyframes border-rainbow {
0% { border-color: red; }
17% { border-color: orange; }
33% { border-color: yellow; }
50% { border-color: green; }
67% { border-color: blue; }
83% { border-color: indigo; }
100% { border-color: violet; }
}
/* Theme selector */
.theme-selector {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.theme-btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.theme-btn i {
font-size: 16px;
}
.theme-btn.normal {
background: #f0f0f0;
color: #333;
}
.theme-btn.glow {
background: linear-gradient(45deg, cyan, magenta);
color: white;
text-shadow: 0 0 5px black;
}
.theme-btn.hecker {
background: black;
color: #00ff00;
border: 2px solid #00ff00;
font-family: 'Courier New', monospace;
}
.theme-btn.rainbow {
background: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet);
color: white;
text-shadow: 0 0 5px black;
animation: rainbow 3s linear infinite;
}
.theme-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
/* Sensor grid - 4 kolom */
.sensor-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
margin-bottom: 20px;
border: 2px solid #e9ecef;
}
.sensor-item {
background: white;
padding: 20px 10px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border: 1px solid #dee2e6;
transition: transform 0.2s;
}
.sensor-item:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.sensor-icon {
font-size: 32px;
margin-bottom: 10px;
}
.sensor-label {
font-size: 14px;
color: #6c757d;
margin-bottom: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.sensor-value {
font-size: 28px;
font-weight: bold;
color: #495057;
line-height: 1.2;
}
.sensor-unit {
font-size: 14px;
color: #adb5bd;
margin-left: 2px;
}
.sensor-trend {
font-size: 14px;
margin-top: 8px;
font-weight: bold;
}
.trend-up { color: #28a745; }
.trend-down { color: #dc3545; }
.trend-stable { color: #ffc107; }
.status {
text-align: center;
padding: 10px;
background-color: #e8f5e8;
border-radius: 5px;
color: #2e7d32;
font-weight: bold;
margin-bottom: 20px;
}
.error {
background-color: #ffebee;
color: #c62828;
}
.chart-container {
position: relative;
height: 500px;
width: 100%;
margin-bottom: 20px;
}
.last-update {
text-align: right;
font-size: 0.9em;
color: #666;
margin-top: 10px;
}
.debug-info {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
max-height: 150px;
overflow: auto;
margin-top: 10px;
display: none;
}
.show-debug {
display: block;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
/* Data count selector */
.data-count-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
border: 1px solid #dee2e6;
}
.data-count-btn {
padding: 8px 16px;
border: 2px solid #4CAF50;
background: white;
color: #4CAF50;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
min-width: 80px;
}
.data-count-btn:hover {
background: #4CAF50;
color: white;
transform: scale(1.05);
}
.data-count-btn.active {
background: #4CAF50;
color: white;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
}
/* Footer */
.footer {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
border-top: 3px solid #4CAF50;
}
.footer-title {
text-align: center;
margin-bottom: 15px;
font-weight: bold;
color: #495057;
}
.sensor-note {
background-color: #fff3e0;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
font-size: 0.9em;
color: #e65100;
}
.data-info {
text-align: center;
margin: 10px 0;
font-size: 14px;
color: #6c757d;
}
.data-info strong {
color: #4CAF50;
font-size: 18px;
}
</style>
</head>
<body class="theme-normal">
<div class="container">
<h1>
<i class="fas fa-microchip"></i>
Real-time Sensor Monitoring - DHT11, HCSR04, Soil
<i class="fas fa-chart-line"></i>
</h1>
<!-- Theme Selector -->
<div class="theme-selector">
<button class="theme-btn normal" onclick="setTheme('normal')">
<i class="fas fa-sun"></i> Normal
</button>
<button class="theme-btn glow" onclick="setTheme('glow')">
<i class="fas fa-moon"></i> Glow in the Dark
</button>
<button class="theme-btn hecker" onclick="setTheme('hecker')">
<i class="fas fa-code"></i> Hecker
</button>
<button class="theme-btn rainbow" onclick="setTheme('rainbow')">
<i class="fas fa-rainbow"></i> Rainbow
</button>
</div>
<div class="status" id="status">Menunggu data pertama...</div>
<!-- Sensor Grid - 4 kotak untuk 4 sensor -->
<div class="sensor-grid" id="sensorGrid">
<!-- Akan diisi oleh JavaScript -->
</div>
<div class="chart-container">
<canvas id="sensorChart"></canvas>
</div>
<div class="last-update" id="lastUpdate"></div>
<!-- Data Count Selector -->
<div class="data-count-selector" id="dataCountSelector">
<!-- Akan diisi oleh JavaScript -->
</div>
<div class="data-info" id="dataInfo">
Menampilkan <strong id="currentDataCount">20</strong> data terakhir dari total <strong id="totalDataCount">0</strong> data
</div>
<div class="control-panel">
<button onclick="toggleDebug()">
<i class="fas fa-bug"></i> Tampilkan/Sembunyikan Debug
</button>
</div>
<div id="debugInfo" class="debug-info"></div>
<!-- Footer dengan tombol data count -->
<div class="footer">
<div class="footer-title">
<i class="fas fa-chart-bar"></i> Pilih Jumlah Data yang Ditampilkan
</div>
<div class="data-count-selector" id="footerDataCountSelector">
<!-- Akan diisi oleh JavaScript -->
</div>
<div class="sensor-note">
<strong>Urutan Sensor di Spreadsheet:</strong> Kolom A: tanggal, Kolom B: time, Kolom C: temp, Kolom D: humidity, Kolom E: distance, Kolom F: soil
</div>
</div>
</div>
<script>
// Konfigurasi spreadsheet
const SPREADSHEET_ID = '1bxniTPfEGUORBOF3DfdJFvUWj5WpHL_bfR0my0Ypmrk';
const SHEET_GID = '0';
const CSV_URL = `https://docs.google.com/spreadsheets/d/${SPREADSHEET_ID}/export?format=csv&gid=${SHEET_GID}`;
// Data sensor terakhir untuk trend
let previousValues = {
suhu: 0,
kelembaban: 0,
jarak: 0,
soil: 0
};
// Jumlah data yang ditampilkan (default 20)
let displayDataCount = 20;
// Semua data yang tersedia
let allSensorData = [];
// Daftar jumlah data yang tersedia
const dataCountOptions = [50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600];
// Fungsi untuk mengganti tema
window.setTheme = function(theme) {
document.body.className = '';
document.body.classList.add(`theme-${theme}`);
// Simpan tema di localStorage
localStorage.setItem('selectedTheme', theme);
}
// Load tema tersimpan
const savedTheme = localStorage.getItem('selectedTheme') || 'normal';
setTheme(savedTheme);
// Inisialisasi chart
const ctx = document.getElementById('sensorChart').getContext('2d');
let sensorChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Suhu (°C)',
data: [],
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1,
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 5
},
{
label: 'Kelembaban (%)',
data: [],
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.1,
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 5
},
{
label: 'Jarak (cm)',
data: [],
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1,
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 5
},
{
label: 'Soil',
data: [],
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
tension: 0.1,
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 5
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 500
},
plugins: {
title: {
display: true,
text: `Sensor Readings - Last ${displayDataCount} Data Points`
},
legend: {
position: 'top',
labels: {
usePointStyle: true,
boxWidth: 6
}
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
title: {
display: true,
text: 'Waktu'
},
ticks: {
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 20
}
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Nilai Sensor'
},
beginAtZero: true
}
}
}
});
// Definisi sensor untuk grid (4 sensor)
const sensors = [
{ id: 'suhu', label: 'SUHU', icon: 'fa-temperature-high', unit: '°C', color: '#ff6384' },
{ id: 'kelembaban', label: 'KELEMBABAN', icon: 'fa-tint', unit: '%', color: '#36a2eb' },
{ id: 'jarak', label: 'JARAK', icon: 'fa-ruler', unit: 'cm', color: '#4bc0c0' },
{ id: 'soil', label: 'SOIL', icon: 'fa-seedling', unit: '', color: '#9966ff' }
];
// Fungsi untuk membuat tombol data count
function createDataCountButtons() {
const selector1 = document.getElementById('dataCountSelector');
const selector2 = document.getElementById('footerDataCountSelector');
let buttonsHtml = '';
dataCountOptions.forEach(count => {
buttonsHtml += `
<button class="data-count-btn ${displayDataCount === count ? 'active' : ''}"
onclick="setDataCount(${count})">
${count.toLocaleString()} data
</button>
`;
});
selector1.innerHTML = buttonsHtml;
selector2.innerHTML = buttonsHtml;
}
// Fungsi untuk mengatur jumlah data yang ditampilkan
window.setDataCount = function(count) {
displayDataCount = count;
// Update active state pada semua tombol
document.querySelectorAll('.data-count-btn').forEach(btn => {
if (btn.textContent.includes(count.toString())) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Update judul chart
sensorChart.options.plugins.title.text = `Sensor Readings - Last ${displayDataCount} Data Points`;
// Update tampilan dengan data yang sudah ada
if (allSensorData.length > 0) {
updateChartWithData();
}
// Simpan preferensi
localStorage.setItem('displayDataCount', count);
addDebug(`Jumlah data diubah menjadi ${count}`);
}
// Fungsi untuk membersihkan nilai dan konversi ke number
function cleanNumericValue(value) {
if (value === undefined || value === null || value === '') return 0;
// Hapus spasi dan konversi koma ke titik
let cleaned = value.toString().trim().replace(/,/g, '.');
// Ambil angka pertama yang ditemukan
const match = cleaned.match(/-?\d+\.?\d*/);
if (match) {
return parseFloat(match[0]);
}
return 0;
}
// Fungsi untuk update chart dengan data yang sudah difilter
function updateChartWithData() {
if (allSensorData.length === 0) return;
const dataToShow = allSensorData.slice(-displayDataCount);
// Buat label waktu (gabungkan tanggal dan time)
const labels = dataToShow.map((row, index) => {
const tanggal = row['tanggal'] || '';
const time = row['time'] || '';
if (tanggal && time) {
return `${tanggal} ${time}`;
} else if (tanggal) {
return tanggal;
} else if (time) {
return time;
} else {
return `Data ${index + 1}`;
}
});
// Data untuk chart
const suhuData = dataToShow.map(row => cleanNumericValue(row['temp']));
const kelembabanData = dataToShow.map(row => cleanNumericValue(row['humidity']));
const jarakData = dataToShow.map(row => cleanNumericValue(row['distance']));
const soilData = dataToShow.map(row => cleanNumericValue(row['soil']));
// Update chart
sensorChart.data.labels = labels;
sensorChart.data.datasets[0].data = suhuData;
sensorChart.data.datasets[1].data = kelembabanData;
sensorChart.data.datasets[2].data = jarakData;
sensorChart.data.datasets[3].data = soilData;
sensorChart.update();
// Update info data
document.getElementById('currentDataCount').textContent = dataToShow.length;
document.getElementById('totalDataCount').textContent = allSensorData.length;
}
// Fungsi untuk update sensor grid
function updateSensorGrid(values) {
const grid = document.getElementById('sensorGrid');
let html = '';
sensors.forEach(sensor => {
const currentValue = values[sensor.id] || 0;
const previousValue = previousValues[sensor.id] || 0;
// Tentukan trend
let trendClass = 'trend-stable';
let trendIcon = 'fa-minus';
if (currentValue > previousValue) {
trendClass = 'trend-up';
trendIcon = 'fa-arrow-up';
} else if (currentValue < previousValue) {
trendClass = 'trend-down';
trendIcon = 'fa-arrow-down';
}
// Format angka dengan 1 desimal
const displayValue = currentValue.toFixed(1);
html += `
<div class="sensor-item" style="border-left: 4px solid ${sensor.color}">
<div class="sensor-icon">
<i class="fas ${sensor.icon}" style="color: ${sensor.color}"></i>
</div>
<div class="sensor-label">${sensor.label}</div>
<div class="sensor-value">
${displayValue}<span class="sensor-unit">${sensor.unit}</span>
</div>
<div class="sensor-trend ${trendClass}">
<i class="fas ${trendIcon}"></i>
</div>
</div>
`;
});
grid.innerHTML = html;
// Update previous values
previousValues = { ...values };
}
// Fungsi toggle debug
function toggleDebug() {
const debugDiv = document.getElementById('debugInfo');
debugDiv.classList.toggle('show-debug');
}
function addDebug(message, data = null) {
const debugDiv = document.getElementById('debugInfo');
const timestamp = new Date().toLocaleTimeString();
let html = `<div><strong>[${timestamp}]</strong> ${message}`;
if (data) {
html += `<pre>${JSON.stringify(data, null, 2)}</pre>`;
}
html += '</div>';
debugDiv.innerHTML = html + debugDiv.innerHTML;
if (debugDiv.children.length > 10) {
debugDiv.removeChild(debugDiv.lastChild);
}
console.log(`[${timestamp}]`, message, data);
}
async function fetchData() {
try {
updateStatus('Mengambil data...', 'info');
const cacheBuster = `&_=${new Date().getTime()}`;
const urlWithCache = CSV_URL + cacheBuster;
addDebug('Mengambil data dari: ' + urlWithCache);
const response = await fetch(urlWithCache);
if (!response.ok) throw new Error('Gagal mengambil data: ' + response.status);
const csvText = await response.text();
addDebug('CSV berhasil diambil, panjang: ' + csvText.length + ' karakter');
// Tampilkan 3 baris pertama CSV untuk debugging
const firstLines = csvText.split('\n').slice(0, 4).join('\n');
addDebug('3 baris pertama CSV:\n' + firstLines);
const parsed = Papa.parse(csvText, {
header: true,
skipEmptyLines: true,
transformHeader: function(header) {
// Biarkan header apa adanya (sekarang lowercase)
return header.trim();
}
});
if (parsed.errors.length > 0) {
addDebug('Parse errors:', parsed.errors);
}
addDebug('Data parsed, jumlah baris: ' + parsed.data.length);
addDebug('Header yang ditemukan:', parsed.meta.fields);
// Tampilkan contoh data pertama (jika ada)
if (parsed.data.length > 0) {
addDebug('Contoh data pertama:', parsed.data[0]);
// Cek apakah data memiliki nilai
const firstRow = parsed.data[0];
addDebug('Nilai temp:', firstRow['temp']);
addDebug('Nilai humidity:', firstRow['humidity']);
addDebug('Nilai distance:', firstRow['distance']);
addDebug('Nilai soil:', firstRow['soil']);
}
const data = parsed.data;
if (data.length === 0) {
addDebug('Tidak ada data setelah parsing');
updateStatus('Tidak ada data di spreadsheet', 'error');
return;
}
// Filter baris yang memiliki data sensor
const validData = data.filter(row => {
// Cek apakah ada nilai di kolom temp, humidity, distance, atau soil
const hasTemp = row['temp'] && row['temp'].toString().trim() !== '';
const hasHumidity = row['humidity'] && row['humidity'].toString().trim() !== '';
const hasDistance = row['distance'] && row['distance'].toString().trim() !== '';
const hasSoil = row['soil'] && row['soil'].toString().trim() !== '';
return hasTemp || hasHumidity || hasDistance || hasSoil;
});
addDebug('Data valid: ' + validData.length + ' baris');
if (validData.length === 0) {
addDebug('Tidak ada data valid');
updateStatus('Tidak ada data sensor yang valid', 'error');
return;
}
// Simpan semua data
allSensorData = validData;
// Ambil nilai sensor terbaru untuk grid
const latestRow = validData[validData.length - 1];
addDebug('Data terbaru:', latestRow);
const sensorValues = {
suhu: cleanNumericValue(latestRow['temp']),
kelembaban: cleanNumericValue(latestRow['humidity']),
jarak: cleanNumericValue(latestRow['distance']),
soil: cleanNumericValue(latestRow['soil'])
};
addDebug('Nilai sensor terbaru:', sensorValues);
// Update sensor grid
updateSensorGrid(sensorValues);
// Update chart dengan jumlah data yang dipilih
updateChartWithData();
updateStatus('✅ Data berhasil diperbarui (' + validData.length + ' baris data)', 'success');
document.getElementById('lastUpdate').textContent =
`Terakhir diperbarui: ${new Date().toLocaleString('id-ID')}`;
} catch (error) {
console.error('Error:', error);
updateStatus('❌ Gagal mengambil data: ' + error.message, 'error');
addDebug('ERROR: ' + error.message, error);
}
}
function updateStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
statusDiv.className = 'status';
if (type === 'error') {
statusDiv.classList.add('error');
} else if (type === 'success') {
statusDiv.style.backgroundColor = '#e8f5e8';
statusDiv.style.color = '#2e7d32';
} else {
statusDiv.style.backgroundColor = '#e3f2fd';
statusDiv.style.color = '#0d47a1';
}
}
function startFetching() {
// Load preferensi jumlah data
const savedCount = localStorage.getItem('displayDataCount');
if (savedCount) {
displayDataCount = parseInt(savedCount);
}
// Buat tombol data count
createDataCountButtons();
// Mulai fetch data
fetchData();
setInterval(fetchData, 5000); // waktu update
}
window.addEventListener('load', startFetching);
</script>
</body>
</html>