# 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](https://sides.tck.com.my). --- ## Table of Contents - [Features](#features) - [Tech Stack](#tech-stack) - [Architecture](#architecture) - [Project Structure](#project-structure) - [Database Schema](#database-schema) - [Requirements](#requirements) - [Deployment (Docker)](#deployment-docker) - [Environment Variables](#environment-variables) - [User Accounts](#user-accounts) - [Third-Party Packages](#third-party-packages) - [Frontend Libraries (CDN)](#frontend-libraries-cdn) - [Localization](#localization) - [Activity Logging](#activity-logging) - [Android App Integration](#android-app-integration) - [Backup & Restore](#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.stationid` is a varchar primary key (e.g., `KBLG0031`), not an auto-increment integer - `rainfall.id` and `waterlevel.id` use explicit values set by the external IoT data insertion system - PostgreSQL RULEs are configured for `ON CONFLICT DO NOTHING` on sensor tables to handle duplicate inserts - DB sequences are reset to 300,000+ to avoid conflicts with existing data - `activity_log.user_id` has 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 ```bash git clone /root/sides cd /root/sides ``` ### 2. Configure environment ```bash # 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 ```bash docker compose build app docker compose up -d ``` ### 4. Install Laravel dependencies ```bash 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 ```bash docker compose exec app chown -R 1000:1000 /var/www/html/storage /var/www/html/bootstrap/cache ``` ### 6. (Optional) Restore from backup ```bash # 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 ```bash 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_blocked` flag, `login_attempts` counter) - 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: ```php $isAndroid = preg_match('/Android/i', request()->userAgent()); ``` --- ## Backup & Restore ### Create a backup ```bash docker compose exec postgres pg_dump -U sides_user sides > backup/sides_$(date +%Y%m%d).sql ``` ### Restore from backup ```bash 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 ```bash 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 ```bash # 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 ```