Files
sides/.planning/codebase/ARCHITECTURE.md
2026-05-28 16:25:22 +08:00

245 lines
16 KiB
Markdown

<!-- refreshed: 2026-05-28 -->
# Architecture
**Analysis Date:** 2026-05-28
## System Overview
This is a **Laravel 12 MVC** application — the **Sabo Integrated Debris Flow Monitoring and Early Warning System (SIDES)** for Sungai Kupang, Baling, Malaysia. It monitors rainfall, water level, and siren station data from debris flow monitoring stations, serves a dashboard with Leaflet maps + Chart.js graphs, sends Firebase Cloud Messaging (FCM) alerts, and exports data to Excel/PDF.
```text
┌─────────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ Blade Layouts (layout.app, layout.homeapp) / PDF Templates / JSON │
│ `resources/views/layout/`, `resources/views/pdf/` │
├──────────────────┬────────────────────┬─────────────────────────────┤
│ Web Routes │ API Routes │ Console / Artisan │
│ `routes/web.php`│ `routes/api.php` │ `routes/console.php` │
├─────────┬────────┴─────────┬──────────┴──────────┬──────────────────┤
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │Controllers │ │ Auth Ctrlrs │ │ API Controllers │ │
│ │ Web │ │ (Breeze) │ │ (JSON responses) │ │
│ └──────┬─────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ FcmService (`app/Services/FcmService.php`) │ │
│ │ → Firebase Cloud Messaging (topic-based push) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Eloquent Model Layer (minimal — only User model used) │ │
│ │ All other data access via DB::table() / DB::select() │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ Tables: users, station, rainfall, waterlevel, siren, │ │
│ │ notification, password_reset_tokens, sessions, │ │
│ │ cache, jobs, job_batches │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Component Responsibilities
| Component | Responsibility | File(s) |
|-----------|----------------|---------|
| Web Controllers | Serve Blade views with paginated/aggregated station data | `app/Http/Controllers/{Map,Rainfall,WaterLevel,Siren,Notification,cctv,Admin,Profile,Locale}Controller.php` |
| Auth Controllers | Login/Register/Password Reset/Verify Email via Laravel Breeze | `app/Http/Controllers/Auth/*.php` (9 files) |
| API Controllers | Return JSON for external IoT/second-screen consumption | `app/Http/Controllers/Api/{Station,Auth,Alert}Controller.php` |
| MapController | Dashboard entry — joins station + latest rainfall + latest waterlevel + latest siren | `app/Http/Controllers/MapController.php` |
| AdminController | CRUD for stations and users (access_level=1 gate) | `app/Http/Controllers/AdminController.php` |
| FcmService | Google OAuth2 token fetch → FCM v1 HTTP API topic push | `app/Services/FcmService.php` |
| Export Layer | Excel (Maatwebsite) + PDF (DomPDF) generation | `app/Exports/*.php`, `resources/views/pdf/*.blade.php` |
| Middleware | Authentication gate + admin role check + locale | `app/Http/Middleware/{Admin,Localization}Middleware.php` |
## Pattern Overview
**Overall:** Model-View-Controller with heavy use of raw SQL (PostgreSQL dialect) and **zero Eloquent ORM outside the User model**.
**Key Characteristics:**
- All database queries use `DB::table()` or `DB::select()` — not Eloquent models
- Only `User` model (`app/Models/User.php`) extends `Authenticatable` — used for auth + password reset notification override
- Controllers return Blade views (`view()`) for web, JSON (`response()->json()`) for API
- No repository/DAO abstraction layer — queries live directly in controllers
- Single service class (`FcmService`) handles Firebase Cloud Messaging
- Localization via session-based middleware with two locales: English (`en`) and Bahasa Malaysia (`bm`)
## Layers
**Web Controllers (Presentation + Logic):**
- Purpose: Handle HTTP requests, query data via raw SQL, return Blade views or JSON
- Location: `app/Http/Controllers/` (11 files) + `app/Http/Controllers/Auth/` (9 files)
- Contains: Request handling, SQL queries, view/JSON responses, form validation (inline `$request->validate()`)
- Depends on: `DB` facade, `Carbon`, `Excel`/`Pdf` facades, `FcmService` (AlertController only)
- Used by: Web routes (`routes/web.php`, `routes/auth.php`)
**API Controllers:**
- Purpose: External data endpoints for mobile/IoT clients, always return JSON
- Location: `app/Http/Controllers/Api/` (3 files)
- Contains: Station data queries, custom auth (username/password via raw SQL), FCM alert proxy
- Used by: API routes (`routes/api.php`)
- Key difference from web controllers: No middleware/auth gates on most API routes (except login)
**Service Layer:**
- Purpose: Encapsulate external service integrations
- Location: `app/Services/FcmService.php`
- Contains: Firebase OAuth2 token generation, FCM v1 HTTP API calls
- Dependencies: `google/auth` package, Laravel `Http` facade
**Export Layer:**
- Purpose: Generate downloadable files (Excel, PDF)
- Location: `app/Exports/*.php` + `resources/views/pdf/*.blade.php`
- Contains: Excel exports using `Maatwebsite\Excel` with `FromCollection`, `WithHeadings`, `ShouldAutoSize`; PDF exports using `Barryvdh\DomPDF`
- Patterns: Export classes receive station/date parameters via constructor, queries data in `collection()`, defines headings in `headings()`
**View Layer:**
- Purpose: Render HTML and PDF output
- Location: `resources/views/`
- Contains: Blade layouts (`layout.app`, `layout.homeapp`), section-based views, component templates, PDF templates
- Organization: Layouts in `layout/`, auth screens in `auth/`, nav in `nav/`, shared components in `components/`
- Key inheritance: `layout.homeapp``layout.home` (public); `layout.app` → all authenticated pages
## Data Flow
### Primary Request Path — Dashboard Page
1. Request hits `GET /dashboard` or `GET /` — mapped in `routes/web.php:60-61`
2. `MapController::getCurrentData()` `app/Http/Controllers/MapController.php:23`
3. Executes a raw query joining `station` with latest `rainfall`, `waterlevel`, `siren` records using subqueries
4. Returns `view('layout.dashboard', compact('data'))`
5. Blade view extends `layout.app` which includes `nav.header` (head, CSS, Leaflet), `nav.navbar` (navigation), and sections `content1` (station data table + map) and `content2` (info section)
6. Response includes embedded JavaScript with `window.translations` for i18n and Leaflet map rendering
### Auth Flow — Login
1. `POST /login``AuthenticatedSessionController::store()` `app/Http/Controllers/Auth/AuthenticatedSessionController.php:55`
2. Uses `LoginRequest` `app/Http/Requests/Auth/LoginRequest.php` for rate limiting (5 attempts)
3. Custom pre-check: queries `User::where('name', $name)` checks `is_blocked` flag
4. If blocked → returns error; if `login_attempts >= 3` → auto-block on failed attempt
5. If success → reset `login_attempts`, regenerate session, redirect to `/`
6. Login page (GET /login) also renders the dashboard map with station data (`create()` method)
### Alert Flow — External IoT Alert → FCM Push
1. External system `POST /api/alert``api.php:18``AlertController::send()` `app/Http/Controllers/Api/AlertController.php:17`
2. Receives `stationid`, `level`, `stationtype` (1=Rainfall, 2=WaterLevel, 3=Siren)
3. Injects `FcmService` via constructor
4. Calls `$this->fcm->sendToTopic($topic, $title, $body)`
5. `FcmService::sendToTopic()` `app/Services/FcmService.php:23`:
- Reads `FIREBASE_PROJECT_ID` and `FIREBASE_CREDENTIALS` from env
- Loads service account JSON, fetches OAuth2 token via `google/auth`
- POSTs to `https://fcm.googleapis.com/v1/projects/{projectId}/messages:send`
- Returns HTTP status code
### Export Flow — Download Excel
1. `GET /rainfall/historical/export``RainfallController::exportHourlyRainfallExcel()` `app/Http/Controllers/RainfallController.php:360`
2. Reads query params (`station`, `startdate`, `enddate`)
3. Instantiates `HourlyRainfallExport($stationid, $startDate, $endDate)` `app/Exports/HourlyRainfallExport.php`
4. Export class queries PostgreSQL with pivot (24 hour columns) and returns Excel download
### Export Flow — Download PDF
1. `GET /export/rainfall-history/pdf``NotificationController::exportHistoryRfPDF()` `app/Http/Controllers/NotificationController.php:91`
2. Queries `notification` join `station` where `stationtype=1`
3. `Pdf::loadView('pdf.rfhistory', compact('rfHistory'))` → renders `resources/views/pdf/rfhistory.blade.php`
4. Returns PDF download response
**State Management:**
- Session-based auth state (standard Laravel session driver)
- Locale stored in session (`Session::put('locale', ...)`)
- No client-side state management (Alpine.js for minimal interactivity in navigation dropdowns)
## Key Abstractions
**Controller Base Class:**
- Purpose: Empty abstract class for controller type-hinting
- File: `app/Http/Controllers/Controller.php`
- Pattern: `abstract class Controller {}` — no shared logic
**Form Request Classes:**
- Purpose: Encapsulate validation and auth logic for form submissions
- Files: `app/Http/Requests/ProfileUpdateRequest.php`, `app/Http/Requests/Auth/LoginRequest.php`
- Pattern: Extends `FormRequest`, defines `rules()`, may include `authorize()` and custom `authenticate()` methods
**Export Classes (Excel):**
- Purpose: Query + format data for spreadsheet download
- Files: `app/Exports/HourlyRainfallExport.php`, `app/Exports/WaterLevelExport.php`
- Pattern: Implement `FromCollection`, `WithHeadings`, `ShouldAutoSize`; constructor receives filter params; `collection()` returns query result
**View Components:**
- Purpose: Blade component classes for layout rendering
- Files: `app/View/Components/AppLayout.php`, `app/View/Components/GuestLayout.php`
- Pattern: Extends `Component`, returns view in `render()`
## Entry Points
**Web Application:**
- Location: `routes/web.php` (main), `routes/auth.php` (auth routes, included from web.php:85)
- Triggers: Browser HTTP requests
- Middleware stack: `web` group → `auth` or `guest` or `admin` → controller
- Custom middleware: `admin` alias → `AdminMiddleware`; `LocalizationMiddleware` appended to `web` group
**API:**
- Location: `routes/api.php`
- Triggers: External IoT systems, mobile clients
- Middleware stack: `api` group only (no auth middleware on station data endpoints)
- No authentication required on station data endpoints (public JSON)
**Console:**
- Location: `routes/console.php`
- Triggers: `php artisan inspire` (only default, no custom commands)
## Architectural Constraints
- **No Eloquent ORM usage:** All data access (except User auth) uses raw SQL via `DB::table()` or `DB::select()` with named/positional bindings. This means: no model relationships, no scopes, no eager loading, no model events.
- **PostgreSQL-specific SQL:** The codebase uses `TO_CHAR()`, `EXTRACT()`, `::date` casts, `::time` casts, `DATE_TRUNC()`, `INTERVAL`, `DISTINCT ON` — this is **not portable** to other databases.
- **SQL injection risk from string interpolation:** Several controllers use string interpolation (`"WHERE s.stationid = '{$stationFilter}'"`) instead of parameterized bindings (see `RainfallController.php:36`, `WaterLevelController.php:30`).
- **No automated tests for domain controllers:** Tests only exist for Laravel Breeze auth scaffolding (`tests/Feature/Auth/` — most files are empty).
- **Service layer is thin:** Only one service class (`FcmService`) exists. All business logic lives in controllers.
- **Mixed view inheritance:** `layout.app` uses `@include()` for header and `@yield()` for content sections; `layouts.app` (Laravel Breeze default) uses component-based slots (`{{ $slot }}`). These are two separate layout systems.
## Error Handling
**Strategy:** Per-controller try/catch with `redirect()->back()->with('error', $message)` pattern.
**Patterns:**
- `AdminController::storeUser()`: Explicit `try/catch` for `ValidationException` and general `\Exception` — returns first error message to session flash
- `AuthenticatedSessionController::store()`: Returns `back()->with('error', ...)` for block/failed states
- `FcmService`: Throws `\Exception` if token fetch fails — caught by `AlertController` implicitly (no try/catch)
- Validation on form requests: Uses `$request->validate()` inline in controllers (most cases) or dedicated `FormRequest` classes
## Cross-Cutting Concerns
**Logging:**
- No application-level logging detected beyond default Laravel log channel
- `FIREBASE_CREDENTIALS` JSON read from file, no credential caching
**Validation:**
- Inline `$request->validate([...])` in most controllers (`AdminController`, `RainfallController`, `WaterLevelController`)
- Form request classes for profile update (`ProfileUpdateRequest`) and login (`LoginRequest`)
- No validation on API POST `/alert` endpoint (raw `$request->stationid` access without validation)
**Authentication:**
- Web: Session-based with Laravel Breeze scaffolding, custom `is_blocked` check and `login_attempts` tracking
- API: Custom username/password verification via raw SQL + `Hash::check()` — returns JSON with `error` boolean
- Admin: Middleware gate `AdminMiddleware` checks `access_level !== 1`
- No token-based or OAuth API auth
**Localization:**
- Two locales: `en` (English) and `bm` (Bahasa Malaysia)
- Language files: `lang/{en,bm}/{messages,toast,auth,validation,pagination,passwords}.php`
- Route: `GET /locale/{locale}``LocaleController::setLocale()` — sets `App::setLocale()` and `Session::put('locale')`
- Middleware: `LocalizationMiddleware` reads locale from session on every request and sets `App::setLocale()`
---
*Architecture analysis: 2026-05-28*