Initial commit projek TA Aquascape
This commit is contained in:
commit
83af86cde4
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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 = "<li style='color:rgba(255,255,255,.5)'>Semua normal ✔</li>";
|
||||
} else {
|
||||
if (badge) badge.textContent = notifs.length;
|
||||
listEl.innerHTML = notifs.map(n=>
|
||||
`<li style="color:${n.level==='danger'?'#ef4444':'#f59e0b'}">${n.msg}</li>`
|
||||
).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);
|
||||
|
||||
})();
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
if(!isLoggedIn()) { header("Location: login.php"); exit; }
|
||||
require_once 'partials/header.php';
|
||||
|
||||
if (!defined('NODERED_URL')) {
|
||||
define('NODERED_URL', 'https://node-red.aquamonn.online');
|
||||
}
|
||||
$noderedUrl = rtrim(NODERED_URL, '/');
|
||||
?>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<?php require_once 'partials/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content page-enter">
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:10px;">
|
||||
<div class="topbar-icons">
|
||||
<div class="notif" id="notifBtn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notif-badge">0</span>
|
||||
<div class="notif-dropdown" id="notifDropdown">
|
||||
<p><b>Notifikasi</b></p>
|
||||
<ul><li>Memuat data…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user" id="userBtn">
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="user-dropdown" id="userDropdown">
|
||||
<div class="user-info">
|
||||
<p class="user-email"><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
|
||||
</div>
|
||||
<a href="logout.php">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter tanggal -->
|
||||
<div style="margin-bottom:14px; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||||
<input type="date" id="filter-date"
|
||||
style="padding:6px 12px; border-radius:8px; border:1px solid rgba(255,255,255,.2);
|
||||
background:rgba(255,255,255,.1); color:#fff; font-size:13px; cursor:pointer;">
|
||||
<button onclick="applyFilter()"
|
||||
style="padding:6px 16px; border-radius:8px; background:#22c55e; color:#fff;
|
||||
border:none; font-size:13px; cursor:pointer; font-weight:600;">
|
||||
Filter
|
||||
</button>
|
||||
<button onclick="clearFilter()"
|
||||
style="padding:6px 16px; border-radius:8px; background:rgba(255,255,255,.15); color:#fff;
|
||||
border:none; font-size:13px; cursor:pointer;">
|
||||
Reset
|
||||
</button>
|
||||
<span id="status-label" style="font-size:11px; color:rgba(255,255,255,.4); margin-left:4px;"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div style="background:white; display:inline-block; padding:5px 15px;
|
||||
border-radius:10px 10px 0 0; font-weight:bold; font-size:12px;">Laporan</div>
|
||||
<table class="catatan-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Waktu</th>
|
||||
<th>Amonia</th>
|
||||
<th>Kekeruhan</th>
|
||||
<th>Suhu</th>
|
||||
<th>Tinggi Air</th>
|
||||
<th>Jarak sensor</th>
|
||||
<th>pH</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody-catatan">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;padding:20px;color:#888;">⏳ Memuat…</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<span id="pagination-info">–</span>
|
||||
<div class="pagination-controls">
|
||||
<button id="btn-prev" onclick="changePage(-1)">Previous</button>
|
||||
<span id="page-indicator" style="padding:0 10px;font-weight:bold;">1</span>
|
||||
<button id="btn-next" onclick="changePage(1)">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
<script>
|
||||
const NODE_RED_URL = "<?= $noderedUrl ?>";
|
||||
|
||||
let currentPage = 1;
|
||||
const rowsPerPage = 10;
|
||||
let historyData = [];
|
||||
let pollingTimer = null;
|
||||
|
||||
// ===============================
|
||||
// Stop Polling
|
||||
// ===============================
|
||||
function stopPolling() {
|
||||
console.log("Menghentikan polling Node-RED");
|
||||
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
pollingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Start Polling
|
||||
// ===============================
|
||||
function startPolling() {
|
||||
|
||||
stopPolling();
|
||||
|
||||
loadHistory();
|
||||
|
||||
pollingTimer = setInterval(() => {
|
||||
loadHistory();
|
||||
}, 5000); // refresh setiap 5 detik
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Ambil History dari Node-RED
|
||||
// ===============================
|
||||
async function fetchHistory(limit = 100, date = null) {
|
||||
|
||||
try {
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", limit);
|
||||
|
||||
if (date) {
|
||||
params.append("date", date);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NODE_RED_URL}/get_history?${params.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept":"application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
const decoded = await response.json();
|
||||
|
||||
let list = [];
|
||||
|
||||
if (Array.isArray(decoded)) {
|
||||
list = decoded;
|
||||
}
|
||||
else if (decoded.items) {
|
||||
list = decoded.items;
|
||||
}
|
||||
else if (decoded.data) {
|
||||
list = decoded.data;
|
||||
}
|
||||
else if (decoded.history) {
|
||||
list = decoded.history;
|
||||
}
|
||||
|
||||
return list;
|
||||
|
||||
} catch(err) {
|
||||
|
||||
console.error("Fetch History Error :", err);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Load Data
|
||||
// ===============================
|
||||
async function loadHistory() {
|
||||
|
||||
document.getElementById("status-label").innerHTML = "Sinkronisasi...";
|
||||
|
||||
const date = document.getElementById("filter-date").value;
|
||||
|
||||
historyData = await fetchHistory(
|
||||
100,
|
||||
date !== "" ? date : null
|
||||
);
|
||||
|
||||
currentPage = 1;
|
||||
|
||||
renderTable();
|
||||
|
||||
document.getElementById("status-label").innerHTML =
|
||||
"Update : " + new Date().toLocaleTimeString();
|
||||
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Render Table
|
||||
// ===============================
|
||||
function renderTable() {
|
||||
|
||||
const tbody = document.getElementById("tbody-catatan");
|
||||
|
||||
tbody.innerHTML = "";
|
||||
|
||||
if(historyData.length===0){
|
||||
|
||||
tbody.innerHTML=`
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;padding:20px;color:#999">
|
||||
Tidak ada data
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
document.getElementById("pagination-info").innerHTML="0 Data";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const start=(currentPage-1)*rowsPerPage;
|
||||
const end=start+rowsPerPage;
|
||||
|
||||
const pageData=historyData.slice(start,end);
|
||||
|
||||
pageData.forEach(item=>{
|
||||
|
||||
let tanggal = "-";
|
||||
let waktu = "-";
|
||||
|
||||
if (item.time) {
|
||||
|
||||
const arr = item.time.split("T");
|
||||
|
||||
tanggal = arr[0];
|
||||
|
||||
waktu = arr[1].replace(".000Z", "");
|
||||
}
|
||||
|
||||
// Data sensor
|
||||
const ammonia = Number(item.ammonia ?? item.amonia ?? 0).toFixed(1);
|
||||
const turbidity = Number(item.turbidity ?? item.kekeruhan ?? 0).toFixed(1);
|
||||
|
||||
// suhu & ph (sesuaikan jika nanti sudah dikirim dari Node-RED)
|
||||
const suhu = Number(item.suhu ?? 0).toFixed(1);
|
||||
const tinggi_air = Number(item.tinggiAir ?? item.tinggi_air ?? 0).toFixed(1);
|
||||
const jarak_sensor = Number(item.distance ?? item.jarak_Sensor ?? 0).toFixed(1);
|
||||
const ph = Number(item.ph ?? 0).toFixed(1);
|
||||
|
||||
tbody.innerHTML+=`
|
||||
<tr>
|
||||
<td>${tanggal}</td>
|
||||
<td>${waktu}</td>
|
||||
<td>${ammonia}</td>
|
||||
<td>${turbidity}</td>
|
||||
<td>${suhu}</td>
|
||||
<td>${tinggi_air}</td>
|
||||
<td>${jarak_sensor}</td>
|
||||
<td>${ph}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
document.getElementById("page-indicator").innerHTML=currentPage;
|
||||
|
||||
document.getElementById("pagination-info").innerHTML=
|
||||
`${start+1}-${Math.min(end,historyData.length)} dari ${historyData.length}`;
|
||||
|
||||
document.getElementById("btn-prev").disabled=currentPage===1;
|
||||
document.getElementById("btn-next").disabled=end>=historyData.length;
|
||||
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Pagination
|
||||
// ===============================
|
||||
function changePage(dir){
|
||||
|
||||
const maxPage=Math.ceil(historyData.length/rowsPerPage);
|
||||
|
||||
currentPage+=dir;
|
||||
|
||||
if(currentPage<1)
|
||||
currentPage=1;
|
||||
|
||||
if(currentPage>maxPage)
|
||||
currentPage=maxPage;
|
||||
|
||||
renderTable();
|
||||
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Filter
|
||||
// ===============================
|
||||
function applyFilter(){
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
function clearFilter(){
|
||||
|
||||
document.getElementById("filter-date").value="";
|
||||
|
||||
loadHistory();
|
||||
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Dropdown User
|
||||
// ===============================
|
||||
document.getElementById("userBtn").onclick=function(){
|
||||
|
||||
document.getElementById("userDropdown")
|
||||
.classList.toggle("show");
|
||||
|
||||
};
|
||||
|
||||
document.getElementById("notifBtn").onclick=function(){
|
||||
|
||||
document.getElementById("notifDropdown")
|
||||
.classList.toggle("show");
|
||||
|
||||
};
|
||||
|
||||
// ===============================
|
||||
// Start
|
||||
// ===============================
|
||||
window.onload=()=>{
|
||||
|
||||
startPolling();
|
||||
|
||||
};
|
||||
|
||||
window.onbeforeunload=()=>{
|
||||
|
||||
stopPolling();
|
||||
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Mencegah error session already started
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Konfigurasi Firebase (Ubah sesuai dengan detail project Firebase kamu)
|
||||
define("FIREBASE_API_KEY", "AIzaSyAGt3x8fgM27kL8Q4naNEDBE1ij5CbbFr0");
|
||||
define("FIREBASE_DB_URL", "https://aquascape-67707-default-rtdb.firebaseio.com/");
|
||||
?>
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
if(!isLoggedIn()) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('NODERED_URL')) {
|
||||
define('NODERED_URL', 'https://node-red.aquamonn.online');
|
||||
}
|
||||
|
||||
require_once 'partials/header.php';
|
||||
?>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<?php require_once 'partials/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content page-enter">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 30px;">
|
||||
<h2 style="color: white; font-weight:800; text-transform: uppercase;">DASHBOARD</h2>
|
||||
<div class="topbar-icons">
|
||||
|
||||
<!-- NOTIFIKASI -->
|
||||
<div class="notif" id="notifBtn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notif-badge" id="notifCount">0</span>
|
||||
<div class="notif-dropdown" id="notifDropdown">
|
||||
<p><b>Notifikasi</b></p>
|
||||
<ul id="notifList"><li>Memuat…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- USER -->
|
||||
<div class="user" id="userBtn">
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="user-dropdown" id="userDropdown">
|
||||
<div class="user-info">
|
||||
<p class="user-email"><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
|
||||
</div>
|
||||
<a href="logout.php">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Cards -->
|
||||
<div class="menu-cards-grid">
|
||||
<a href="monitoring.php" class="menu-card-link">
|
||||
<div class="icon-box">🌡️</div>
|
||||
<span>Monitoring<br>Parameter</span>
|
||||
</a>
|
||||
<a href="catatan.php" class="menu-card-link">
|
||||
<div class="icon-box">📝</div>
|
||||
<span>Catatan</span>
|
||||
</a>
|
||||
<a href="kontrol.php" class="menu-card-link">
|
||||
<div class="icon-box">⚙️</div>
|
||||
<span>Sistem Kontrol</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Grafik -->
|
||||
<div class="chart-container" style="max-width:600px; margin-top:30px; position:relative; height:280px;">
|
||||
<canvas id="phChart"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kirim NODERED_URL ke JS -->
|
||||
<script>
|
||||
window.NODERED_URL = "<?= rtrim(NODERED_URL, '/') ?>";
|
||||
</script>
|
||||
<script src="assets/js/chart.js"></script>
|
||||
|
||||
<script>
|
||||
// Sidebar toggle
|
||||
const toggleBtn = document.getElementById("toggleSidebar");
|
||||
if (toggleBtn) {
|
||||
toggleBtn.onclick = function() {
|
||||
document.querySelector(".sidebar").classList.toggle("hide");
|
||||
};
|
||||
}
|
||||
|
||||
// Dropdown notif & user
|
||||
const notifBtn = document.getElementById("notifBtn");
|
||||
const notifDropdown = document.getElementById("notifDropdown");
|
||||
const userBtn = document.getElementById("userBtn");
|
||||
const userDropdown = document.getElementById("userDropdown");
|
||||
|
||||
notifBtn.onclick = e => { e.stopPropagation(); notifDropdown.classList.toggle("show"); userDropdown.classList.remove("show"); };
|
||||
userBtn.onclick = e => { e.stopPropagation(); userDropdown.classList.toggle("show"); notifDropdown.classList.remove("show"); };
|
||||
document.onclick = () => { notifDropdown.classList.remove("show"); userDropdown.classList.remove("show"); };
|
||||
|
||||
// ── Notifikasi otomatis dari data sensor ─────────────────
|
||||
async function cekNotifikasi() {
|
||||
try {
|
||||
const res = await fetch(window.NODERED_URL + '/get_history', { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const arr = await res.json();
|
||||
if (!Array.isArray(arr) || arr.length === 0) return;
|
||||
|
||||
const latest = arr[0]; // data terbaru
|
||||
const notifs = [];
|
||||
|
||||
const suhu = parseFloat(latest.suhu ?? 0);
|
||||
const ph = parseFloat(latest.ph ?? latest.pH ?? 0);
|
||||
const amonia = parseFloat(latest.amonia ?? 0);
|
||||
const distance = parseFloat(latest.distance ?? 0);
|
||||
|
||||
if (suhu > 30) notifs.push(`🌡️ Suhu tinggi: ${suhu.toFixed(1)}°C`);
|
||||
if (ph < 6.5) notifs.push(`🔬 pH terlalu rendah: ${ph.toFixed(2)}`);
|
||||
if (ph > 8.0) notifs.push(`🔬 pH terlalu tinggi: ${ph.toFixed(2)}`);
|
||||
if (amonia > 2) notifs.push(`⚠️ Amonia tinggi: ${amonia.toFixed(2)} ppm`);
|
||||
if (distance > 70) notifs.push(`📏 Jarak air tinggi: ${distance.toFixed(1)} cm`);
|
||||
if (latest.relay == 1) notifs.push(`💧 Pompa sedang aktif`);
|
||||
|
||||
const badge = document.getElementById("notifCount");
|
||||
const list = document.getElementById("notifList");
|
||||
|
||||
badge.textContent = notifs.length;
|
||||
list.innerHTML = notifs.length
|
||||
? notifs.map(n => `<li>${n}</li>`).join('')
|
||||
: '<li>Semua parameter normal </li>';
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
cekNotifikasi();
|
||||
setInterval(cekNotifikasi, 30000);
|
||||
</script>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?php require_once 'partials/header.php'; ?>
|
||||
|
||||
<div class="bg-login">
|
||||
<div class="auth-container">
|
||||
<div class="card auth-card">
|
||||
<h2>Lupa Password</h2>
|
||||
|
||||
<form id="resetForm">
|
||||
<input type="email" id="email" placeholder="Masukkan email kamu" required>
|
||||
<button type="submit">Reset Password</button>
|
||||
</form>
|
||||
|
||||
<a href="login.php" class="forgot-password" style="text-align:center; margin-top:15px;">
|
||||
Kembali ke Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
||||
<!-- Firebase (VERSI BENAR - modular) -->
|
||||
<script type="module">
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js";
|
||||
import { getAuth, sendPasswordResetEmail } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-auth.js";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyAGt3x8fgM27kL8Q4naNEDBE1ij5CbbFr0",
|
||||
authDomain: "aquascape-67707.firebaseapp.com",
|
||||
};
|
||||
|
||||
// Init Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
|
||||
// Event Reset Password
|
||||
document.getElementById("resetForm").addEventListener("submit", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById("email").value;
|
||||
|
||||
sendPasswordResetEmail(auth, email)
|
||||
.then(() => {
|
||||
alert("Link reset password sudah dikirim ke email!");
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("Error: " + error.message);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
/**
|
||||
* PERBAIKAN PATH:
|
||||
* Menggunakan __DIR__ agar path selalu absolut dan akurat
|
||||
* dari folder 'functions' ke 'config'.
|
||||
*/
|
||||
$configPath = __DIR__ . '/../config/firebase.php';
|
||||
|
||||
if (file_exists($configPath)) {
|
||||
require_once $configPath;
|
||||
} else {
|
||||
// Jika masih error, tampilkan pesan bantuan
|
||||
echo "<div style='background:#ffeded; border:1px solid red; padding:10px; border-radius:5px; font-family:sans-serif;'>";
|
||||
echo "<b>Error:</b> File firebase.php tidak ditemukan!<br>";
|
||||
echo "Dicari di: <code style='color:blue'>$configPath</code>";
|
||||
echo "</div>";
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
require_once 'config/firebase.php';
|
||||
require_once 'partials/header.php';
|
||||
?>
|
||||
|
||||
<div class="aquarium">
|
||||
|
||||
<?php require_once 'partials/navbar.php'; ?>
|
||||
|
||||
<div class="aquarium">
|
||||
|
||||
<?php require_once 'partials/navbar.php'; ?>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Welcome to My Aquascape</h1>
|
||||
</div>
|
||||
|
||||
<!-- Ikan -->
|
||||
<div class="fish fish1">🐟</div>
|
||||
<div class="fish fish2">🐠</div>
|
||||
<div class="fish fish3">🐡</div>
|
||||
<div class="shark">🦈</div>
|
||||
<div class="stingray">🐋</div>
|
||||
<div class="jellyfish j1">🪼</div>
|
||||
<div class="jellyfish j2">🪼</div>
|
||||
<div class="jellyfish j3">🪼</div>
|
||||
|
||||
|
||||
<!-- Gelembung -->
|
||||
<div class="bubble bubble1"></div>
|
||||
<div class="bubble bubble2"></div>
|
||||
<div class="bubble bubble3"></div>
|
||||
<div class="bubble bubble4"></div>
|
||||
<div class="bubble bubble5"></div>
|
||||
<div class="bubble bubble6"></div>
|
||||
<div class="bubble bubble7"></div>
|
||||
<div class="bubble bubble8"></div>
|
||||
<div class="bubble bubble9"></div>
|
||||
<div class="bubble bubble10"></div>
|
||||
|
||||
|
||||
<!-- Terumbu Karang -->
|
||||
<div class="coral">🪸🪸🪸</div>
|
||||
<div class="coral-right">🌿🪸🐚</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,583 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
|
||||
if(!isLoggedIn()) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!defined('NODERED_URL')) {
|
||||
define('NODERED_URL', 'https://node-red.aquamonn.online');
|
||||
}
|
||||
|
||||
require_once 'partials/header.php';
|
||||
$noderedUrl = rtrim(NODERED_URL, '/');
|
||||
?>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<?php require_once 'partials/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content page-enter">
|
||||
|
||||
<style>
|
||||
.sensor-bar-wrap {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: rgba(255,255,255,.15);
|
||||
border-radius: 99px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sensor-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
background: #22c55e;
|
||||
width: 0%;
|
||||
transition: width .6s ease, background .3s;
|
||||
}
|
||||
#conn-status {
|
||||
display: inline-block;
|
||||
width: 9px; height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
margin-right: 6px;
|
||||
transition: background .3s;
|
||||
}
|
||||
#conn-status.online { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
|
||||
#conn-status.offline { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
|
||||
#last-update { font-size: 11px; color: rgba(255,255,255,.55); margin-left: 6px; }
|
||||
.device-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: rgba(255,255,255,.1);
|
||||
color: rgba(255,255,255,.5);
|
||||
transition: background .3s, color .3s;
|
||||
}
|
||||
.device-badge.on { background: rgba(34,197,94,.2); color: #22c55e; }
|
||||
.device-badge.off { background: rgba(239,68,68,.15); color: #ef4444; }
|
||||
</style>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<?php require_once 'partials/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content page-enter">
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-wrap:wrap; gap:10px;">
|
||||
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
|
||||
<div style="display:flex; align-items:center; color:rgba(25, 22, 22, 0.7); font-size:13px;">
|
||||
<span id="conn-status"></span>
|
||||
<span id="conn-label">Menghubungkan…</span>
|
||||
<span id="last-update"></span>
|
||||
</div>
|
||||
<span class="device-badge" id="badge-relay">
|
||||
|
||||
</span>
|
||||
<span class="device-badge" id="badge-lampu">
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div class="topbar-icons">
|
||||
<div class="notif" id="notifBtn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notif-badge">0</span>
|
||||
<div class="notif-dropdown" id="notifDropdown">
|
||||
<p><b>Notifikasi</b></p>
|
||||
<ul><li>Memuat data…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user" id="userBtn">
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="user-dropdown" id="userDropdown">
|
||||
<p><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
|
||||
<a href="logout.php">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRID -->
|
||||
<div class="ctrl-main-grid">
|
||||
|
||||
<!-- BOX SISTEM OTOMATIS -->
|
||||
<div class="ctrl-box">
|
||||
<div class="ctrl-box-title">Sistem Otomatis</div>
|
||||
|
||||
<!-- AMONIA -->
|
||||
<div class="ctrl-item">
|
||||
<span>Batas Amonia</span>
|
||||
<input type="number" id="input-amonia" step="0.1">
|
||||
<button class="btn-mulai" onclick="setAmonia()">Simpan</button>
|
||||
</div>
|
||||
<!-- KEKERUHAN -->
|
||||
<div class="ctrl-item">
|
||||
<span>Batas Kekeruhan</span>
|
||||
<input type="number" id="input-kekeruhan" step="0.1" >
|
||||
<button class="btn-mulai" onclick="setBatasKekeruhan()">Simpan</button>
|
||||
</div>
|
||||
<!-- TINGGI AIR -->
|
||||
<div class="ctrl-item">
|
||||
<span>Batas Tinggi Air</span>
|
||||
<input type="number" id="input-tinggi-air" step="1">
|
||||
<button class="btn-mulai" onclick="setBatasTinggiAir()">Simpan</button>
|
||||
</div>
|
||||
<!-- KONTROL TINGGI AQUARIUM -->
|
||||
<div class="ctrl-item">
|
||||
<span>Tinggi Aquarium</span>
|
||||
<input type="number" id="input-tinggi-aquarium" step="1">
|
||||
<button class="btn-mulai" onclick="setTinggiAquarium()">Simpan</button>
|
||||
</div>
|
||||
|
||||
<!-- MODE POMPA -->
|
||||
<div class="ctrl-item">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOX LAMPU -->
|
||||
<div class="ctrl-box">
|
||||
<div class="ctrl-box-title">Set Lampu</div>
|
||||
|
||||
<div class="ctrl-time">
|
||||
<input type="text" id="jam-mulai">
|
||||
<span style="font-weight:bold;">—</span>
|
||||
<input type="text" id="jam-selesai">
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
|
||||
<button class="btn-mulai" style="background:#e222;" onclick="setJadwalLampu()">Simpan Jadwal</button>
|
||||
</div>
|
||||
|
||||
<!-- MODE LAMPU -->
|
||||
<div class="ctrl-item" style="margin-top:12px;">
|
||||
<span>Lampu</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="toggle-lampu" onchange="kontrolLampu(this.checked)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span id="label-lampu" style="font-size:12px; color:#aaa;">OFF</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const NR = "<?= $noderedUrl ?>";
|
||||
|
||||
|
||||
/* ===================== JADWAL LAMPU ===================== */
|
||||
async function setJadwalLampu() {
|
||||
|
||||
const onTime = document.getElementById("jam-mulai").value.trim();
|
||||
const offTime = document.getElementById("jam-selesai").value.trim();
|
||||
|
||||
if (!onTime || !offTime) {
|
||||
showToast("Isi jam nyala dan jam mati terlebih dahulu", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const re = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
|
||||
if (!re.test(onTime) || !re.test(offTime)) {
|
||||
showToast("Format jam harus HH:MM", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Menggunakan toggle yang ada di HTML
|
||||
const isAuto = document.getElementById("toggle-lampu")?.checked ?? false;
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/api-schedule", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
on_time: onTime,
|
||||
off_time: offTime,
|
||||
enabled: isAuto
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
await response.text();
|
||||
|
||||
showToast("Jadwal lampu berhasil disimpan");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Gagal menyimpan jadwal lampu", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ===================== KONTROL LAMPU ===================== */
|
||||
async function kontrolLampu(isOn) {
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/api-lampu", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lampu: isOn ? "on" : "off"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
await response.text();
|
||||
|
||||
document.getElementById("label-lampu").textContent = isOn ? "ON" : "OFF";
|
||||
|
||||
showToast("Lampu " + (isOn ? "ON" : "OFF"));
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Gagal mengontrol lampu", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ===================== BATAS AMONIA ===================== */
|
||||
async function setAmonia() {
|
||||
|
||||
const nilai = parseFloat(document.getElementById("input-amonia").value);
|
||||
|
||||
if (isNaN(nilai)) {
|
||||
showToast("Masukkan nilai amonia", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/set-pmaks", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
maks: nilai
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
showToast("Batas amonia berhasil disimpan", "success");
|
||||
|
||||
} catch (error) {
|
||||
|
||||
console.error(error);
|
||||
|
||||
showToast("Gagal menyimpan batas amonia", "error");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ===================== BATAS KEKERUHAN ===================== */
|
||||
async function setBatasKekeruhan() {
|
||||
|
||||
const nilai = parseFloat(document.getElementById("input-kekeruhan").value);
|
||||
|
||||
if (isNaN(nilai)) {
|
||||
showToast("Masukkan nilai kekeruhan", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/set-batas_kekeruhan", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
max_kekeruhan: nilai
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
await response.text();
|
||||
|
||||
showToast("Batas kekeruhan berhasil disimpan");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Gagal menyimpan batas kekeruhan", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ===================== BATAS TINGGI AIR ===================== */
|
||||
async function setBatasTinggiAir() {
|
||||
|
||||
const nilai = parseFloat(document.getElementById("input-tinggi-air").value);
|
||||
|
||||
if (isNaN(nilai)) {
|
||||
showToast("Masukkan tinggi air", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/set-batas_distance", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
min_jarak: nilai
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
await response.text();
|
||||
|
||||
showToast("Batas tinggi air berhasil disimpan");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Gagal menyimpan batas tinggi air", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ===================== TINGGI AQUARIUM ===================== */
|
||||
async function setTinggiAquarium() {
|
||||
|
||||
const nilai = parseFloat(document.getElementById("input-tinggi-aquarium").value);
|
||||
|
||||
if (isNaN(nilai)) {
|
||||
showToast("Masukkan tinggi aquarium", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(NR + "/set-tinggi_aquarium", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tinggi_aquarium: nilai
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
|
||||
await response.text();
|
||||
|
||||
showToast("Tinggi aquarium berhasil disimpan");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Gagal menyimpan tinggi aquarium", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ── Load status terkini dari Node-RED /get-status ────── */
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const res = await fetch(NR + '/get-status', { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const d = await res.json();
|
||||
if (!d) return;
|
||||
|
||||
// Pompa
|
||||
if (d.relay !== undefined) {
|
||||
const on = d.relay == 1;
|
||||
|
||||
const statusPompa = document.getElementById('status-pompa');
|
||||
if (statusPompa) {
|
||||
statusPompa.textContent = on ? 'ON' : 'OFF';
|
||||
statusPompa.style.color = on ? '#22c55e' : '#ef4444';
|
||||
}
|
||||
}
|
||||
|
||||
if (d.pompa_mode !== undefined) {
|
||||
const isAuto = d.pompa_mode === 'AUTO';
|
||||
|
||||
const togglePompa = document.getElementById('toggle-pompa-auto');
|
||||
if (togglePompa) togglePompa.checked = isAuto;
|
||||
|
||||
const labelPompa = document.getElementById('label-pompa-mode');
|
||||
if (labelPompa) labelPompa.textContent = isAuto ? 'Auto' : 'Manual';
|
||||
}
|
||||
if (d.batas_amonia !== undefined) {
|
||||
document.getElementById('input-amonia').value = d.batas_amonia;
|
||||
}
|
||||
|
||||
if (d.batas_turbidity !== undefined) {
|
||||
document.getElementById('input-kekeruhan').value = d.batas_turbidity;
|
||||
}
|
||||
|
||||
if (d.batas_distance !== undefined) {
|
||||
document.getElementById('input-tinggi-air').value = d.batas_distance;
|
||||
}
|
||||
|
||||
if (d.tinggi_aquarium !== undefined) {
|
||||
document.getElementById('input-tinggi-aquarium').value = d.tinggi_aquarium;
|
||||
}
|
||||
|
||||
// Lampu
|
||||
if (d.lampu !== undefined) {
|
||||
const on = d.lampu == 1;
|
||||
|
||||
const labelLampu = document.getElementById('label-lampu');
|
||||
if (labelLampu) {
|
||||
labelLampu.textContent = on ? 'ON' : 'OFF';
|
||||
}
|
||||
|
||||
const toggleLampu = document.getElementById('toggle-lampu');
|
||||
if (toggleLampu) {
|
||||
toggleLampu.checked = on;
|
||||
}
|
||||
}
|
||||
|
||||
const jamMulai = document.getElementById("jam-mulai");
|
||||
|
||||
if (jamMulai && !jamMulai.value && d.lampu_on_time) {
|
||||
jamMulai.value = d.lampu_on_time.substring(0,5);
|
||||
}
|
||||
|
||||
const jamSelesai = document.getElementById("jam-selesai");
|
||||
|
||||
if (jamSelesai && !jamSelesai.value && d.lampu_off_time) {
|
||||
jamSelesai.value = d.lampu_off_time.substring(0,5);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
console.warn('[kontrol] Gagal load status:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Toast ────────────────────────────────────────────── */
|
||||
function showToast(message, type = "success") {
|
||||
|
||||
let toast = document.getElementById("nr-toast");
|
||||
|
||||
if (!toast) {
|
||||
toast = document.createElement("div");
|
||||
toast.id = "nr-toast";
|
||||
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
min-width: 320px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
z-index: 99999;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all .35s ease;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,.25);
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
|
||||
if (type === "success") {
|
||||
toast.style.background = "#22c55e";
|
||||
toast.innerHTML = "✅ " + message;
|
||||
} else {
|
||||
toast.style.background = "#ef4444";
|
||||
toast.innerHTML = "❌ " + message;
|
||||
}
|
||||
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateX(0)";
|
||||
|
||||
clearTimeout(toast.timer);
|
||||
|
||||
toast.timer = setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateX(100%)";
|
||||
}, 3000);
|
||||
}
|
||||
/* ── Dropdown ─────────────────────────────────────────── */
|
||||
const notifBtn = document.getElementById('notifBtn');
|
||||
const notifDropdown = document.getElementById('notifDropdown');
|
||||
const userBtn = document.getElementById('userBtn');
|
||||
const userDropdown = document.getElementById('userDropdown');
|
||||
notifBtn.onclick = e => { e.stopPropagation(); notifDropdown.classList.toggle('show'); userDropdown.classList.remove('show'); };
|
||||
userBtn.onclick = e => { e.stopPropagation(); userDropdown.classList.toggle('show'); notifDropdown.classList.remove('show'); };
|
||||
document.onclick = () => { notifDropdown.classList.remove('show'); userDropdown.classList.remove('show'); };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStatus();
|
||||
setInterval(loadStatus, 10000); // refresh status tiap 10 detik
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* NODERED_URL = URL server Node-RED (misal http://192.168.1.x:1880)
|
||||
* node-red.js akan panggil NODERED_URL + "/get-sensor"
|
||||
*
|
||||
* FIREBASE_DB_URL dipakai sebagai fallback jika NODERED_URL tidak diset.
|
||||
*
|
||||
* Ganti IP sesuai server Node-RED kamu.
|
||||
*/
|
||||
|
||||
window.NODERED_URL = "https://node-red.aquamonn.online";
|
||||
window.FIREBASE_DB_URL = "<?= rtrim(FIREBASE_DB_URL, '/') ?>";
|
||||
|
||||
// Jika NODERED_URL tidak dikonfigurasi di PHP, kosongkan agar
|
||||
// node-red.js fallback ke Firebase langsung.
|
||||
if (!window.NODERED_URL) {
|
||||
// Firebase path sesuai flow:
|
||||
// https://aquascape-67707-default-rtdb.firebaseio.com/sensor.json
|
||||
window.FIREBASE_DB_URL = window.FIREBASE_DB_URL || "https://aquascape-67707-default-rtdb.firebaseio.com";
|
||||
}
|
||||
|
||||
window.onSensorUpdate = function(ok) {
|
||||
const dot = document.getElementById("conn-status");
|
||||
const label = document.getElementById("conn-label");
|
||||
const ts = document.getElementById("last-update");
|
||||
if (!dot) return;
|
||||
dot.className = ok ? "online" : "offline";
|
||||
label.textContent = ok ? "Live" : "Offline";
|
||||
if (ok) ts.textContent = " · " + new Date().toLocaleTimeString("id-ID");
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="assets/js/node-red.js"></script>
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
|
||||
// Redirect ke dashboard jika sudah login
|
||||
if(isLoggedIn()) {
|
||||
header("Location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$email = $_POST['email'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
$res = loginUser($email, $password);
|
||||
if($res['success']) {
|
||||
header("Location: dashboard.php");
|
||||
exit;
|
||||
} else {
|
||||
$error = $res['error'];
|
||||
}
|
||||
}
|
||||
|
||||
require_once 'partials/header.php';
|
||||
?>
|
||||
|
||||
<div class="bg-login">
|
||||
<?php require_once 'partials/navbar.php'; ?>
|
||||
<div class="auth-container">
|
||||
<div class="card auth-card">
|
||||
<h2>Login</h2>
|
||||
<?php if($error): ?>
|
||||
<p class="message msg-error"><?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<input type="email" name="email" placeholder="user@gmail.com" required>
|
||||
<input type="password" name="password" placeholder="********" required>
|
||||
<a href="forgot.php" class="forgot-password">
|
||||
Lupa password?
|
||||
</a>
|
||||
<button type="submit">Start</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
// Mulai session, hancurkan, dan redirect ke index (Home)
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: index.php");
|
||||
exit();
|
||||
?>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
if(!isLoggedIn()) { header("Location: login.php"); exit; }
|
||||
require_once 'partials/header.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.sensor-bar-wrap {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: rgba(255,255,255,.15);
|
||||
border-radius: 99px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sensor-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
background: #22c55e;
|
||||
width: 0%;
|
||||
transition: width .6s ease, background .3s;
|
||||
}
|
||||
#conn-status {
|
||||
display: inline-block;
|
||||
width: 9px; height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #6b7280;
|
||||
margin-right: 6px;
|
||||
transition: background .3s;
|
||||
}
|
||||
#conn-status.online { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
|
||||
#conn-status.offline { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
|
||||
#last-update { font-size: 11px; color: rgba(255,255,255,.55); margin-left: 6px; }
|
||||
.device-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: rgba(255,255,255,.1);
|
||||
color: rgba(255,255,255,.5);
|
||||
transition: background .3s, color .3s;
|
||||
}
|
||||
.device-badge.on { background: rgba(34,197,94,.2); color: #22c55e; }
|
||||
.device-badge.off { background: rgba(239,68,68,.15); color: #ef4444; }
|
||||
</style>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<?php require_once 'partials/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content page-enter">
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-wrap:wrap; gap:10px;">
|
||||
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
|
||||
<div style="display:flex; align-items:center; color:rgba(255,255,255,.7); font-size:13px;">
|
||||
<span id="conn-status"></span>
|
||||
<span id="conn-label">Menghubungkan…</span>
|
||||
<span id="last-update"></span>
|
||||
</div>
|
||||
<span class="device-badge" id="badge-relay">
|
||||
|
||||
</span>
|
||||
<span class="device-badge" id="badge-lampu">
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div class="topbar-icons">
|
||||
<div class="notif" id="notifBtn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notif-badge">0</span>
|
||||
<div class="notif-dropdown" id="notifDropdown">
|
||||
<p><b>Notifikasi</b></p>
|
||||
<ul><li>Memuat data…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user" id="userBtn">
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="user-dropdown" id="userDropdown">
|
||||
<p><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
|
||||
<a href="logout.php">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KARTU ATAS: Amonia & Kekeruhan -->
|
||||
<div class="mon-top-grid">
|
||||
<div class="mon-circle-card">
|
||||
<div class="circle-val" id="val-amonia">— mg/L<mg>
|
||||
<L></L></div>
|
||||
<div class="title">Amonia</div>
|
||||
<div class="sensor-bar-wrap">
|
||||
<div class="sensor-bar-fill" id="bar-amonia"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mon-circle-card">
|
||||
<div class="circle-val" id="val-distance">— cm</div>
|
||||
<div class="title">Distance</div>
|
||||
<div class="sensor-bar-wrap">
|
||||
<div class="sensor-bar-fill" id="bar-distance"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mon-circle-card">
|
||||
<!-- kekeruhan = turbidity dari topic aquamonn/kekeruhan -->
|
||||
<div class="circle-val" id="val-kekeruhan">— NTU</div>
|
||||
<div class="title">Kekeruhan</div>
|
||||
<div class="sensor-bar-wrap">
|
||||
<div class="sensor-bar-fill" id="bar-kekeruhan"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KARTU BAWAH: Suhu & pH -->
|
||||
<div class="mon-bot-grid">
|
||||
<!-- Suhu → field "suhu" -->
|
||||
<div class="mon-pill-card">
|
||||
<span style="font-size:1.5rem;">☁️</span>
|
||||
<div style="text-align:right; line-height:1; flex:1;">
|
||||
<span id="val-suhu" style="font-size:1.8rem; font-weight:800;">—</span><br>
|
||||
<span style="font-size:0.8rem; font-weight:normal;">Celcius</span>
|
||||
<div class="sensor-bar-wrap" style="margin-top:6px;">
|
||||
<div class="sensor-bar-fill" id="bar-suhu"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- pH → field "ph" (Node-RED) / "pH" (ESP32) keduanya ditangani -->
|
||||
<div class="mon-pill-card">
|
||||
<div style="flex:1; text-align:left;">
|
||||
<span id="val-ph" style="font-size:1.5rem;">— pH</span>
|
||||
<div class="sensor-bar-wrap" style="margin-top:6px;">
|
||||
<div class="sensor-bar-fill" id="bar-ph"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size:1.5rem;">💧</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* NODERED_URL = URL server Node-RED (misal http://192.168.1.x:1880)
|
||||
* node-red.js akan panggil NODERED_URL + "/get-sensor"
|
||||
*
|
||||
* FIREBASE_DB_URL dipakai sebagai fallback jika NODERED_URL tidak diset.
|
||||
*
|
||||
* Ganti IP sesuai server Node-RED kamu.
|
||||
*/
|
||||
|
||||
window.NODERED_URL = "https://node-red.aquamonn.online";
|
||||
window.FIREBASE_DB_URL = "<?= rtrim(FIREBASE_DB_URL, '/') ?>";
|
||||
|
||||
// Jika NODERED_URL tidak dikonfigurasi di PHP, kosongkan agar
|
||||
// node-red.js fallback ke Firebase langsung.
|
||||
if (!window.NODERED_URL) {
|
||||
// Firebase path sesuai flow:
|
||||
// https://aquascape-67707-default-rtdb.firebaseio.com/sensor.json
|
||||
window.FIREBASE_DB_URL = window.FIREBASE_DB_URL || "https://aquascape-67707-default-rtdb.firebaseio.com";
|
||||
}
|
||||
|
||||
window.onSensorUpdate = function(ok) {
|
||||
const dot = document.getElementById("conn-status");
|
||||
const label = document.getElementById("conn-label");
|
||||
const ts = document.getElementById("last-update");
|
||||
if (!dot) return;
|
||||
dot.className = ok ? "online" : "offline";
|
||||
label.textContent = ok ? "Live" : "Offline";
|
||||
if (ok) ts.textContent = " · " + new Date().toLocaleTimeString("id-ID");
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="assets/js/node-red.js"></script>
|
||||
|
||||
<script>
|
||||
const notifBtn = document.getElementById("notifBtn");
|
||||
const notifDropdown = document.getElementById("notifDropdown");
|
||||
const userBtn = document.getElementById("userBtn");
|
||||
const userDropdown = document.getElementById("userDropdown");
|
||||
|
||||
notifBtn.onclick = e => {
|
||||
e.stopPropagation();
|
||||
notifDropdown.classList.toggle("show");
|
||||
userDropdown.classList.remove("show");
|
||||
};
|
||||
userBtn.onclick = e => {
|
||||
e.stopPropagation();
|
||||
userDropdown.classList.toggle("show");
|
||||
notifDropdown.classList.remove("show");
|
||||
};
|
||||
document.onclick = () => {
|
||||
notifDropdown.classList.remove("show");
|
||||
userDropdown.classList.remove("show");
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
</body>
|
||||
<script>
|
||||
document.getElementById("toggleSidebar").onclick = function() {
|
||||
document.querySelector(".sidebar").classList.toggle("hide");
|
||||
};
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Aquascape IoT Dashboard</title>
|
||||
<!-- Include Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@600&display=swap" rel="stylesheet">
|
||||
<!-- Include Chart.js Library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<nav class="navbar">
|
||||
<div class="logo">Aquascape</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php">HOME</a>
|
||||
<?php if(isset($_SESSION['user_token'])): ?>
|
||||
<a href="dashboard.php">DASHBOARD</a>
|
||||
<a href="logout.php">LOGOUT</a>
|
||||
<?php else: ?>
|
||||
<a href="login.php">LOGIN</a>
|
||||
<a href="register.php">REGISTER</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
?>
|
||||
|
||||
<div class="sidebar" id="sidebar">
|
||||
|
||||
<div class="sidebar-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
|
||||
<h3 class="sidebar-title" style="margin:0; font-size:18px; line-height:1;">
|
||||
DASHBOARD
|
||||
</h3>
|
||||
|
||||
<button id="toggleSidebar"
|
||||
style="background:none; border:none; color:black; font-size:18px; cursor:pointer;">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ul>
|
||||
<li class="<?= ($currentPage == 'dashboard.php') ? 'active' : '' ?>">
|
||||
<a href="dashboard.php">
|
||||
<i class="fas fa-home"></i>
|
||||
<span class="text">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="<?= ($currentPage == 'monitoring.php') ? 'active' : '' ?>">
|
||||
<a href="monitoring.php">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span class="text">Monitoring Parameter</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="<?= ($currentPage == 'kontrol.php') ? 'active' : '' ?>">
|
||||
<a href="kontrol.php">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<span class="text">Sistem Kontrol</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="<?= ($currentPage == 'catatan.php') ? 'active' : '' ?>">
|
||||
<a href="catatan.php">
|
||||
<i class="fas fa-note-sticky"></i>
|
||||
<span class="text">Catatan</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<br><br>
|
||||
|
||||
<li>
|
||||
<a href="logout.php">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span class="text">Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
require_once 'functions/auth.php';
|
||||
|
||||
// Redirect ke dashboard jika sudah login
|
||||
if(isLoggedIn()) {
|
||||
header("Location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$username = $_POST['username'];
|
||||
$email = $_POST['email'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
$res = registerUser($username, $email, $password);
|
||||
if($res['success']) {
|
||||
$success = 'Registrasi berhasil! Silahkan Login.';
|
||||
} else {
|
||||
$error = $res['error'];
|
||||
}
|
||||
}
|
||||
|
||||
require_once 'partials/header.php';
|
||||
?>
|
||||
|
||||
<div class="bg-login">
|
||||
<?php require_once 'partials/navbar.php'; ?>
|
||||
|
||||
<div class="auth-container">
|
||||
<div class="card auth-card">
|
||||
<h2>Register</h2>
|
||||
<?php if($error): ?>
|
||||
<p class="message msg-error"><?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if($success): ?>
|
||||
<p class="message msg-success"><?= htmlspecialchars($success) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'partials/footer.php'; ?>
|
||||
Loading…
Reference in New Issue