SIDES — SABO Integrated Debris Flows Monitoring and Early Warning System
SIDES is a flood and debris flow monitoring system built for the Sabo Dam construction project at Sg Kupang, Baling, Kedah, Malaysia. It provides real-time monitoring of rainfall, water level, siren status, and CCTV feeds from field stations, with historical data analysis, notifications, and an admin dashboard for station and user management.
The system is part of the flood and debris flow mitigation initiative by Jabatan Pengairan dan Saliran (JPS) Kedah and is live at https://sides.tck.com.my.
Table of Contents
- Features
- Tech Stack
- Architecture
- Project Structure
- Database Schema
- Requirements
- Deployment (Docker)
- Environment Variables
- User Accounts
- Third-Party Packages
- Frontend Libraries (CDN)
- Localization
- Activity Logging
- Android App Integration
- Backup & Restore
Features
Monitoring Dashboard
- Interactive OpenStreetMap showing all stations with real-time sensor data
- Station markers color-coded by type (rainfall, water level, siren)
Rainfall Monitoring
- Current Rainfall — real-time rainfall readings per station with hourly graphs
- Daily Rainfall — date-based rainfall data with hourly interval breakdown
- Threshold Monitoring — cumulative rainfall totals with 4-tier alert levels:
- Light (1–10 mm) — green
- Moderate (11–30 mm) — yellow
- Heavy (31–60 mm) — orange
- Very Heavy (>60 mm) — red
- Historical Rainfall — query by station and date range with CSV export
Water Level Monitoring
- Current Water Level — real-time readings per station with graphs
- Historical Water Level — query by station and date range with CSV export
Siren Monitoring
- Siren status for all siren-equipped stations (Normal / Warning / Danger)
- Active column shows the last data received timestamp (from siren, rainfall, or waterlevel data)
- Siren history with PDF export
Notifications
- Rainfall, water level, and siren notifications with history
- PDF export for notification history
CCTV
- Live CCTV feed links per station
- Admin-only inline editing of CCTV URLs
Admin Panel
- Station Management — CRUD operations, CSV import/export with sample CSV download
- User Management — CRUD operations, password updates
- Activity Logs — per-user activity log viewer with CSV export (tracks login, logout, CRUD operations, CSV import/export)
Other
- Bilingual support (English / Bahasa Malaysia)
- Responsive design with Boxicons UI
- PDF report generation (siren history, notification history)
- HTTPS via Caddy reverse proxy with auto Let's Encrypt
- Android app detection — EWT link hidden when accessed from Android app
Tech Stack
| Layer | Technology |
|---|---|
| Backend | PHP 8.2, Laravel 12 |
| Database | PostgreSQL 18 |
| Web Server | Nginx (stable-alpine) |
| Frontend | Blade templates, Tailwind CSS, Vite |
| JS Libraries | Boxicons, Flatpickr, Chart.js |
| Maps | OpenStreetMap + Leaflet.js |
| Containerization | Docker, Docker Compose |
| Reverse Proxy | Caddy (auto HTTPS) |
| Asset Building | Vite 7, Node.js LTS |
Architecture
Internet → Caddy (HTTPS/443) → Nginx (8080) → PHP-FPM (9000) → Laravel
│
PostgreSQL (5432)
Additional services:
- pgAdmin (5050) — database management UI
- Dozzle (777) — container log viewer
- FileBrowser (8900) — file management UI
All services run in Docker containers on a Debian LXC (Proxmox), connected via the sides_net bridge network.
Project Structure
sides/
├── .env # Docker Compose environment (DB creds, TZ)
├── docker-compose.yml # Service definitions (app, postgres, web, pgadmin, dozzle, filebrowser)
├── Dockerfile # PHP 8.2-FPM with pgsql, gd, composer, node
├── docker/
│ ├── nginx/default.conf # Nginx config with CSP headers, fastcgi proxy params
│ └── postgres/ # PostgreSQL data directory
├── backup/ # Database backups (e.g., sides_20260528/)
├── src/ # Laravel application root
│ ├── .env # Laravel config (APP_URL, DB, timezone)
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/ # RainfallController, WaterLevelController, SirenController, AdminController, etc.
│ │ │ └── Middleware/ # AdminMiddleware, ForceRequestScheme, TrustProxies
│ │ ├── Models/ # Eloquent models (User, Station, ActivityLog, etc.)
│ │ ├── Exports/ # Maatwebsite Excel exports (StationExport, WaterLevelExport, etc.)
│ │ └── Imports/ # Maatwebsite Excel imports (StationImport)
│ ├── database/migrations/ # 15 migrations (users, station, rainfall, waterlevel, siren, notification, activity_log, etc.)
│ ├── resources/
│ │ ├── views/layout/ # Blade templates (dashboard, rainfall, waterlevel, siren, cctv, admin, etc.)
│ │ ├── js/ # Frontend JavaScript (app.js)
│ │ └── css/ # Tailwind styles
│ ├── routes/web.php # Application routes
│ ├── lang/en/ # English translations
│ ├── lang/bm/ # Bahasa Malaysia translations
│ └── public/ # Document root (index.php, build assets, js, css)
├── filebrowser-data/ # FileBrowser config
└── filebrowser-files/ # FileBrowser shared files
Database Schema
| Table | Description | Key Columns |
|---|---|---|
users |
System users | id, name, email (nullable), password, access_level, is_blocked, login_attempts |
station |
Monitoring stations | stationid (PK, varchar), name, district, lat, lng, rainfall, waterlevel, siren, cctv_link |
rainfall |
Rainfall readings | id (PK, int), stationid (FK), timestamp, interval, rainfall |
waterlevel |
Water level readings | id (PK, int), stationid (FK), datetime, waterlevel |
siren |
Siren events | id (PK), stationid (FK), active_time, level (N/H/L) |
notification |
System notifications | id, stationid, timestamp, title, description |
activity_log |
User activity tracking | id, user_id, user_name, action, subject_type, subject_id, properties (JSON), ip_address, created_at |
sessions |
User sessions (database driver) | |
cache |
Application cache (database driver) |
Notes:
station.stationidis a varchar primary key (e.g.,KBLG0031), not an auto-increment integerrainfall.idandwaterlevel.iduse explicit values set by the external IoT data insertion system- PostgreSQL RULEs are configured for
ON CONFLICT DO NOTHINGon sensor tables to handle duplicate inserts - DB sequences are reset to 300,000+ to avoid conflicts with existing data
activity_log.user_idhas no foreign key constraint — logs are preserved when users are deleted
Requirements
- OS: Debian/Ubuntu Linux (deployed on Proxmox LXC)
- Docker: 24+ with Docker Compose v2
- Ports: 8080 (HTTP), 5432 (PostgreSQL), 5050 (pgAdmin), 777 (Dozzle), 8900 (FileBrowser)
- External: Caddy reverse proxy handling TLS termination on port 443
- Disk: ~2 GB for application + database (238K rainfall records, 138K water level records)
Deployment (Docker)
1. Clone the repository
git clone <repo-url> /root/sides
cd /root/sides
2. Configure environment
# Edit the root .env for Docker services
cp .env.example .env
# Set POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, TZ
# Edit src/.env for Laravel
# Set APP_URL, DB_HOST=postgres, DB_DATABASE, DB_USERNAME, DB_PASSWORD
3. Build and start containers
docker compose build app
docker compose up -d
4. Install Laravel dependencies
docker compose exec app composer install -u root
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed
5. Set file permissions
docker compose exec app chown -R 1000:1000 /var/www/html/storage /var/www/html/bootstrap/cache
6. (Optional) Restore from backup
# Copy backup SQL to postgres container
docker compose cp backup/sides_20260528/sides_db.sql postgres:/tmp/
docker compose exec postgres psql -U sides_user -d sides -f /tmp/sides_db.sql
7. Verify
curl -I http://localhost:8080
Environment Variables
Root .env (Docker Compose)
| Variable | Description | Default |
|---|---|---|
TZ |
Container timezone | Asia/Kuala_Lumpur |
POSTGRES_DB |
Database name | sides |
POSTGRES_USER |
Database user | sides_user |
POSTGRES_PASSWORD |
Database password | (generated) |
PGADMIN_EMAIL |
pgAdmin login email | admin@sides.didkedah.gov.my |
PGADMIN_PASSWORD |
pgAdmin login password | sides2026admin |
src/.env (Laravel)
| Variable | Description | Default |
|---|---|---|
APP_NAME |
Application name | SIDES |
APP_ENV |
Environment | local |
APP_URL |
Public URL | https://sides.tck.com.my |
APP_TIMEZONE |
Application timezone | Asia/Kuala_Lumpur |
DB_CONNECTION |
Database driver | pgsql |
DB_HOST |
Database host (container name) | postgres |
DB_PORT |
Database port | 5432 |
DB_DATABASE |
Database name | sides |
SESSION_DRIVER |
Session driver | database |
User Accounts
All passwords use Laravel's hashed cast. The following accounts exist:
| Username | Access Level | Description | Password |
|---|---|---|---|
admin |
1 (Admin) | System administrator | sides2026 |
jpskedah |
1 (Admin) | JPS Kedah officer | sides2026 |
ijantck |
1 (Admin) | TCK staff | sides2026 |
imam14 |
1 (Admin) | TCK staff | sides2026 |
Security notes:
- Accounts are blocked after 3 failed login attempts (
is_blockedflag,login_attemptscounter) - Blocked accounts must be manually unblocked in the database:
UPDATE users SET is_blocked = false, login_attempts = 0 WHERE name = 'username'; - Password resets are done through the admin User Management panel
Third-Party Packages
Composer (PHP)
| Package | Purpose |
|---|---|
laravel/framework ^12.0 |
Core Laravel framework |
laravel/breeze ^2.3 |
Authentication scaffolding |
barryvdh/laravel-dompdf ^3.1 |
PDF generation (siren history, notifications) |
maatwebsite/excel ^3.1 |
CSV/Excel import and export |
google/auth ^1.49 |
Google Auth library (Firebase push notifications) |
laravel/tinker ^2.10 |
Interactive REPL |
NPM (Build)
| Package | Purpose |
|---|---|
vite ^7.0.7 |
Asset bundler |
tailwindcss ^3.1.0 |
CSS framework |
alpinejs ^3.4.2 |
Lightweight JS reactivity |
axios ^1.11.0 |
HTTP client |
laravel-vite-plugin ^2.0 |
Vite integration for Laravel |
Frontend Libraries (CDN)
Loaded via CDN in Blade templates (see CSP headers in docker/nginx/default.conf):
| Library | Source | Purpose |
|---|---|---|
| Boxicons | cdn.jsdelivr.net |
Icon library |
| Flatpickr | cdn.jsdelivr.net |
Date/time picker |
| Chart.js | cdn.jsdelivr.net |
Rainfall/water level graphs |
| jQuery | code.jquery.com |
DOM manipulation |
| Bootstrap 5 | cdn.jsdelivr.net |
CSS framework (admin panels) |
| Leaflet.js | unpkg.com |
OpenStreetMap integration |
| Data Tables | cdn.datatables.net |
Sortable/searchable tables |
Localization
SIDES supports two languages:
- English (
en) —src/lang/en/messages.php,src/lang/en/toast.php - Bahasa Malaysia (
bm) —src/lang/bm/messages.php,src/lang/bm/toast.php
Language switcher available in the navbar via the /locale/{locale} route.
Activity Logging
User actions are tracked in the activity_log table via the ActivityLog::record() helper. Logged actions include:
| Category | Actions |
|---|---|
| Auth | login, logout |
| Stations | create_station, update_station, delete_station |
| Users | create_user, update_user, update_password, delete_user |
| CSV | import_stations_csv, export_stations_csv |
| CCTV | update_cctv_link |
| Logs | export_user_logs |
Activity logs are accessible from User Management → View Logs (per-user), with CSV export.
Android App Integration
The system detects Android user agents and hides the Early Warning Threshold (EWT) nav link when accessed from the Android companion app. Detection is server-side:
$isAndroid = preg_match('/Android/i', request()->userAgent());
Backup & Restore
Create a backup
docker compose exec postgres pg_dump -U sides_user sides > backup/sides_$(date +%Y%m%d).sql
Restore from backup
docker compose cp backup/sides_YYYYMMDD/sides_db.sql postgres:/tmp/
docker compose exec postgres psql -U sides_user -d sides -f /tmp/sides_db.sql
Reset sequences after restore
docker compose exec postgres psql -U sides_user -d sides -c "
SELECT setval('rainfall_id_seq', 300000);
SELECT setval('waterlevel_id_seq', 300000);
SELECT setval('users_id_seq', (SELECT MAX(id) + 1 FROM users));
SELECT setval('notification_id_seq', (SELECT MAX(id) + 1 FROM notification));
"
Service Ports
| Service | Container | Port | URL |
|---|---|---|---|
| Nginx (HTTP) | sides-web |
8080 | http://localhost:8080 |
| PostgreSQL | sides-db |
5432 | localhost:5432 |
| pgAdmin | sides-pgAdmin |
5050 | http://localhost:5050 |
| Dozzle (Logs) | — | 777 | http://localhost:777 |
| FileBrowser | quantum-prod |
8900 | http://localhost:8900 |
| SIDES (public) | — | 443 | https://sides.tck.com.my (via Caddy) |
Useful Commands
# Rebuild and restart
docker compose up -d --build
# View logs
docker compose logs -f app
docker compose logs -f web
# Artisan commands (run as root due to vendor permissions)
docker compose exec -u root app php artisan migrate
docker compose exec -u root app php artisan route:clear
docker compose exec -u root app php artisan view:clear
docker compose exec -u root app php artisan config:clear
docker compose exec -u root app composer dump-autoload
# Copy file to container
docker compose cp src/path/to/file app:/var/www/html/path/to/file