# Architecture ## System Overview SIDES (Sistem Informasi Data dan Early Warning System) is a Laravel 12 web application that collects, stores, and displays real-time environmental monitoring data from rainfall, water level, and siren stations. It provides a dashboard with a map view of station status, historical data with export capabilities (Excel, PDF), and push notifications via Firebase Cloud Messaging (FCM). The system runs entirely in Docker containers using nginx as a reverse proxy, PHP-FPM for application code, and PostgreSQL 16 for persistence. External IoT stations submit readings through a public REST API; authenticated admin users manage stations and user accounts through a Blade-based web UI. ## Container Architecture ``` +------------------+ | User Browser | +--------+---------+ | | :80 v +--------+---------+ | web (nginx) | | nginx:stable- | | alpine | +--------+---------+ | fastcgi :9000 | +--------+---------+ ./src -----> | app (php-fpm) | (bind mount) | php:8.2-fpm | +--------+---------+ | pgsql :5432 | +--------+---------+ +------------------+ | postgres | | pgadmin | | postgres:16 | | dpage/pgadmin4 | +------------------+ +------------------+ 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 | |------------|---------------------|--------|--------------------------------------------| | tckdev-app | php:8.2-fpm (custom)| 9000 | Laravel application (PHP-FPM) | | tckdev-db | postgres:16 | 5432 | PostgreSQL database | | tckdev-web | nginx:stable-alpine | 80 | Reverse proxy, serves static assets | | tckdev-pgAdmin | dpage/pgadmin4 | 5050 | Database administration UI | ### Networking and Volumes - All containers share the `tckdev_net` bridge network. - `src/` is bind-mounted into both `app` and `web` at `/var/www/html`, so code changes are reflected without rebuilding. - `pgdata` and `pgadmin_data` are Docker named volumes -- not bind mounts. - The nginx config is bind-mounted from `docker/nginx/default.conf`. - `docker/backup` is bind-mounted into pgadmin at `/backups`. ### Entrypoint Behaviour The `app` container runs `docker/entrypoint.sh` before starting PHP-FPM. This script: 1. Copies `.env.example` to `.env` if no `.env` exists. 2. Runs `composer install` if `vendor/` is missing. 3. Runs `npm install` if `node_modules/` is missing. 4. Runs `npm run build` (Vite) if `public/build/` is missing. 5. Runs `php artisan migrate --force` when `RUN_MIGRATIONS=true`. 6. Runs `php artisan db:seed --force` when `RUN_SEEDER=true`. 7. Caches config, routes, and views for production performance. ## Request Flow ### Web UI Request (Blade pages) ``` Browser --:80--> nginx | +-- static asset? --> /var/www/html/public/{file} | +-- *.php? ---------> fastcgi://app:9000 | Laravel Router | +-- auth middleware --> Breeze session auth +-- admin middleware --> access_level === 1 check | Controller | DB::table() / DB::select() | v PostgreSQL | 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`. 4. Laravel routes the request through middleware (`auth`, `admin`, or none). 5. Controllers query PostgreSQL using `DB::table()` or `DB::select()` with parameterized bindings. 6. Responses are rendered Blade views (HTML) or JSON for AJAX endpoints. ### API Request (IoT station data) ``` IoT Device --:80/api/...--> nginx ---> app:9000 | Laravel API Router (no auth middleware on data endpoints) | Controller (Api\*) | DB::table() / DB::select() | v 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 The application uses six database tables. There are no foreign key constraints; relationships are enforced at the application level. `User` is the only Eloquent model. All other data access uses the `DB` query builder with parameterized queries. ``` +----------------+ +----------------+ +----------------+ | station | | rainfall | | waterlevel | |----------------| |----------------| |----------------| | stationid (PK) |<--+---| id (PK) | +----| id (PK) | | name | | | stationid | | | stationid | | district | | | timestamp | | | datetime | | lng | | | anncum | | | waterlevel | | lat | | | daily | | | alert | | mainriverbasin | | | hourly | | | warning | | subriverbasin | | | currentrf | | | danger | | rainfall (flag)| | | battery | | +----------------+ | waterlevel (f) | | +----------------+ | | siren (flag) | | | | cctv_link | | +----------------+ | +----------------+ +----------------+ | | siren | | | notification | +---| id (PK) | +---| id (PK) | | stationid | | stationid | | stationtype | | timestamp | | active_time | | stationtype | | level | | level | +----------------+ | active_time | +----------------+ +----------------+ | users | |----------------| | id (PK) | | name | | email | | access_level | | password | | login_attempts | | is_blocked | +----------------+ ``` ```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 | |---------------|--------------------------------------------------------| | `station` | Master list of monitoring stations with location and capability flags | | `rainfall` | Timestamped rainfall readings per station | | `waterlevel` | Timestamped water level readings with alert/warning/danger thresholds | | `siren` | Siren activation events with level (N/D/W/A) | | `notification`| Notification log entries linking stations to alert levels | | `users` | Application users; `access_level=1` for admin | ## Application Layer ### Controllers **Web controllers** (render Blade views): | Controller | Responsibility | |-------------------------|-------------------------------------------------------| | `MapController` | Dashboard/map view; joins station with latest readings | | `RainfallController` | Rainfall display, graphs, historical data, Excel export | | `WaterLevelController` | Water level display, graphs, historical data, Excel export | | `SirenController` | Siren status display, history, PDF export | | `NotificationController`| Rainfall/water level/siren notification views and PDF exports | | `AdminController` | Station and user CRUD (admin-only) | | `cctvController` | CCTV feed display | | `LocaleController` | Language switching | | `ProfileController` | User profile management (Breeze) | | Auth controllers | Login, register, password reset (Breeze scaffolding) | **API controllers** (return JSON): | Controller | Responsibility | |-------------------------|-------------------------------------------------------| | `Api\StationController` | Current data, rainfall, water level, notifications, siren status for IoT/external consumption | | `Api\AuthController` | Token-less login; validates username/password, returns user JSON | | `Api\AlertController` | Routes FCM push notifications to the correct topic based on stationtype and level | ### Middleware | Middleware | Behavior | |-------------------------|-------------------------------------------------------| | `auth` | Redirects unauthenticated users to login (Breeze) | | `admin` | Checks `access_level === 1`; redirects non-admins to dashboard | | `LocalizationMiddleware`| Sets application locale from session | ### Services | Service | Responsibility | |---------------|--------------------------------------------------------------| | `FcmService` | Obtains Google OAuth2 access token from Firebase service account credentials, sends FCM v1 API messages to topics | ### FCM Topic Routing `AlertController::send()` determines the FCM topic from the request's `stationtype` and `level`: | stationtype | Level | FCM Topic | |-------------|----------|---------------------------| | 1 | Warning | `FCM_TOPIC_RAINFALL_WARNING` | | 1 | Danger | `FCM_TOPIC_RAINFALL_DANGER` | | 2 | Warning | `FCM_TOPIC_WATERLEVEL_ALERT` | | 2 | Danger | `FCM_TOPIC_WATERLEVEL_DANGER` | | 3 | (any) | `FCM_TOPIC_RAINFALL_WARNING` | ## Authentication Two separate authentication mechanisms coexist: - **Web UI**: Laravel Breeze provides session-based authentication (login, register, password reset, email verification). The `auth` middleware guards protected routes. - **API**: `POST /api/login` accepts `username` and `password`, validates against the `users` table, and returns user data as JSON. There is no token or session persisted for API consumers -- each API data endpoint is stateless and unauthenticated. ## Frontend Stack - Blade templates with Tailwind CSS (via `tailwind.config.js`). - Vite handles asset bundling (`vite.config.js`, `postcss.config.js`). - Map rendering uses Leaflet with station markers. - Client-side JavaScript in `public/js/` handles graph rendering and AJAX calls. ## Database Configuration - **Connection**: `pgsql` (PostgreSQL 16) -- configured in `.env` as `DB_CONNECTION=pgsql`. - **Host**: `postgres` (Docker service name). - **Session driver**: `database` (Laravel's database session storage). - **Cache store**: `database`. - **Queue connection**: `database`. All session, cache, and queue data lives in PostgreSQL tables created by Laravel's default migrations (`cache`, `jobs`, `sessions` tables). ## Directory Structure ``` tckdev/ docker/ Docker configuration entrypoint.sh App container startup script nginx/ default.conf nginx reverse proxy config src/ Laravel application (bind-mounted into containers) app/ Http/ Controllers/ Web controllers Controllers/Api/ API controllers (StationController, AuthController, AlertController) Middleware/ AdminMiddleware, LocalizationMiddleware Requests/ Form request validation Exports/ Maatwebsite Excel export classes Models/ User (only Eloquent model) Notifications/ ResetPasswordNotification Services/ FcmService Providers/ AppServiceProvider View/Components/ AppLayout, GuestLayout (Blade components) bootstrap/ config/ Laravel config files database/ migrations/ Schema migrations (13 files) seeders/ Database seeders factories/ Model factories public/ Document root (served by nginx) resources/ views/ Blade templates routes/ web.php Web routes (authenticated + admin + public) api.php API routes (stateless, no auth middleware) auth.php Breeze auth routes console.php Artisan commands storage/ App storage (logs, firebase credentials, uploads) tests/ PHPUnit tests docker-compose.yml Service definitions (Compose v2, no version key) Dockerfile Custom php:8.2-fpm image ```