Skip to content

Add dashboard UI, JS, CSS, and template #15

Merged
merged 1 commit into from
Mar 5, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Install These Packages To Run The Code:
python -m install flask
python -m install paho-mqtt
61 changes: 61 additions & 0 deletions src/dashboard/js/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// ── Smart Campus Environmental Monitor ──────────────────────
// dashboard.js
// Author: Favour Ezeh
// Role: Project Manager | Dashboard Lead
// 4005CMD Integrative Project — Coventry University
// ────────────────────────────────────────────────────────────

// ── AUTO REFRESH ─────────────────────────────────────────────
// TODO Sprint 4: Replace with live MQTT data using Flask endpoint
// Currently page refreshes every 30 seconds to simulate updates

const REFRESH_INTERVAL = 30000; // 30 seconds

setTimeout(function() {
location.reload();
}, REFRESH_INTERVAL);


// ── ALERT COUNTER ────────────────────────────────────────────
// Updates the page title to show alert count so it's visible
// even when the browser tab is in the background

document.addEventListener("DOMContentLoaded", function() {
const alertBadges = document.querySelectorAll(".badge.bg-danger");
const alertCount = alertBadges.length;

if (alertCount > 0) {
document.title = `(${alertCount} Alerts) Environmental Campus Monitor`;
} else {
document.title = "Environmental Campus Monitor";
}
});


// ── TIMESTAMP ────────────────────────────────────────────────
// Shows the last time the page was refreshed so users know
// how current the data is

document.addEventListener("DOMContentLoaded", function() {
const now = new Date();
const timeString = now.toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});

const navbar = document.querySelector(".navbar-text");
if (navbar) {
navbar.textContent = `Last updated: ${timeString} | 4005CMD — Coventry University`;
}
});


// ── HISTORICAL CHARTS ────────────────────────────────────────
// TODO Sprint 3: Build out Chart.js graphs here using data
// fetched from Flask /api/history endpoint
// This will include:
// - Temperature trend line chart
// - CO2 trend line chart
// - Occupancy bar chart
// - Noise level chart
151 changes: 151 additions & 0 deletions src/dashboard/static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/* ── GLOBAL ────────────────────────────────────────────────── */
body {
background-color: #f0f4f8;
font-family: Arial, sans-serif;
color: #333333;
}

/* ── NAVBAR ────────────────────────────────────────────────── */
.navbar {
background-color: #1F4E79;
padding: 12px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.navbar-brand {
font-size: 1.2rem;
font-weight: bold;
color: #ffffff !important;
letter-spacing: 0.5px;
}

.navbar-text {
color: #BDD7EE !important;
font-size: 0.85rem;
}

/* ── PAGE TITLE ─────────────────────────────────────────────── */
.page-title {
color: #1F4E79;
font-weight: bold;
font-size: 1.6rem;
}

/* ── STAT CARDS ─────────────────────────────────────────────── */
.stat-card {
background-color: #ffffff;
border-radius: 10px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
border-left: 5px solid #2E75B6;
}

.stat-card.alert-stat {
border-left: 5px solid #dc3545;
}

.stat-number {
font-size: 2rem;
font-weight: bold;
color: #1F4E79;
}

.alert-stat .stat-number {
color: #dc3545;
}

.stat-label {
font-size: 0.85rem;
color: #666666;
margin-top: 4px;
}

/* ── DASHBOARD CARDS ────────────────────────────────────────── */
.dashboard-card {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
overflow: hidden;
height: 100%;
}

.card-header-custom {
background-color: #1F4E79;
color: #ffffff;
font-weight: bold;
font-size: 1rem;
padding: 12px 20px;
letter-spacing: 0.3px;
}

.alert-header {
background-color: #c0392b;
}

.card-body {
padding: 16px;
}

/* ── TABLE ──────────────────────────────────────────────────── */
.table thead th {
background-color: #D6E4F0;
color: #1F4E79;
font-weight: bold;
font-size: 0.85rem;
border: none;
}

.table tbody tr:hover {
background-color: #f0f7ff;
}

.table td {
font-size: 0.9rem;
vertical-align: middle;
}

/* ── ALERT ITEMS ────────────────────────────────────────────── */
.alert-item {
background-color: #fff5f5;
border-left: 4px solid #dc3545;
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 10px;
}

.alert-room {
font-weight: bold;
color: #c0392b;
font-size: 0.85rem;
}

.alert-message {
font-size: 0.85rem;
color: #333333;
margin-top: 2px;
}

.alert-value {
font-size: 0.8rem;
color: #666666;
margin-top: 4px;
}

/* ── BADGES ─────────────────────────────────────────────────── */
.badge {
font-size: 0.78rem;
padding: 5px 10px;
border-radius: 20px;
}

/* ── RESPONSIVE ─────────────────────────────────────────────── */
@media (max-width: 768px) {
.stat-card {
margin-bottom: 16px;
}

.navbar-text {
display: none;
}
}
123 changes: 123 additions & 0 deletions src/dashboard/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{% extends "base.html" %}

{% block content %}

<!-- Page Header -->
<div class="row mb-4">
<div class="col">
<h2 class="page-title">Dashboard Overview</h2>
<p class="text-muted">Live environmental monitoring across campus rooms</p>
</div>
<div class="col-auto">
<span class="badge bg-success fs-6">● System Online</span>
</div>
</div>

<!-- Stats Summary Row -->
<div class="row mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">3</div>
<div class="stat-label">Rooms Monitored</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">12</div>
<div class="stat-label">Active Sensors</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card alert-stat">
<div class="stat-number">{{ alerts|length }}</div>
<div class="stat-label">Active Alerts</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">Live</div>
<div class="stat-label">Data Status</div>
</div>
</div>
</div>

<!-- Live Readings and Alerts Row -->
<div class="row mb-4">

<!-- Live Sensor Readings -->
<div class="col-md-8">
<div class="dashboard-card">
<div class="card-header-custom">
Live Sensor Readings
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Room</th>
<th>Sensor</th>
<th>Value</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for reading in readings %}
<tr>
<td>{{ reading.room }}</td>
<td>{{ reading.sensor }}</td>
<td><strong>{{ reading.value }}</strong></td>
<td>
{% if reading.status == "Alert" %}
<span class="badge bg-danger">⚠ Alert</span>
{% else %}
<span class="badge bg-success">✓ Normal</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>

<!-- Active Alerts Panel -->
<div class="col-md-4">
<div class="dashboard-card">
<div class="card-header-custom alert-header">
Active Alerts
</div>
<div class="card-body">
{% if alerts %}
{% for alert in alerts %}
<div class="alert-item">
<div class="alert-room">{{ alert.room }}</div>
<div class="alert-message">{{ alert.message }}</div>
<div class="alert-value">Reading: <strong>{{ alert.value }}</strong></div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center mt-3">No active alerts</p>
{% endif %}
</div>
</div>
</div>

</div>

<!-- Historical Charts Row -->
<div class="row mb-4">
<div class="col-12">
<div class="dashboard-card">
<div class="card-header-custom">
Historical Data
</div>
<div class="card-body text-center py-5">
<p class="text-muted fs-5">Historical Charts — Coming Sprint 3</p>
<p class="text-muted">Will display temperature, CO2, noise and occupancy trends over time</p>
</div>
</div>
</div>
</div>

{% endblock %}