Files
sides/docs/02-ARCHITECTURE.md
root 9122deaacd fix: seeder idempotent with firstOrCreate
Use firstOrCreate instead of create so db:seed can run safely
on container restart without duplicate key violation.
2026-05-21 02:31:47 +08:00

166 lines
7.7 KiB
Markdown

# Architecture
## Technology Stack
| Layer | Technology | Version |
|-------|-----------|---------|
| Backend | Laravel (PHP) | 12.x |
| PHP | PHP-FPM | 8.2 |
| Frontend | Blade Templates + Alpine.js | 3.x |
| CSS | Tailwind CSS + Bootstrap 5.3 | 3.x / 5.3 |
| Charts | Custom JS (Chart.js via `graph.js`) | — |
| Maps | Leaflet.js | 1.9.4 |
| Build Tool | Vite | 7.x |
| Database | PostgreSQL | 15 |
| Cache/Session | Database (via Laravel) | — |
| Queue | Database (via Laravel) | — |
| Containerization | Docker + Docker Compose | 3.9 |
| Web Server | Nginx | stable-alpine |
| PDF Generation | barryvdh/laravel-dompdf | 3.1 |
| Excel Export | maatwebsite/excel | 3.1 |
| Push Notifications | Firebase Cloud Messaging (FCM) via Google Auth | 1.49 |
| Date Picker | Flatpickr | 4.6 |
| Auth Scaffold | Laravel Breeze | 2.3 |
| Data Pipeline | Python (psycopg2, ftplib) | 3.x |
## Container Architecture
The application runs in 5 Docker containers defined in `docker-compose.yml`:
```
┌──────────────────────────────────────────────────────────┐
│ Docker Network: tckdev_net │
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ web │ │ app │ │ postgres │ │
│ │ (nginx) │──>│ (PHP-FPM│──>│ (DB) │ │
│ │ :80 │ │ :9000) │ │ :5432 │ │
│ └─────────┘ └─────────┘ └──────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ pgAdmin │ │ adminer │ │
│ │ :5050 │ │ :6060 │ (DB management tools) │
│ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────┘
```
### Container Details
| Container | Image | Port | Purpose |
|-----------|-------|------|---------|
| `tckdev-app` | Custom (PHP 8.2-FPM + Composer 2.3 + Node.js) | 9000 (internal) | Laravel application |
| `tckdev-web` | nginx:stable-alpine | 80 | Reverse proxy & static files |
| `tckdev-db` | postgres:15 | 5432 | PostgreSQL database |
| `tckdev-pgAdmin` | dpage/pgadmin4 | 5050 | Database management UI |
| `tckdev-adminer` | adminer | 6060 | Lightweight DB management UI |
## Request Flow
```
Browser --> Nginx (:80) --> PHP-FPM (:9000) --> Laravel Router
|
┌─────────────┼─────────────────┐
v v v
Web Routes API Routes Auth Routes
(web.php) (api.php) (auth.php)
| | |
v v v
Controllers Api/Controllers Breeze Controllers
| |
v v
DB::raw() SQL DB::raw() SQL
| |
v v
PostgreSQL PostgreSQL
|
v
Blade Views --> HTML Response
```
## Middleware Stack
| Middleware | Route Group | Purpose |
|-----------|------------|---------|
| `auth` | Web (protected) | Require authenticated session |
| `admin` | Admin routes | Require `access_level = 1` |
| `LocalizationMiddleware` | All web routes | Set locale from session (`en`/`bm`) |
| `guest` | Auth routes (login/register) | Redirect away if already authenticated |
## Database Design
### Core Tables
```
┌─────────────┐ ┌──────────────┐
│ station │ │ users │
│─────────────│ │──────────────│
│ stationid* │ │ id* │
│ name │ │ name │
│ district │ │ email │
│ lng, lat │ │ password │
│ mainriverbasin│ │ access_level │
│ subriverbasin│ │ is_blocked │
│ rainfall │ │ login_attempts│
│ waterlevel │ └──────────────┘
│ siren │
│ cctv_link │
└──────┬──────┘
│ (stationid FK via application logic, not DB constraint)
┌────┼────────────┬──────────────┐
v v v v
┌──────────┐ ┌────────────┐ ┌───────────┐
│ rainfall │ │ waterlevel │ │ siren │
│──────────│ │────────────│ │───────────│
│ id* │ │ id* │ │ id* │
│ stationid│ │ stationid │ │ stationid │
│ timestamp│ │ datetime │ │ stationtype│
│ anncum │ │ waterlevel │ │ active_time│
│ daily │ │ alert │ │ level │
│ hourly │ │ warning │ └───────────┘
│ currentrf│ │ danger │
│ battery │ └────────────┘
└──────────┘
v
┌──────────────┐
│ notification │
│──────────────│
│ id* │
│ stationid │
│ timestamp │
│ stationtype │ (1=rainfall, 2=waterlevel, 3=siren)
│ level │ (Normal, Alert, Warning, Danger)
│ active_time │
└──────────────┘
```
### Station Types
The `station` table uses boolean flags to indicate which monitoring types each station supports:
- `rainfall = 1` — Station has rainfall monitoring
- `waterlevel = 1` — Station has water level monitoring
- `siren = 1` — Station has a warning siren
### Foreign Key Note
There are **no database-level foreign keys** between `station` and the data tables. Relationships are maintained at the application level via `stationid` column matching.
### Laravel Standard Tables
The application also uses standard Laravel tables:
- `users` — Authentication and authorization
- `password_reset_tokens` — Password reset flow
- `sessions` — Database-backed sessions
- `cache`, `cache_locks` — Database cache store
- `jobs`, `job_batches`, `failed_jobs` — Database queue
## Authentication & Authorization
- **Web auth**: Laravel Breeze (session-based, Blade views)
- **API auth**: Custom token-less login via `Api\AuthController` (returns user info, no token generation)
- **Admin access**: Controlled by `AdminMiddleware` checking `access_level === 1`
- **User blocking**: Users can be blocked via `is_blocked` flag (though no middleware enforces it currently)