Compare commits
9 Commits
6863f39a24
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
226e142e3e | ||
|
|
e6a2001840 | ||
|
|
ed2c05d277 | ||
|
|
bed954bc35 | ||
|
|
cfedeb38df | ||
|
|
d68afb7d03 | ||
|
|
0937d14faf | ||
|
|
72036cb2c1 | ||
|
|
e22644b32b |
109
README.md
109
README.md
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user