Compare commits

...

9 Commits

Author SHA1 Message Date
root
226e142e3e Add autoscript log viewer page in admin panel 2026-05-21 15:37:57 +08:00
root
e6a2001840 Fix nginx healthcheck: use pid file, fix pgadmin healthcheck: use file-size check for busybox wget 2026-05-21 11:32:32 +08:00
root
ed2c05d277 Fix app healthcheck: use /proc/1/cmdline instead of pgrep 2026-05-21 11:24:47 +08:00
root
bed954bc35 Remove tckdev from outer repo (it's its own repository) 2026-05-21 11:21:33 +08:00
root
cfedeb38df Add station CSV import, navbar dropdown hover fix, and docker health checks 2026-05-21 11:21:33 +08:00
root
d68afb7d03 Add docker health checks, CSV import docs 2026-05-21 11:21:20 +08:00
root
0937d14faf fix: set timezone to Asia/Kuala_Lumpur on all containers
Add TZ=Asia/Kuala_Lumpur and PGTZ to all services, mount
/etc/localtime read-only so log timestamps match local time.
2026-05-21 07:43:17 +08:00
root
72036cb2c1 docs: sync latest doc updates to repo 2026-05-21 03:50:53 +08:00
root
e22644b32b docs: rewrite README for SIDES project
Replace template boilerplate with project description, quick start,
documentation index linking to docs/, stack summary, and project structure.
2026-05-21 03:39:23 +08:00
6 changed files with 308 additions and 56 deletions

109
README.md
View File

@@ -1,65 +1,64 @@
# Laravel using PostgreSQL in Docker
# SIDES
<p align="center">
<img src="./docker/image/laravel+docker.png" alt="docker+laravel">
</p>
**Sabo Integrated Debris Flow Monitoring and Early Warning System**
## Introduction
Flood and debris flow early warning system for Sungai Kupang, Malaysia. Operated under Jabatan Pengairan dan Saliran (JPS) — the Malaysian Department of Irrigation and Drainage.
Build a simple laravel application development environment with docker compose.
Monitors rainfall, water levels, and siren activations at remote telemetry stations with a real-time web dashboard, interactive map, and push notifications.
## Requirement
- Docker ^19.*
## Installation
1. Git clone & move to working directory
2. Settings your credentials, copy `.env.example` to `.env`
3. Execute the following command for create application
## Quick Start
```bash
$ make create-project
git clone <repo-url> && cd tckdev
cp .env.example .env # Configure credentials
cp src/.env.example src/.env # Configure Laravel
docker compose up -d --build
```
4. Next, set environment DB for app laravel in `src/.env` variable :
The app container automatically runs migrations, seeding, and asset building on first start. Access the dashboard at `http://localhost`.
## Documentation
Full documentation is in [`docs/`](docs/):
| Document | Description |
|----------|-------------|
| [01-OVERVIEW](docs/01-OVERVIEW.md) | Project overview, purpose, and high-level architecture |
| [02-ARCHITECTURE](docs/02-ARCHITECTURE.md) | Technology stack, container layout, request flow, middleware |
| [03-DEPLOYMENT](docs/03-DEPLOYMENT.md) | Docker Compose deployment, reverse proxy setup, production checklist |
| [04-DATA-PIPELINE](docs/04-DATA-PIPELINE.md) | Python autoscript, FTP ingestion, CSV parsing, alert triggers |
| [05-FEATURES](docs/05-FEATURES.md) | User-facing features, routes, and access control |
| [06-DATABASE](docs/06-DATABASE.md) | PostgreSQL schema, tables, relationships |
| [07-API](docs/07-API.md) | REST API reference for station data and push notifications |
| [08-CONFIGURATION](docs/08-CONFIGURATION.md) | Environment variables, Docker Compose config, Nginx, Laravel |
## Stack
- **Backend**: Laravel 12 (PHP 8.2-FPM)
- **Database**: PostgreSQL 16
- **Frontend**: Blade + Alpine.js + Tailwind CSS + Leaflet.js
- **Push Notifications**: Firebase Cloud Messaging
- **Data Pipeline**: Python (psycopg2, ftplib)
- **Deployment**: Docker Compose (nginx, php-fpm, postgres, pgadmin)
## Project Structure
```
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=<your_db_name> // same in root .env variable POSTGRES_DB
DB_USERNAME=<your_db_user> // same in root .env variable POSTGRES_USER
DB_PASSWORD=<your_db_password> // same in root .env variable POSTGRES_PASSWORD
tckdev/
├── src/ # Laravel application (bind-mounted into containers)
│ ├── app/Http/Controllers/
│ ├── database/migrations/
│ ├── routes/
│ ├── resources/views/
│ └── .env
├── autoscript/
│ └── sidesdecode.py # Python data ingestion pipeline
├── docker/
│ ├── nginx/default.conf
│ └── entrypoint.sh
├── docs/ # Documentation wiki
├── docker-compose.yml
├── Dockerfile
├── .env # Docker-level credentials
└── .env.example
```
5. show application in [http://localhost:85](http://localhost:85)
<img src="./docker/image/app.png" alt="app+laravel">
1. show adminer in [http://localhost:8080](http://localhost:8080)
<img src="./docker/image/adminer.png" alt="adminer">
7. show pgadmin in [http://localhost:5050](http://localhost:5050)
<img src="./docker/image/pgadmin.png" alt="pgadmin">
8. list execute command in [Makefile](Makefile).
## Container details :
- ``app`` use image:
- [php](https://hub.docker.com/_/php):8.2-fpm
- [composer](https://hub.docker.com/_/composer):2.3
- [npm](https://deb.nodesource.com/setup_lts.x):latest
- ``web`` use image:
- [nginx](https://hub.docker.com/_/nginx):stable-alpine
- ``db`` use image:
- [postgres](https://hub.docker.com/_/postgres):15
- ``adminer`` use image:
- [adminer](https://hub.docker.com/_/adminer):latest
*Optional*
- ``pgadmin`` use image:
- [pgadmin](https://hub.docker.com/_/pgadmin):latest

View File

@@ -10,13 +10,22 @@ services:
dockerfile: Dockerfile
volumes:
- ./src:/var/www/html
- ./autoscript/:/var/log/sides/autoscript:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
postgres:
condition: service_healthy
networks:
- tckdev_net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "grep -q php-fpm /proc/1/cmdline || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
environment:
- TZ=Asia/Kuala_Lumpur
- RUN_MIGRATIONS=true
- RUN_SEEDER=true
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
@@ -28,7 +37,10 @@ services:
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
environment:
- TZ=Asia/Kuala_Lumpur
- PGTZ=Asia/Kuala_Lumpur
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
@@ -51,20 +63,32 @@ services:
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- /etc/localtime:/etc/localtime:ro
depends_on:
- app
app:
condition: service_healthy
networks:
- tckdev_net
healthcheck:
test: ["CMD-SHELL", "test -f /var/run/nginx.pid"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
environment:
- TZ=Asia/Kuala_Lumpur
pgadmin:
image: dpage/pgadmin4
container_name: tckdev-pgAdmin
environment:
- TZ=Asia/Kuala_Lumpur
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./backup:/backups
- /etc/localtime:/etc/localtime:ro
ports:
- "5050:80"
depends_on:
@@ -73,6 +97,12 @@ services:
networks:
- tckdev_net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget http://localhost:80/login -O /tmp/hc 2>/dev/null; test -s /tmp/hc"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
pgdata:

View File

@@ -45,6 +45,33 @@ SIDES (Sabo Integrated Debris Flow Monitoring and Early Warning System) is a web
+---------+
```
```mermaid
flowchart TD
I["Internet"]:::internet --> RP["Reverse Proxy (TLS termination)"]:::proxy
RP --> NET["Docker Network (tckdev_net)"]:::network
subgraph NET["Docker Network"]
N["nginx (tckdev-web :80)"]:::server --> P["php-fpm (tckdev-app :9000)"]:::app --> DB["PostgreSQL (tckdev-db :5432)"]:::db
PG["pgAdmin (tckdev-pgAdmin :5050)"]:::tool --> DB
V["pgdata volume"]:::volume --> DB
end
P --> L["Laravel 12 Application"]:::laravel
L --> F["FCM API"]:::api
classDef internet fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef proxy fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
classDef network fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef app fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
classDef tool fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
classDef volume fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
classDef laravel fill:#8BC34A,stroke:#333,stroke-width:2px,color:#fff;
classDef api fill:#607D8B,stroke:#333,stroke-width:2px,color:#fff;
```
All four services run as Docker containers on a single host:
| Container | Image | Purpose |

View File

@@ -37,6 +37,21 @@ SIDES (Sistem Informasi Data dan Early Warning System) is a Laravel 12 web appli
volume: pgdata volume: pgadmin_data
```
```mermaid
flowchart TD
UB["User Browser"]:::client -->|:80| N["web (nginx)\nnginx:stable-alpine"]:::server
N -->|fastcgi :9000| A["app (php-fpm)\nphp:8.2-fpm\n(bind mount ./src)"]:::app
A -->|pgsql :5432| DB["postgres\npostgres:16\nvolume: pgdata"]:::db
PG["pgAdmin\ndpage/pgadmin4\nvolume: pgadmin_data"]:::tool --> DB
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef app fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
classDef tool fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
```
### Container Summary
| Container | Image | Port | Purpose |
@@ -92,6 +107,34 @@ Browser --:80--> nginx
Blade view / JSON response
```
```mermaid
flowchart TD
B["Browser"]:::client -->|:80| N["nginx"]:::server
N -->|static asset?| SA["/var/www/html/public/{file}"]:::static
N -->|*.php?| P["fastcgi://app:9000"]:::php
P --> R["Laravel Router"]:::router
R --> AM["auth middleware\n(Breeze session auth)"]:::middleware
R --> ADM["admin middleware\n(access_level === 1 check)"]:::middleware
R --> C["Controller"]:::controller
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
DB --> V["Blade view / JSON response"]:::response
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef static fill:#9E9E9E,stroke:#333,stroke-width:2px,color:#fff;
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
classDef middleware fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
classDef response fill:#607D8B,stroke:#333,stroke-width:2px,color:#fff;
```
1. Browser hits port 80 on the `web` container.
2. nginx serves static files from `/var/www/html/public` directly.
3. PHP requests are forwarded via FastCGI to `app:9000`.
@@ -114,6 +157,24 @@ IoT Device --:80/api/...--> nginx ---> app:9000
PostgreSQL
```
```mermaid
flowchart TD
D["IoT Device"]:::device -->|:80/api/...| N["nginx"]:::server
N --> P["app:9000 (php-fpm)"]:::php
P --> R["Laravel API Router\n(no auth middleware on data endpoints)"]:::router
R --> C["Controller (Api\\*)"]:::controller
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
classDef device fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
```
API routes under `/api/` have no middleware. The `POST /api/login` endpoint validates credentials against the `users` table using `DB::select()` with bound parameters and `Hash::check()`. There are no API tokens -- authentication is a single request/response returning user data on success.
## Data Model
@@ -156,6 +217,77 @@ The application uses six database tables. There are no foreign key constraints;
+----------------+
```
```mermaid
erDiagram
station {
int stationid
string name
string district
float lng
float lat
string mainriverbasin
string subriverbasin
boolean rainfall_flag
boolean waterlevel_flag
boolean siren_flag
string cctv_link
}
rainfall {
int id
int stationid
datetime timestamp
float anncum
float daily
float hourly
float currentrf
float battery
}
waterlevel {
int id
int stationid
datetime datetime
float waterlevel
string alert
string warning
string danger
}
siren {
int id
int stationid
string stationtype
datetime active_time
string level
}
notification {
int id
int stationid
datetime timestamp
string stationtype
string level
datetime active_time
}
users {
int id
string name
string email
string access_level
string password
int login_attempts
boolean is_blocked
}
station ||--o{ rainfall : "has"
station ||--o{ waterlevel : "has"
station ||--o{ siren : "has"
station ||--o{ notification : "has"
```
### Table Descriptions
| Table | Purpose |

View File

@@ -93,8 +93,11 @@ These routes require both authentication and admin privileges (`admin` middlewar
|---|---|
| `GET /stationmanagement` | List all stations (paginated, 5 per page). |
| `POST /stationmanagement/store` | Add a new station. |
| `POST /stationmanagement/import` | Bulk import stations from a CSV file. |
| `GET /stationmanagement/csvtemplate` | Download a sample CSV template for station imports. |
| `POST /stationmanagement/{stationid}/update` | Edit an existing station. |
| `DELETE /stationmanagement/{stationid}/delete` | Delete a station. |
| `GET /autoscriptlogs` | View data pipeline log files (error log and main log). |
### User Management

View File

@@ -61,6 +61,67 @@ persistent storage. Data survives container restarts and rebuilds.
|--------------|
```
```mermaid
erDiagram
station {
int stationid
string name
string district
float lng
float lat
string mainriverbasin
string subriverbasin
float rainfall
float waterlevel
boolean siren
string cctv_link
}
rainfall {
int id
int stationid
datetime timestamp
float anncum
float daily
float hourly
float currentrf
float battery
}
waterlevel {
int id
int stationid
datetime datetime
float waterlevel
string alert
string warning
string danger
}
siren {
int id
int stationid
string stationtype
datetime active_time
string level
}
notification {
int id
int stationid
datetime timestamp
string stationtype
string level
datetime active_time
}
station ||--o{ rainfall : "has"
station ||--o{ waterlevel : "has"
station ||--o{ siren : "has"
station ||--o{ notification : "has"
```
**Note:** There are no database-level foreign keys between `station` and the
data tables. Relationships are enforced at the application level by matching
`stationid` values.