From 83af86cde4c37f99f147789ac9a5ee6d34b191fa Mon Sep 17 00:00:00 2001 From: developer Date: Tue, 28 Jul 2026 13:29:36 +0700 Subject: [PATCH] Initial commit projek TA Aquascape --- aquascape/dashboard.php | 0 assets/css/custom.css | 950 ++++++++++++++++++++++++++++++++++++++++ assets/js/chart.js | 147 +++++++ assets/js/node-red.js | 110 +++++ catatan.php | 346 +++++++++++++++ config/firebase.php | 10 + dashboard.php | 138 ++++++ forgot.php | 50 +++ functions/auth.php | 93 ++++ index.php | 54 +++ kontrol.php | 583 ++++++++++++++++++++++++ login.php | 48 ++ logout.php | 8 + monitoring.php | 202 +++++++++ partials/footer.php | 7 + partials/header.php | 14 + partials/navbar.php | 13 + partials/sidebar.php | 59 +++ register.php | 51 +++ 19 files changed, 2883 insertions(+) create mode 100644 aquascape/dashboard.php create mode 100644 assets/css/custom.css create mode 100644 assets/js/chart.js create mode 100644 assets/js/node-red.js create mode 100644 catatan.php create mode 100644 config/firebase.php create mode 100644 dashboard.php create mode 100644 forgot.php create mode 100644 functions/auth.php create mode 100644 index.php create mode 100644 kontrol.php create mode 100644 login.php create mode 100644 logout.php create mode 100644 monitoring.php create mode 100644 partials/footer.php create mode 100644 partials/header.php create mode 100644 partials/navbar.php create mode 100644 partials/sidebar.php create mode 100644 register.php diff --git a/aquascape/dashboard.php b/aquascape/dashboard.php new file mode 100644 index 0000000..e69de29 diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..e49e7bf --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,950 @@ +/* Reset Basic */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background-color: #f4f7f6; + color: #333; +} + +/* Warna Utama */ +.bg-tosca { + background-color: #5FBDBD; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Navbar */ +.navbar { + display: flex; + justify-content: space-between; + padding: 15px 30px; + background: #e0f2f1; /* Sedikit terang menyesuaikan gambar */ + font-weight: bold; +} +.navbar .logo { + font-weight: 900; + font-size: 1.2rem; + color: #333; +} +.navbar .nav-links a { + text-decoration: none; + color: #333; + margin-left: 20px; + font-size: 14px; + transition: color 0.3s; +} +.navbar .nav-links a:hover { + color: #5FBDBD; +} + +/* Hero Section */ +.hero { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + color: white; + font-size: 2.5rem; + font-style: italic; + font-weight: 600; +} + +/* Cards (Umum) */ +.card { + background: white; + padding: 30px; + border-radius: 15px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +/* Auth Pages (Login/Register) */ +.auth-container { + display: flex; + justify-content: center; + align-items: center; + height: calc(100vh - 60px); /* Kurangi tinggi navbar */ + background-color: transparent; +} + +.auth-card { + width: 320px; + text-align: center; + border-radius: 20px; +} +.auth-card h2 { + margin-bottom: 25px; + font-weight: 800; +} +.auth-card input { + width: 100%; + padding: 12px; + margin-bottom: 15px; + border: none; + background: #e0e0e0; + border-radius: 20px; + outline: none; + text-align: center; + font-weight: 500; +} +.auth-card button { + padding: 10px 40px; + border: none; + background: #ddd; + border-radius: 20px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s; +} +.auth-card button:hover { + background: #ccc; +} +.message { + font-size: 12px; + margin-bottom: 15px; +} +.msg-error { color: red; } +.msg-success { color: green; } + +/* Dashboard Layout */ +.dashboard-layout { + display: flex; + height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 250px; + background: #e0f2f1; + padding: 20px; +} +.sidebar h3 { + margin-bottom: 30px; + font-size: 1.1rem; + font-weight: 800; + letter-spacing: 1px; +} +.sidebar ul { + list-style: none; +} +.sidebar ul li { + margin-bottom: 15px; +} +.sidebar ul li a { + text-decoration: none; + color: #555; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; +} +.sidebar ul li.active a { + color: #333; + font-weight: bold; + /* Indikator aktif mirip gambar */ + position: relative; +} + +/* Main Content */ +.main-content { + flex: 1; + background: #5FBDBD; + padding: 30px; + overflow-y: auto; +} + +/* Sensor Cards */ +.cards-grid { + display: flex; + gap: 20px; + margin-bottom: 20px; + flex-wrap: wrap; + justify-content: center; +} +.sensor-card { + background: #e0f2f1; + border-radius: 15px; + padding: 20px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 150px; + height: 150px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} +.sensor-val { + font-size: 1.2rem; + font-weight: bold; + background: white; + border-radius: 50%; + width: 70px; + height: 70px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 10px; +} + +/* Chart Container */ +.chart-container { + background: white; + border-radius: 15px; + padding: 20px; + height: 300px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + margin: 20px auto; + max-width: 600px; +} + +/* Control Settings */ +.controls-container { + background: #e0f2f1; + border-radius: 15px; + padding: 20px; + margin-top: 20px; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +/* Toggle Switch */ +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} +.switch input { + opacity: 0; + width: 0; + height: 0; +} +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + transition: .4s; + border-radius: 50%; +} +input:checked + .slider { + background-color: #333; +} +input:checked + .slider:before { + transform: translateX(20px); +} + +/* ==== TAMBAHAN CSS SESUAI GAMBAR UI ==== */ + +/* Dashboard Menu Cards (Gambar 1) */ +.menu-cards-grid { + display: flex; + gap: 20px; + justify-content: center; + margin-bottom: 30px; +} +.menu-card-link { + background: #e0f2f1; + border-radius: 15px; + padding: 20px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 180px; + height: 140px; + text-decoration: none; + color: #333; + font-weight: bold; + font-size: 14px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + transition: transform 0.2s; +} +.menu-card-link:hover { + transform: scale(1.05); +} +.menu-card-link .icon-box { + font-size: 40px; + margin-bottom: 10px; +} + +/* Monitoring Parameter (Gambar 2) */ +.mon-top-grid { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 30px; +} +.mon-circle-card { + background: #9bdcd5; + border-radius: 20px; + padding: 20px; + width: 200px; + text-align: center; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} +.mon-circle-card .circle-val { + background: white; + border-radius: 50%; + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 15px auto; + font-weight: bold; + font-size: 1.1rem; + color: #333; +} +.mon-circle-card .title { + font-weight: 800; + font-size: 1.1rem; + color: #222; +} + +.mon-bot-grid { + display: flex; + flex-direction: column; + grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); + align-items: center; + gap: 20px; +} +.mon-pill-card { + background: #9bdcd5; + border-radius: 20px; + padding: 15px 30px; + width: 350px; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: bold; + font-size: 1.2rem; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +/* Sistem Kontrol (Gambar 3) */ +.ctrl-main-grid { + display: flex; + justify-content: center; + gap: 30px; + margin-top: 20px; +} +.ctrl-box { + background: #e0f2f1; + border-radius: 20px; + padding: 25px; + width: 450px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} +.ctrl-box-title { + background: white; + border-radius: 20px; + padding: 10px; + text-align: center; + font-weight: 800; + margin-bottom: 25px; +} +.ctrl-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + font-weight: bold; + font-size: 14px; +} +.ctrl-item input[type="text"] { + width: 60px; + padding: 5px; + border-radius: 5px; + border: none; + text-align: center; + font-weight: bold; +} +.ctrl-time { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-bottom: 20px; +} +.ctrl-time input { + background: white; + border: none; + padding: 10px 15px; + border-radius: 20px; + font-weight: bold; + width: 80px; + text-align: center; +} +.btn-mulai { + background: #e8d5d5; + border: none; + padding: 8px 30px; + border-radius: 15px; + font-weight: bold; + cursor: pointer; + display: block; + margin: 0 auto; + transition: 0.2s; +} +.btn-mulai:hover { + background: #d4baba; +} + +/* Catatan Table (Gambar 4) */ +.table-container { + background: #9bdcd5; + border-radius: 15px; + padding: 20px; + margin-top: 20px; +} +.catatan-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 10px; + overflow: hidden; +} +.catatan-table th, .catatan-table td { + padding: 12px; + text-align: center; + border-bottom: 1px solid #eee; + font-size: 14px; +} +.catatan-table th { + background: #fdfdfd; + font-weight: bold; +} +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 15px; + padding: 0 10px; + font-size: 14px; + font-weight: bold; +} +.pagination-controls button { + background: #333; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + margin-left: 5px; +} +.dashboard-layout { + display: flex; +} + +.dashboard-layout { + display: flex; +} + +/* Sidebar collapse */ +.sidebar.hide { + width: 80px; + padding: 20px 10px; +} + +/* Hilangkan teks menu */ +.sidebar.hide .text { + display: none; +} + +/* Hilangkan tulisan DASHBOARD */ +.sidebar.hide .sidebar-title { + display: none; +} + +/* Biar tombol tetap keliatan & ke tengah */ +.sidebar.hide .sidebar-header { + justify-content: center; +} + +/* ICON tetap center */ +.sidebar.hide ul li a { + justify-content: center; +} + + +/* Background aquarium */ +.aquarium { + position: relative; + height: 100vh; + overflow: hidden; + background: linear-gradient(to bottom, #0099cc, #003366); +} + +/* Hero */ +.hero { + position: relative; + z-index: 2; + text-align: center; + padding-top: 120px; + color: white; + font-size: 2rem; +} + +/* IKAN */ +.fish { + position: absolute; + font-size: 40px; + animation: swim linear infinite; +} + + +.fish1 { top: 30%; animation-duration: 12s; } +.fish2 { top: 50%; animation-duration: 18s; } +.fish3 { top: 70%; animation-duration: 20s; } + +@keyframes swim { + 0% { + left: -100px; + transform: scaleX(-1); /* dibalik jadi hadap kanan */ + } + 100% { + left: 110%; + transform: scaleX(-1); /* tetap hadap kanan */ + } +} + + +.shark { + position: absolute; + top: 40%; + font-size: 80px; + animation: swimShark 25s linear infinite; +} + +@keyframes swimShark { + 0% { + right: -150px; + transform: scaleX(1); /* hadap kiri (default emoji) */ + } + 100% { + right: 110%; + transform: scaleX(1); + } +} + +.stingray { + position: absolute; + bottom: 20%; + font-size: 70px; + animation: swimRay 30s linear infinite; + opacity: 0.9; +} + +@keyframes swimRay { + 0% { + right: -150px; + transform: scaleX(1); /* hadap kiri (default emoji) */ + } + 100% { + right: 110%; + transform: scaleX(1); + } +} + +.jellyfish { + position: absolute; + font-size: 50px; + animation: floatUpDown ease-in-out infinite; +} +.j1 { + left: 20%; + animation-duration: 6s; +} + +.j2 { + left: 50%; + animation-duration: 8s; +} + +.j3 { + left: 75%; + animation-duration: 7s; +} +@keyframes floatUpDown { + 0% { + top: 20%; + } + 50% { + top: 60%; + } + 100% { + top: 20%; + } +} + + +/* GELEMBUNG */ +.bubble { + position: absolute; + bottom: -50px; + width: 12px; + height: 12px; + background: rgba(255, 255, 255, 0.8); /* lebih terang */ + border-radius: 50%; + box-shadow: 0 0 10px rgba(255,255,255,0.8); /* glow biar keliatan */ + animation: rise linear infinite; +} + + +.bubble1 { left: 20%; animation-duration: 5s; } +.bubble2 { left: 40%; animation-duration: 3s; } +.bubble3 { left: 20%; animation-duration: 5s; } +.bubble4 { left: 80%; animation-duration: 4s; } +.bubble5 { left: 20%; animation-duration: 5s; } +.bubble6 { left: 40%; animation-duration: 3s; } +.bubble7 { left: 20%; animation-duration: 5s; } +.bubble8 { left: 80%; animation-duration: 4s; } +.bubble9 { left: 20%; animation-duration: 5s; } +.bubble10 { left: 80%; animation-duration: 4s; } + + +@keyframes rise { + 0% { + transform: translateY(0) scale(1); + opacity: 0.7; + } + 100% { + transform: translateY(-100vh) scale(1.5); + opacity: 0; + } +} + +/* TERUMBU KARANG */ +.coral { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + font-size: 150px; /* dibesarin */ + text-align: left; + padding-left: 20px; +} +.coral-right { + position: absolute; + bottom: 0; + right: 10px; + font-size: 150px; + transform: rotate(-5deg); /* sedikit miring biar natural */ +} +.hero h1 { + font-family: 'Baloo 2', cursive; + font-size: 60px; + color: #aefeff; /* warna aqua */ + +} +.bg-login { + min-height: 100vh; + display: flex; + flex-direction: column; + + background: linear-gradient(to bottom, #0099cc, #003366); +} +.auth-container { + display: flex; + justify-content: center; + align-items: center; + flex: 1; +} + +.auth-card { + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(12px); + border-radius: 20px; + padding: 35px; + width: 320px; + color: white; + + border: 1px solid rgba(255,255,255,0.2); + + box-shadow: + 0 10px 30px rgba(0,0,0,0.3); + + animation: fadeIn 0.8s ease; +} +.auth-card h2 { + text-align: center; + margin-bottom: 20px; + font-size: 28px; + color: #ffffff; +} +.auth-card input { + width: 100%; + padding: 12px; + margin-bottom: 15px; + border-radius: 12px; + border: none; + outline: none; + + background: rgba(255,255,255,0.2); + color: white; +} + +.auth-card input::placeholder { + color: #eee; +} +.auth-card button { + width: 100%; + padding: 12px; + border-radius: 12px; + border: none; + + background: linear-gradient(45deg, #00c9ff, #92fe9d); + color: black; + font-weight: bold; + + cursor: pointer; + transition: 0.3s; +} + +.auth-card button:hover { + transform: scale(1.05); +} +.forgot-password { + display: block; + text-align: center; + font-size: 12px; + color: white; + text-decoration: none; + margin-bottom: 15px; + opacity: 0.8; +} + +.forgot-password:hover { + opacity: 1; + text-decoration: underline; +} +/* item dasar */ +.sidebar ul li a { + padding: 10px 12px; + border-radius: 10px; + transition: all 0.3s ease; +} + +/* hover */ +.sidebar ul li a:hover { + background: rgba(0, 0, 0, 0.08); +} + +/* ACTIVE MENU */ +.sidebar ul li.active a { + background: #5FBDBD; + color: white; + font-weight: bold; + border-radius: 10px; +} + + +/* ICON ikut berubah */ +.sidebar ul li.active i { + color: #000; +} +.sidebar ul li { + position: relative; +} + +.sidebar ul li.active::before { + content: ""; + position: absolute; + left: -20px; + top: 0; + height: 100%; + width: 5px; + background: #00c9ff; + border-radius: 0 5px 5px 0; +} +.sidebar ul li a { + padding: 10px; + transition: 0.3s; +} + +.sidebar ul li a:hover { + background: rgba(0,0,0,0.1); +} +.sidebar ul li a { + padding: 10px; + border-radius: 10px; + transition: 0.3s; +} + +.sidebar ul li a:hover { + background: #cceeee; +} +/* Header sidebar saat collapse */ +.sidebar.hide .sidebar-header { + display: flex; + justify-content: center; + align-items: center; +} + +/* Biar tombol sama posisi icon menu */ +#toggleSidebar { + display: flex; + justify-content: center; + align-items: center; + width: 50px; + height: 50px; +} + +/* Biar icon menu konsisten */ +.sidebar ul li i { + width: 40px; + text-align: center; + font-size: 18px; +} +.sidebar { + width: 250px; + transition: width 0.3s ease; +} + +/* teks fade out */ +.sidebar .text { + transition: opacity 0.2s ease; +} + +.sidebar.hide .text { + opacity: 0; +} + +/* judul juga fade */ +.sidebar-title { + transition: opacity 0.2s ease; +} + +.sidebar.hide .sidebar-title { + opacity: 0; +} +.sidebar ul li a { + transition: all 0.2s ease; +} + +.sidebar ul li a:hover { + transform: scale(1.05); +} +/* Animasi masuk halaman */ +.page-enter { + opacity: 0; + transform: translateY(20px); + animation: fadeSlideIn 0.6s ease forwards; +} + +@keyframes fadeSlideIn { + to { + opacity: 1; + transform: translateY(0); + } +} +.menu-card-link { + opacity: 0; + transform: translateY(20px); + animation: fadeSlideIn 0.5s ease forwards; +} + +/* delay tiap card */ +.menu-card-link:nth-child(1) { animation-delay: 0.1s; } +.menu-card-link:nth-child(2) { animation-delay: 0.2s; } +.menu-card-link:nth-child(3) { animation-delay: 0.3s; } +.topbar-icons { + display: flex; + gap: 20px; + align-items: center; + color: white; + font-size: 18px; +} + +.notif, .user { + position: relative; + cursor: pointer; +} + +.notif-dropdown, .user-dropdown { + display: none; + position: absolute; + top: 45px; + right: 0; + + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(15px); + + color: white; + width: 240px; + border-radius: 16px; + padding: 15px; + + box-shadow: + 0 10px 25px rgba(0,0,0,0.3), + inset 0 0 10px rgba(255,255,255,0.1); + + border: 1px solid rgba(255,255,255,0.2); + + animation: dropdown3D 0.3s ease; + z-index: 999; +} + + +.user-dropdown { + width: 260px; /* diperlebar */ +} + + +/* aktif */ +.show { + display: block; +} + +/* animasi */ +@keyframes dropdown3D { + from { + opacity: 0; + transform: translateY(-10px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} +/* KHUSUS NOTIF */ +.notif-dropdown { + width: 280px; /* diperlebar */ +} + +/* KHUSUS USER */ +.user-dropdown { + width: 260px; /* biar email muat */ +} +.notif-dropdown ul { + list-style: none; /* INI KUNCINYA */ + padding: 0; + margin: 0; +} +.notif-dropdown li { + padding: 8px 12px; + text-align: left; +} +.notif-dropdown li::before { + content: "πŸ””"; + margin-right: 8px; +} diff --git a/assets/js/chart.js b/assets/js/chart.js new file mode 100644 index 0000000..d283449 --- /dev/null +++ b/assets/js/chart.js @@ -0,0 +1,147 @@ +document.addEventListener('DOMContentLoaded', async function () { + const ctx = document.getElementById('phChart'); + if (!ctx) return; + + const NODERED_URL = (window.NODERED_URL || 'https://node-red.aquamonn.online').replace(/\/$/, ''); + + const chart = new Chart(ctx, { + type: 'line', + data: { + labels: [], + datasets: [ + { + label: 'pH', + data: [], + borderColor: '#2980b9', + backgroundColor: 'rgba(41, 128, 185, 0.15)', + tension: 0.3, + fill: true, + pointRadius: 3, + yAxisID: 'yPh' + }, + { + label: 'Suhu (Β°C)', + data: [], + borderColor: '#e67e22', + backgroundColor: 'rgba(230, 126, 34, 0.10)', + tension: 0.3, + fill: false, + pointRadius: 3, + yAxisID: 'ySuhu' + }, + { + label: 'Amonia (ppm)', + data: [], + borderColor: '#27ae60', + backgroundColor: 'rgba(39, 174, 96, 0.10)', + tension: 0.3, + fill: false, + pointRadius: 3, + yAxisID: 'yAmonia' + }, + { + label: 'Distance (cm)', + data: [], + borderColor: '#3b27ae', + backgroundColor: 'rgba(59, 39, 174, 0.10)', + tension: 0.3, + fill: false, + pointRadius: 3, + yAxisID: 'ydistance' + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { display: true, labels: { color: '#000000', font: { size: 12 } } }, + title: { + display: true, + text: 'Grafik Sensor Realtime', + color: '#0b0b0b', + font: { size: 14, weight: 'bold' } + } + }, + scales: { + x: { + ticks: { color: '#ccc', font: { size: 10 }, maxTicksLimit: 10 }, + grid: { color: 'rgba(255,255,255,0.08)' } + }, + yPh: { + type: 'linear', + position: 'left', + title: { display: true, text: 'pH', color: '#2980b9' }, + ticks: { color: '#2980b9' }, + grid: { color: 'rgba(255,255,255,0.08)' }, + min: 0, max: 14 + }, + ySuhu: { + type: 'linear', + position: 'right', + title: { display: true, text: 'Β°C', color: '#e67e22' }, + ticks: { color: '#e67e22' }, + grid: { drawOnChartArea: false }, + min: 20, max: 40 + }, + yAmonia: { + type: 'linear', + position: 'right', + title: { display: true, text: 'ppm', color: '#27ae60' }, + ticks: { color: '#27ae60' }, + grid: { drawOnChartArea: false }, + min: 0, max: 10, + offset: true + }, + ydistance: { + type: 'linear', + position: 'right', + title: { display: true, text: 'cm', color: '#3b27ae' }, + ticks: { color: '#2927ae' }, + grid: { drawOnChartArea: false }, + min: 0, max: 30, + offset: true + } + } + } + }); + + async function loadChartData() { + try { + const res = await fetch(NODERED_URL + '/get_history', { cache: 'no-store' }); + if (!res.ok) throw new Error('HTTP ' + res.status); + const arr = await res.json(); + + if (!Array.isArray(arr) || arr.length === 0) { + chart.options.plugins.title.text = 'Grafik Sensor β€” Menunggu data…'; + chart.update(); + return; + } + + const slice = arr.slice(0, 20).reverse(); + + chart.data.labels = slice.map(r => { + if (r.jam) return r.jam.substring(0, 5); + const d = new Date(r.timestamp ?? 0); + return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); + }); + + chart.data.datasets[0].data = slice.map(r => parseFloat(r.ph ?? r.pH ?? 0)); + chart.data.datasets[1].data = slice.map(r => parseFloat(r.suhu ?? r.temperature ?? 0)); + chart.data.datasets[2].data = slice.map(r => parseFloat(r.amonia ?? 0)); + chart.data.datasets[3].data = slice.map(r => parseFloat(r.distance ?? 0)); + + chart.options.plugins.title.text = 'Grafik Sensor Realtime '; + chart.update(); + + } catch(e) { + chart.options.plugins.title.text = 'Grafik β€” Gagal: ' + e.message; + chart.update(); + console.warn('Chart error:', e.message); + } + } + + await loadChartData(); + setInterval(loadChartData, 30000); +}); \ No newline at end of file diff --git a/assets/js/node-red.js b/assets/js/node-red.js new file mode 100644 index 0000000..199e77b --- /dev/null +++ b/assets/js/node-red.js @@ -0,0 +1,110 @@ + + +(function () { + "use strict"; + + const POLL_MS = 3000; + const DATA_URL = (window.NODERED_URL || "").replace(/\/$/, "") + "/get-data"; + + const SENSORS = { + suhu: { valId:"val-suhu", barId:"bar-suhu", unit:"", decimals:1, max:45, color: v=>(v>32||v<18)?"#f59e0b":"#22c55e" }, + ph: { valId:"val-ph", barId:"bar-ph", unit:" pH", decimals:2, max:14, color: v=>(v<6.5||v>8.5)?"#ef4444":"#22c55e" }, + amonia: { valId:"val-amonia", barId:"bar-amonia", unit:" ppm", decimals:2, max:10, color: v=>v>2?"#ef4444":v>1?"#f59e0b":"#22c55e" }, + kekeruhan: { valId:"val-kekeruhan", barId:"bar-kekeruhan", unit:" NTU", decimals:1, max:150, color: v=>v>80?"#ef4444":v>40?"#f59e0b":"#22c55e" }, + distance: { valId:"val-distance", barId:"bar-distance", unit:" cm", decimals:1, max:100, color: v=>v>70?"#ef4444":v>50?"#f59e0b":"#22c55e" }, + }; + + function updateSensor(key, raw) { + const cfg = SENSORS[key]; + if (!cfg || raw===null || raw===undefined) return; + const v = parseFloat(raw); + if (isNaN(v)) return; + const valEl = document.getElementById(cfg.valId); + const barEl = document.getElementById(cfg.barId); + if (valEl) valEl.textContent = v.toFixed(cfg.decimals) + cfg.unit; + if (barEl) { + barEl.style.width = Math.min(100, Math.max(0, v/cfg.max*100)) + "%"; + barEl.style.background = cfg.color(v); + } + } + + function updateDevice(badgeId, valId, raw) { + const valEl = document.getElementById(valId); + const badgeEl = document.getElementById(badgeId); + if (!valEl || !badgeEl || raw===null || raw===undefined) return; + const on = (raw==1 || String(raw).toUpperCase()==="ON"); + valEl.textContent = on ? "ON" : "OFF"; + badgeEl.classList.toggle("on", on); + badgeEl.classList.toggle("off", !on); + } + + const RULES = [ + {key:"amonia", label:"Amonia", dangerHi:2, warnHi:1 }, + {key:"suhu", label:"Suhu", dangerHi:33, warnHi:30, dangerLo:17, warnLo:20 }, + {key:"ph", label:"pH", dangerLo:6.0, warnLo:6.5,dangerHi:9.0,warnHi:8.5}, + {key:"kekeruhan", label:"Kekeruhan", dangerHi:80, warnHi:40 }, + {key:"distance", label:"Distance", dangerHi:70, warnHi:50 } + ]; + + function checkThresholds(data) { + const notifs = []; + RULES.forEach(r => { + const v = parseFloat(data[r.key]); + if (isNaN(v)) return; + if ((r.dangerHi!==undefined&&v>=r.dangerHi)||(r.dangerLo!==undefined&&v<=r.dangerLo)) + notifs.push({level:"danger", msg:`⚠️ ${r.label} KRITIS: ${v.toFixed(2)}`}); + else if ((r.warnHi!==undefined&&v>=r.warnHi)||(r.warnLo!==undefined&&v<=r.warnLo)) + notifs.push({level:"warn", msg:`πŸ”” ${r.label} Abnormal: ${v.toFixed(2)}`}); + }); + const badge = document.querySelector(".notif-badge"); + const listEl = document.querySelector("#notifDropdown ul"); + if (!listEl) return; + if (!notifs.length) { + if (badge) badge.textContent = "0"; + listEl.innerHTML = "
  • Semua normal βœ”
  • "; + } else { + if (badge) badge.textContent = notifs.length; + listEl.innerHTML = notifs.map(n=> + `
  • ${n.msg}
  • ` + ).join(""); + } + } + + async function fetchData() { + try { + const res = await fetch(DATA_URL, {cache:"no-store"}); + if (!res.ok) throw new Error("HTTP "+res.status); + const data = await res.json(); + + // normalize: turbidity β†’ kekeruhan, pH β†’ ph + const d = { + suhu: data.suhu ?? data.temperature ?? null, + ph: data.ph ?? data.pH ?? null, + amonia: data.amonia ?? null, + kekeruhan: data.kekeruhan ?? data.turbidity ?? null, + relay: data.relay ?? null, + lampu: data.lampu ?? null, + distance: data.distance ?? null + }; + + Object.keys(SENSORS).forEach(k => updateSensor(k, d[k])); + updateDevice("badge-relay", "val-relay", d.relay); + updateDevice("badge-lampu", "val-lampu", d.lampu); + checkThresholds(d); + + if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(true); + } catch(err) { + console.warn("[node-red.js]", err.message); + if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(false); + } + } + + if (!window.NODERED_URL) { + console.error("[node-red.js] window.NODERED_URL belum diset di monitoring.php!"); + return; + } + + fetchData(); + setInterval(fetchData, POLL_MS); + +})(); \ No newline at end of file diff --git a/catatan.php b/catatan.php new file mode 100644 index 0000000..2b02658 --- /dev/null +++ b/catatan.php @@ -0,0 +1,346 @@ + + +
    + + +
    + +
    +
    +
    + + 0 +
    +

    Notifikasi

    +
    • Memuat data…
    +
    +
    +
    + +
    + + Logout +
    +
    +
    +
    + + +
    + + + + +
    + +
    +
    Laporan
    + + + + + + + + + + + + + + + + + + +
    TanggalWaktuAmoniaKekeruhanSuhuTinggi AirJarak sensorpH
    ⏳ Memuat…
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/config/firebase.php b/config/firebase.php new file mode 100644 index 0000000..4319284 --- /dev/null +++ b/config/firebase.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..9e714da --- /dev/null +++ b/dashboard.php @@ -0,0 +1,138 @@ + + +
    + + +
    + + +
    +

    DASHBOARD

    +
    + + +
    + + 0 +
    +

    Notifikasi

    +
    • Memuat…
    +
    +
    + + +
    + +
    + + Logout +
    +
    + +
    +
    + + + + + +
    + +
    + +
    +
    + + + + + + + + \ No newline at end of file diff --git a/forgot.php b/forgot.php new file mode 100644 index 0000000..95d5bf3 --- /dev/null +++ b/forgot.php @@ -0,0 +1,50 @@ + + +
    +
    +
    +

    Lupa Password

    + +
    + + +
    + + + Kembali ke Login + +
    +
    +
    + + + + + diff --git a/functions/auth.php b/functions/auth.php new file mode 100644 index 0000000..9db3a1f --- /dev/null +++ b/functions/auth.php @@ -0,0 +1,93 @@ +"; + echo "Error: File firebase.php tidak ditemukan!
    "; + echo "Dicari di: $configPath"; + echo ""; + die(); +} + +function isLoggedIn() { + return isset($_SESSION['user_token']); +} + +function registerUser($username, $email, $password) { + // PROTEKSI: Cek apakah cURL dimuat oleh web server + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'Modul cURL tidak aktif di browser. Buka XAMPP Control Panel, klik Stop pada Apache, lalu Start kembali. Pastikan php.ini yang diedit benar.' + ]; + } + + $url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" . FIREBASE_API_KEY; + $data = json_encode(['email' => $email, 'password' => $password, 'returnSecureToken' => true]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + + if ($err) return ['success' => false, 'error' => 'Gagal koneksi: ' . $err]; + + $result = json_decode($response, true); + + if (isset($result['idToken'])) { + return ['success' => true]; + } else { + $message = $result['error']['message'] ?? 'Gagal mendaftar'; + if ($message == 'EMAIL_EXISTS') $message = 'Email sudah terdaftar.'; + if ($message == 'WEAK_PASSWORD') $message = 'Password terlalu lemah.'; + return ['success' => false, 'error' => $message]; + } +} + +function loginUser($email, $password) { + // PROTEKSI: Cek apakah cURL dimuat oleh web server + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'Fitur cURL tidak terbaca oleh Apache. Silakan RESTART Apache di XAMPP Control Panel.' + ]; + } + + $url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=" . FIREBASE_API_KEY; + $data = json_encode(['email' => $email, 'password' => $password, 'returnSecureToken' => true]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + $result = json_decode($response, true); + curl_close($ch); + + if (isset($result['idToken'])) { + $_SESSION['user_token'] = $result['idToken']; + $_SESSION['email'] = $result['email']; + return ['success' => true]; + } else { + $message = $result['error']['message'] ?? 'Login Gagal'; + if ($message == 'INVALID_LOGIN_CREDENTIALS') $message = 'Email atau password salah.'; + return ['success' => false, 'error' => $message]; + } +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..2ee1454 --- /dev/null +++ b/index.php @@ -0,0 +1,54 @@ + + +
    + + + +
    + + + +
    +

    Welcome to My Aquascape

    +
    + + +
    🐟
    +
    🐠
    +
    🐑
    +
    🦈
    +
    πŸ‹
    +
    πŸͺΌ
    +
    πŸͺΌ
    +
    πŸͺΌ
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    πŸͺΈπŸͺΈπŸͺΈ
    +
    🌿πŸͺΈπŸš
    + +
    + + + + + + + + \ No newline at end of file diff --git a/kontrol.php b/kontrol.php new file mode 100644 index 0000000..b047eb5 --- /dev/null +++ b/kontrol.php @@ -0,0 +1,583 @@ + + +
    + + +
    + + + +
    + + +
    + + +
    +
    +
    + + Menghubungkan… + +
    + + + + + + +
    +
    +
    + + 0 +
    +

    Notifikasi

    +
    • Memuat data…
    +
    +
    +
    + +
    +

    + Logout +
    +
    +
    +
    + + +
    + + +
    +
    Sistem Otomatis
    + + +
    + Batas Amonia + + +
    + +
    + Batas Kekeruhan + + +
    + +
    + Batas Tinggi Air + + +
    + +
    + Tinggi Aquarium + + +
    + + +
    + +
    +
    + + +
    +
    Set Lampu
    + +
    + + β€” + +
    + +
    + +
    + + +
    + Lampu + + OFF +
    +
    + +
    + +
    +
    + + + + + + + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..7d64d92 --- /dev/null +++ b/login.php @@ -0,0 +1,48 @@ + + + + + \ No newline at end of file diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..f2ca65e --- /dev/null +++ b/logout.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/monitoring.php b/monitoring.php new file mode 100644 index 0000000..a641860 --- /dev/null +++ b/monitoring.php @@ -0,0 +1,202 @@ + + + + +
    + + +
    + + +
    +
    +
    + + Menghubungkan… + +
    + + + + + + +
    +
    +
    + + 0 +
    +

    Notifikasi

    +
    • Memuat data…
    +
    +
    +
    + +
    +

    + Logout +
    +
    +
    +
    + + +
    +
    +
    β€” mg/L +
    +
    Amonia
    +
    +
    +
    +
    +
    +
    β€” cm
    +
    Distance
    +
    +
    +
    +
    +
    + +
    β€” NTU
    +
    Kekeruhan
    +
    +
    +
    +
    +
    + + +
    + +
    + ☁️ +
    + β€”
    + Celcius +
    +
    +
    +
    +
    + +
    +
    + β€” pH +
    +
    +
    +
    + πŸ’§ +
    +
    + +
    + + + + + + + + \ No newline at end of file diff --git a/partials/footer.php b/partials/footer.php new file mode 100644 index 0000000..e4d25b5 --- /dev/null +++ b/partials/footer.php @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/partials/header.php b/partials/header.php new file mode 100644 index 0000000..6c04143 --- /dev/null +++ b/partials/header.php @@ -0,0 +1,14 @@ + + + + + + Aquascape IoT Dashboard + + + + + + + + \ No newline at end of file diff --git a/partials/navbar.php b/partials/navbar.php new file mode 100644 index 0000000..896a2c5 --- /dev/null +++ b/partials/navbar.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/partials/sidebar.php b/partials/sidebar.php new file mode 100644 index 0000000..ca620d9 --- /dev/null +++ b/partials/sidebar.php @@ -0,0 +1,59 @@ + + + diff --git a/register.php b/register.php new file mode 100644 index 0000000..51a3706 --- /dev/null +++ b/register.php @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file