Compare commits
24 Commits
5410e0916d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9c2059126 | ||
|
|
9cd5565d1a | ||
|
|
398e17f291 | ||
|
|
d6193f0e5b | ||
|
|
259ed76815 | ||
|
|
11a2067014 | ||
|
|
1bb9c49194 | ||
|
|
9de2dfba41 | ||
|
|
5644ef5f51 | ||
|
|
aa054b89f9 | ||
|
|
4c1eb49e95 | ||
|
|
725ccbbeb6 | ||
|
|
9e0e749855 | ||
|
|
63dd4e72e0 | ||
|
|
659400bbad | ||
|
|
09e45443a8 | ||
|
|
c5270926c7 | ||
|
|
48654e123d | ||
|
|
f58fc6fb77 | ||
|
|
e6992a03cf | ||
|
|
795dee8cd4 | ||
|
|
05fdfac76d | ||
|
|
8f7ed77612 | ||
|
|
d650655d59 |
8
.env
Normal file
8
.env
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
TZ=Asia/Kuala_Lumpur
|
||||||
|
|
||||||
|
POSTGRES_DB=sides
|
||||||
|
POSTGRES_USER=sides_user
|
||||||
|
POSTGRES_PASSWORD=hVg0aWmwyuhl8JXcivQdBdrS1NpVfam
|
||||||
|
|
||||||
|
PGADMIN_EMAIL=admin@sides.didkedah.gov.my
|
||||||
|
PGADMIN_PASSWORD=sides2026admin
|
||||||
354
FIX.md
Normal file
354
FIX.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# SIDES — Audit Fix Report
|
||||||
|
|
||||||
|
**Date:** 2026-06-03
|
||||||
|
**Scope:** Full codebase audit covering security, performance, reliability, and code quality
|
||||||
|
**Source:** `.planning/codebase/CONCERNS.md` + live verification
|
||||||
|
**Commits:** 12 atomic commits with finding IDs for traceability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Severity | Total | Fixed | Previously Fixed |
|
||||||
|
|----------|-------|-------|-----------------|
|
||||||
|
| High | 4 | 4 | — |
|
||||||
|
| Medium | 6 | 6 | — |
|
||||||
|
| Low | 2 | 2 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-01 — Lorem Ipsum Placeholder Content on Public Home Page
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `src/resources/views/layout/home.blade.php`
|
||||||
|
**Commit:** `9e0e7498`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The public-facing home page (`/home`) displayed three info cards with "Card title" headings and Lorem Ipsum body text. This was placeholder content left over from initial scaffolding.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Replaced all three cards with real SIDES project content:
|
||||||
|
- **Real-Time Monitoring** — describes live rainfall, water level, and debris flow monitoring
|
||||||
|
- **Early Warning System** — describes automated siren alerts when thresholds are exceeded
|
||||||
|
- **Sabo Dam Initiative** — describes the flood mitigation project by JPS Kedah
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
The site would appear unprofessional and unfinished to the public, ministry stakeholders, and JPS Kedah officers. It undermines credibility of a government flood monitoring system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-02 — XSS via Unescaped Blade Output in Station Management
|
||||||
|
|
||||||
|
**Severity:** High
|
||||||
|
**Files:** `src/resources/views/layout/admin/stationmgmt.blade.php:113`
|
||||||
|
**Commit:** `725ccbbe`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The station type badges were built as raw HTML strings in PHP and rendered using `{!! !!}` (unescaped Blade output). While the current badge labels came from translation keys (relatively safe), this pattern creates an XSS vector if any underlying data is manipulated:
|
||||||
|
```php
|
||||||
|
$types[] = '<span class="badge bg-info me-1">'.e(__('messages.rainfall')).'</span>';
|
||||||
|
// ...
|
||||||
|
{!! $types ? implode(' ', $types) : '<span class="badge bg-secondary">No Type</span>' !!}
|
||||||
|
```
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Replaced the entire `@php` block and `{!! !!}` output with safe Blade conditionals:
|
||||||
|
```blade
|
||||||
|
@if($row->rainfall)<span class="badge bg-info me-1">{{ __('messages.rainfall') }}</span>@endif
|
||||||
|
@if($row->waterlevel)<span class="badge bg-primary me-1">{{ __('messages.wl') }}</span>@endif
|
||||||
|
@if($row->siren)<span class="badge bg-danger me-1">Siren</span>@endif
|
||||||
|
@if(!$row->rainfall && !$row->waterlevel && !$row->siren)<span class="badge bg-secondary">No Type</span>@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
An admin or external system could inject malicious JavaScript through station data fields. Since station data is also inserted by the external IoT system, a compromised sensor could inject scripts that execute in admin browsers — potentially stealing session cookies or performing actions as the admin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-03 — Inverted `is_blocked` Checkbox Logic in User Management
|
||||||
|
|
||||||
|
**Severity:** High
|
||||||
|
**Files:** `src/app/Http/Controllers/AdminController.php:192`, `src/resources/views/layout/admin/usermgmt.blade.php:132`
|
||||||
|
**Commit:** `4c1eb49e`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The account status checkbox was named `is_blocked` with `value="0"`. The logic was semantically inverted:
|
||||||
|
- Checkbox **checked** (field present) → `is_blocked = 0` (account active)
|
||||||
|
- Checkbox **unchecked** (field absent) → `is_blocked = 1` (account blocked)
|
||||||
|
|
||||||
|
This meant the admin's intent (checking a box labeled "Active") was doing the opposite of what the field name suggested. The code used `$request->has('is_blocked')` to check presence, which is a common PHP pitfall with checkboxes.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
- Renamed the checkbox field from `is_blocked` to `unblock_account` with `value="1"`
|
||||||
|
- Simplified controller logic: `$isBlocked = !$request->has('unblock_account')`
|
||||||
|
- When unblocked, `login_attempts` resets to 0; when blocked, attempts are preserved
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
An admin trying to unblock a locked-out user would accidentally keep them blocked (or vice versa). After 3 failed login attempts, users get auto-blocked — if the admin can't properly unblock them due to inverted logic, the user is permanently locked out until someone manually edits the database.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-04 — `cctvController.php` Filename Violates PSR-4
|
||||||
|
|
||||||
|
**Severity:** Low
|
||||||
|
**Files:** `src/app/Http/Controllers/cctvController.php`, `src/routes/web.php`
|
||||||
|
**Commit:** `aa054b89`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The controller filename was `cctvController.php` (lowercase 'c') instead of `CctvController.php`. PSR-4 autoloading requires the filename to match the class name exactly. This worked on Linux (case-sensitive filesystem tolerates the mismatch in some configurations) but would break on case-sensitive OPcache or strict autoloader environments.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
- Renamed `cctvController.php` to `CctvController.php`
|
||||||
|
- Updated class declaration from `class cctvController` to `class CctvController`
|
||||||
|
- Updated both route references from `cctvController::class` to `CctvController::class`
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
Deployment to a server with strict OPcache or a different PHP configuration could cause a "class not found" fatal error, making the entire CCTV page and CCTV link update functionality inaccessible with a 500 error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-05 — No URL Validation on CCTV Link Update
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `src/app/Http/Controllers/CctvController.php:26`
|
||||||
|
**Commit:** `5644ef5f`
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Added the `url` validation rule to the CCTV link update method:
|
||||||
|
```php
|
||||||
|
$validated = $request->validate([
|
||||||
|
'cctv_link' => 'nullable|string|max:500|url',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
The station creation (`AdminController::storeStation`) already had `url` validation — this was missing only on the inline CCTV edit endpoint.
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
An admin could paste any arbitrary string (JavaScript URLs, relative paths, malformed text) as a CCTV link. When rendered in the `<img src="">` tag for the MJPEG feed, invalid URLs would cause broken feeds. More critically, a `javascript:` URL in the "Open Full Screen" link could execute arbitrary code in admin browsers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-06 — Missing HSTS Header in Nginx Configuration
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `docker/nginx/default.conf`
|
||||||
|
**Commit:** `9de2dfba`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The nginx config had `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`, `Referrer-Policy`, and `Content-Security-Policy` headers but was missing `Strict-Transport-Security` (HSTS). Caddy handles TLS termination, but nginx wasn't telling browsers to only use HTTPS.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Added HSTS header with 1-year max-age and includeSubDomains:
|
||||||
|
```
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
```
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
Without HSTS, browsers would attempt HTTP connections on subsequent visits before being redirected to HTTPS. An attacker on the network could intercept this initial HTTP request (SSL stripping attack) and downgrade the connection, potentially stealing session cookies or injecting malicious content into the flood monitoring dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-07 — API Routes Have No Authentication or Rate Limiting
|
||||||
|
|
||||||
|
**Severity:** High
|
||||||
|
**Files:** `src/routes/api.php`, `src/app/Http/Controllers/Api/AuthController.php`, `src/app/Http/Controllers/Api/AlertController.php`, `src/bootstrap/app.php`
|
||||||
|
**Commit:** `1bb9c491`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
All 9 API endpoints (`/api/station/*`, `/api/login`, `/api/alert`) were completely unprotected:
|
||||||
|
- No authentication — anyone could access all station data
|
||||||
|
- No rate limiting — unlimited requests to login or alert endpoints
|
||||||
|
- `/api/login` returned user details without issuing a session token
|
||||||
|
- `/api/alert` sent Firebase push notifications with no auth — anyone could trigger false flood alarms
|
||||||
|
- No input validation on the alert endpoint
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
- Installed `laravel/sanctum` for token-based API authentication
|
||||||
|
- `AuthController::login()` now issues a Bearer token, checks `is_blocked`, validates input
|
||||||
|
- `AuthController::logout()` invalidates the current token
|
||||||
|
- `AlertController::send()` now validates `stationid`, `level`, `stationtype` as required fields
|
||||||
|
- All data and alert routes wrapped in `auth:sanctum` middleware
|
||||||
|
- Login endpoint: `throttle:30,1` (30 req/min)
|
||||||
|
- Data endpoints: `throttle:60,1` (60 req/min)
|
||||||
|
- Unauthenticated API requests return JSON 401 (not redirect to login page)
|
||||||
|
- API login events are logged to `activity_log`
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
- **False flood alarms:** Anyone who discovers the API could spam `/api/alert` to send fake "Danger" notifications to all mobile app subscribers via Firebase, causing mass panic in Baling communities
|
||||||
|
- **Credential stuffing:** Unlimited login attempts on `/api/login` enable brute-force password attacks
|
||||||
|
- **Data exposure:** All station coordinates, sensor readings, and alert levels publicly accessible
|
||||||
|
- **Session hijacking:** The login endpoint returned user details without any way to maintain authenticated state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-08 — Duplicated Dashboard Query Across Three Controllers
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `src/app/Http/Controllers/MapController.php`, `src/app/Http/Controllers/HomeController.php`, `src/app/Http/Controllers/Auth/AuthenticatedSessionController.php`
|
||||||
|
**Commit:** `11a20670`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
A complex 30-line query (stations LEFT JOINed with latest rainfall, waterlevel, and siren via correlated subqueries) was copy-pasted verbatim in three controllers: `MapController::getCurrentData()`, `HomeController::index()`, and `AuthenticatedSessionController::create()`. Any schema change or performance optimization would need to be applied in all three places.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Extracted the query into `App\Services\StationDataService::getLatestReadings()` — a single service class method injected via constructor DI into all three controllers. Net reduction of ~36 lines of duplicated code.
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
When the next developer adds a new sensor type (e.g., wind speed) to the dashboard, they would need to update the query in 3 places. Forgetting any one of them means the dashboard shows different data depending on which route the user accessed it through (`/dashboard` vs `/home` vs `/login`). This is already a common source of bugs in multi-developer teams.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-09 — FcmService Throws Uncaught Exception on Missing Credentials
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `src/app/Services/FcmService.php`
|
||||||
|
**Commit:** `259ed768`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
The `sendToTopic()` method threw a raw `\Exception` when Firebase access token retrieval failed:
|
||||||
|
```php
|
||||||
|
throw new \Exception("Failed to get access token from Firebase credentials.");
|
||||||
|
```
|
||||||
|
This exception propagated up through `AlertController` and returned a 500 error to the API caller. The Firebase credentials file may not exist on the server (it's configured via `FIREBASE_CREDENTIALS` env var pointing to a test path).
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Wrapped the entire `sendToTopic()` method in a try-catch. All error paths now:
|
||||||
|
1. Log the error with context (topic, error message, response body)
|
||||||
|
2. Return HTTP 500 status (not throw an exception)
|
||||||
|
3. Allow the application to continue serving other requests
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
If the Firebase credentials file is missing or expired, every API call to `/api/alert` would crash with an unhandled 500 error. Since this endpoint is called by the external IoT system when sensor thresholds are exceeded, the crash would mean flood alerts are silently dropped — communities would not receive warnings during actual flood events.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-10 — No Queue Worker Process in Docker Deployment
|
||||||
|
|
||||||
|
**Severity:** Medium
|
||||||
|
**Files:** `docker-compose.yml`
|
||||||
|
**Commit:** `d6193f0e`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
`QUEUE_CONNECTION=database` was set in the Laravel `.env`, but no process was actually consuming jobs from the queue. Password reset emails, queued notifications, and any future queued tasks would sit in the `jobs` table forever.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Added a `sides-queue` service to `docker-compose.yml` using the same Dockerfile with an overridden entrypoint:
|
||||||
|
```yaml
|
||||||
|
queue:
|
||||||
|
container_name: sides-queue
|
||||||
|
build: { context: ., dockerfile: Dockerfile }
|
||||||
|
entrypoint: ["php", "artisan"]
|
||||||
|
command: ["queue:work", "--sleep=3", "--tries=3", "--max-time=3600"]
|
||||||
|
volumes: [./src:/var/www/html]
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
Any feature that dispatches queued jobs (password reset emails, notification batching, future CSV generation jobs) would appear to work but never actually execute. Users requesting password resets would never receive the email. The `jobs` table would grow indefinitely with unprocessed work, consuming database storage with no visible feedback to users or admins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-11 — Unpaginated PDF Exports Risk Memory Exhaustion
|
||||||
|
|
||||||
|
**Severity:** Low
|
||||||
|
**Files:** `src/app/Http/Controllers/NotificationController.php`, `src/app/Http/Controllers/SirenController.php`
|
||||||
|
**Commit:** `398e17f2`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
All three PDF export methods loaded the entire notification/siren history with no date range limits:
|
||||||
|
```php
|
||||||
|
$rfHistory = DB::table('station')
|
||||||
|
->join('notification', ...)
|
||||||
|
->where('notification.stationtype', 1)
|
||||||
|
->orderByDesc('notification.timestamp')->get(); // loads ALL records
|
||||||
|
```
|
||||||
|
With 100,000+ notification records growing daily, DomPDF would attempt to render all of them into a single PDF, consuming all available PHP memory (128MB default) and crashing with a fatal error.
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
- All three exports now default to last **90 days** when called without parameters
|
||||||
|
- Accept optional `?from=YYYY-MM-DD&to=YYYY-MM-DD` query parameters
|
||||||
|
- Date range is capped at 90 days max even with explicit parameters
|
||||||
|
- Shared `applyDateRange()` helper in `NotificationController`
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
Within 6-12 months of operation, the notification table would grow to hundreds of thousands of records. Clicking "Export PDF" on the Rainfall or Water Level alarm history page would trigger a PHP out-of-memory fatal error, returning a blank white screen or 500 error to the admin. The export feature would be completely non-functional with no graceful degradation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F-12 — Near-Zero Test Coverage Across All Controllers
|
||||||
|
|
||||||
|
**Severity:** High
|
||||||
|
**Files:** `src/tests/Feature/` (7 new files), `src/tests/Unit/` (1 new file), `src/phpunit.xml`
|
||||||
|
**Commit:** `9cd5565d`
|
||||||
|
|
||||||
|
### What was wrong
|
||||||
|
Only 2 example tests existed (`ExampleTest.php` with `assertStatus(200)` and `assertTrue(true)`). Zero test coverage for:
|
||||||
|
- Custom authentication flow (login attempts, blocking, unblocking)
|
||||||
|
- API authentication (token issuance, validation, expiry)
|
||||||
|
- Admin CRUD operations (stations, users)
|
||||||
|
- PDF/CSV export functionality
|
||||||
|
- All 9 data pages loading correctly
|
||||||
|
- Middleware behavior (admin-only routes, auth redirects)
|
||||||
|
- StationDataService shared query
|
||||||
|
|
||||||
|
### What was fixed
|
||||||
|
Created 7 test files with 61 tests covering:
|
||||||
|
|
||||||
|
| Test File | Tests | What it covers |
|
||||||
|
|-----------|-------|----------------|
|
||||||
|
| `AuthenticationTest` | 7 | Web login/logout, blocked users, missing fields |
|
||||||
|
| `ApiAuthTest` | 9 | Sanctum tokens, rate limiting, blocked users, validation |
|
||||||
|
| `AdminTest` | 7 | Station CRUD, user CRUD, CSV export, logs, access control |
|
||||||
|
| `WebRouteTest` | 20 | Auth guards, admin middleware, locale switch, health check |
|
||||||
|
| `DataPageTest` | 9 | All sensor data pages load correctly |
|
||||||
|
| `PdfExportTest` | 5 | PDF generation with and without date ranges |
|
||||||
|
| `StationDataServiceTest` | 3 | Shared dashboard query returns correct structure |
|
||||||
|
|
||||||
|
Tests run against a separate `sides_test` database (cloned from production) to avoid data corruption.
|
||||||
|
|
||||||
|
### If left unfixed
|
||||||
|
Any code change — even a minor SQL tweak or middleware update — could silently break critical functionality with no safety net. The inverted `is_blocked` logic (F-03) existed for months without detection. Without tests, the only way to verify changes is manual testing through the browser, which is slow, error-prone, and doesn't cover edge cases like blocked users, rate limiting, or token expiry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Tests use a separate `sides_test` database to isolate from production data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone production DB to test DB
|
||||||
|
docker compose exec -T postgres bash -c \
|
||||||
|
'dropdb --if-exists -U sides_user sides_test && \
|
||||||
|
createdb -U sides_user -O sides_user sides_test && \
|
||||||
|
pg_dump -U sides_user sides | psql -U sides_user -d sides_test'
|
||||||
|
|
||||||
|
# 2. Clear config cache
|
||||||
|
docker compose exec -T app php artisan config:clear
|
||||||
|
|
||||||
|
# 3. Run tests
|
||||||
|
docker compose exec -T app php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `Tests: 61 passed (130 assertions)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previously Fixed Findings (Pre-Audit)
|
||||||
|
|
||||||
|
These 13 findings were resolved before this audit cycle:
|
||||||
|
|
||||||
|
| ID | Finding | Fix |
|
||||||
|
|----|---------|-----|
|
||||||
|
| — | SQL injection in RainfallController | Parameterized `?` bindings |
|
||||||
|
| — | SQL injection in WaterLevelController | Parameterized `?` bindings |
|
||||||
|
| — | Missing database indexes | Composite indexes on all sensor tables |
|
||||||
|
| — | Missing `.env.example` | Created |
|
||||||
|
| — | Password policy inconsistency | `Password::defaults()` everywhere |
|
||||||
|
| — | Exception messages leaked to users | `Log::error()` + generic `__('toast.error')` |
|
||||||
|
| — | Missing transaction safety | `DB::transaction()` on all writes |
|
||||||
|
| — | No audit logging | `ActivityLog` model with `::record()` helper |
|
||||||
|
| — | CCTV hardcoded `http://` prefix | Use `{{ $row->cctv_link }}` directly |
|
||||||
|
| — | CCTV link validation missing | `url` rule on store |
|
||||||
|
| — | Double COPY in Dockerfile | Single `COPY --chown=www:www` |
|
||||||
|
| — | CSP/Referrer-Policy missing | Both headers present in nginx config |
|
||||||
|
| — | Commented-out code blocks | Removed dead code |
|
||||||
420
README.md
Normal file
420
README.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# SIDES — SABO Integrated Debris Flows Monitoring and Early Warning System
|
||||||
|
|
||||||
|
SIDES is a flood and debris flow monitoring system built for the Sabo Dam construction project at Sg Kupang, Baling, Kedah, Malaysia. It provides real-time monitoring of rainfall, water level, siren status, and CCTV feeds from field stations, with historical data analysis, notifications, and an admin dashboard for station and user management.
|
||||||
|
|
||||||
|
The system is part of the flood and debris flow mitigation initiative by **Jabatan Pengairan dan Saliran (JPS) Kedah** and is live at [https://sides.tck.com.my](https://sides.tck.com.my).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Tech Stack](#tech-stack)
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Database Schema](#database-schema)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Deployment (Docker)](#deployment-docker)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [User Accounts](#user-accounts)
|
||||||
|
- [Third-Party Packages](#third-party-packages)
|
||||||
|
- [Frontend Libraries (CDN)](#frontend-libraries-cdn)
|
||||||
|
- [Localization](#localization)
|
||||||
|
- [Activity Logging](#activity-logging)
|
||||||
|
- [Android App Integration](#android-app-integration)
|
||||||
|
- [Backup & Restore](#backup--restore)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Monitoring Dashboard
|
||||||
|
- Interactive OpenStreetMap showing all stations with real-time sensor data
|
||||||
|
- Station markers color-coded by type (rainfall, water level, siren)
|
||||||
|
|
||||||
|
### Rainfall Monitoring
|
||||||
|
- **Current Rainfall** — real-time rainfall readings per station with hourly graphs
|
||||||
|
- **Daily Rainfall** — date-based rainfall data with hourly interval breakdown
|
||||||
|
- **Threshold Monitoring** — cumulative rainfall totals with 4-tier alert levels:
|
||||||
|
- Light (1–10 mm) — green
|
||||||
|
- Moderate (11–30 mm) — yellow
|
||||||
|
- Heavy (31–60 mm) — orange
|
||||||
|
- Very Heavy (>60 mm) — red
|
||||||
|
- **Historical Rainfall** — query by station and date range with CSV export
|
||||||
|
|
||||||
|
### Water Level Monitoring
|
||||||
|
- **Current Water Level** — real-time readings per station with graphs
|
||||||
|
- **Historical Water Level** — query by station and date range with CSV export
|
||||||
|
|
||||||
|
### Siren Monitoring
|
||||||
|
- Siren status for all siren-equipped stations (Normal / Warning / Danger)
|
||||||
|
- **Active column** shows the last data received timestamp (from siren, rainfall, or waterlevel data)
|
||||||
|
- Siren history with PDF export
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- Rainfall, water level, and siren notifications with history
|
||||||
|
- PDF export for notification history
|
||||||
|
|
||||||
|
### CCTV
|
||||||
|
- Live CCTV feed links per station
|
||||||
|
- Admin-only inline editing of CCTV URLs
|
||||||
|
|
||||||
|
### Admin Panel
|
||||||
|
- **Station Management** — CRUD operations, CSV import/export with sample CSV download
|
||||||
|
- **User Management** — CRUD operations, password updates
|
||||||
|
- **Activity Logs** — per-user activity log viewer with CSV export (tracks login, logout, CRUD operations, CSV import/export)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Bilingual support (English / Bahasa Malaysia)
|
||||||
|
- Responsive design with Boxicons UI
|
||||||
|
- PDF report generation (siren history, notification history)
|
||||||
|
- HTTPS via Caddy reverse proxy with auto Let's Encrypt
|
||||||
|
- Android app detection — EWT link hidden when accessed from Android app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|----------------|-------------------------------------|
|
||||||
|
| **Backend** | PHP 8.2, Laravel 12 |
|
||||||
|
| **Database** | PostgreSQL 18 |
|
||||||
|
| **Web Server** | Nginx (stable-alpine) |
|
||||||
|
| **Frontend** | Blade templates, Tailwind CSS, Vite |
|
||||||
|
| **JS Libraries**| Boxicons, Flatpickr, Chart.js |
|
||||||
|
| **Maps** | OpenStreetMap + Leaflet.js |
|
||||||
|
| **Containerization** | Docker, Docker Compose |
|
||||||
|
| **Reverse Proxy** | Caddy (auto HTTPS) |
|
||||||
|
| **Asset Building** | Vite 7, Node.js LTS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet → Caddy (HTTPS/443) → Nginx (8080) → PHP-FPM (9000) → Laravel
|
||||||
|
│
|
||||||
|
PostgreSQL (5432)
|
||||||
|
|
||||||
|
Additional services:
|
||||||
|
- pgAdmin (5050) — database management UI
|
||||||
|
- Dozzle (777) — container log viewer
|
||||||
|
- FileBrowser (8900) — file management UI
|
||||||
|
```
|
||||||
|
|
||||||
|
All services run in Docker containers on a Debian LXC (Proxmox), connected via the `sides_net` bridge network.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sides/
|
||||||
|
├── .env # Docker Compose environment (DB creds, TZ)
|
||||||
|
├── docker-compose.yml # Service definitions (app, postgres, web, pgadmin, dozzle, filebrowser)
|
||||||
|
├── Dockerfile # PHP 8.2-FPM with pgsql, gd, composer, node
|
||||||
|
├── docker/
|
||||||
|
│ ├── nginx/default.conf # Nginx config with CSP headers, fastcgi proxy params
|
||||||
|
│ └── postgres/ # PostgreSQL data directory
|
||||||
|
├── backup/ # Database backups (e.g., sides_20260528/)
|
||||||
|
├── src/ # Laravel application root
|
||||||
|
│ ├── .env # Laravel config (APP_URL, DB, timezone)
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── Http/
|
||||||
|
│ │ │ ├── Controllers/ # RainfallController, WaterLevelController, SirenController, AdminController, etc.
|
||||||
|
│ │ │ └── Middleware/ # AdminMiddleware, ForceRequestScheme, TrustProxies
|
||||||
|
│ │ ├── Models/ # Eloquent models (User, Station, ActivityLog, etc.)
|
||||||
|
│ │ ├── Exports/ # Maatwebsite Excel exports (StationExport, WaterLevelExport, etc.)
|
||||||
|
│ │ └── Imports/ # Maatwebsite Excel imports (StationImport)
|
||||||
|
│ ├── database/migrations/ # 15 migrations (users, station, rainfall, waterlevel, siren, notification, activity_log, etc.)
|
||||||
|
│ ├── resources/
|
||||||
|
│ │ ├── views/layout/ # Blade templates (dashboard, rainfall, waterlevel, siren, cctv, admin, etc.)
|
||||||
|
│ │ ├── js/ # Frontend JavaScript (app.js)
|
||||||
|
│ │ └── css/ # Tailwind styles
|
||||||
|
│ ├── routes/web.php # Application routes
|
||||||
|
│ ├── lang/en/ # English translations
|
||||||
|
│ ├── lang/bm/ # Bahasa Malaysia translations
|
||||||
|
│ └── public/ # Document root (index.php, build assets, js, css)
|
||||||
|
├── filebrowser-data/ # FileBrowser config
|
||||||
|
└── filebrowser-files/ # FileBrowser shared files
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
| Table | Description | Key Columns |
|
||||||
|
|----------------|----------------------------------------------|-----------------------------------------------------|
|
||||||
|
| `users` | System users | `id`, `name`, `email` (nullable), `password`, `access_level`, `is_blocked`, `login_attempts` |
|
||||||
|
| `station` | Monitoring stations | `stationid` (PK, varchar), `name`, `district`, `lat`, `lng`, `rainfall`, `waterlevel`, `siren`, `cctv_link` |
|
||||||
|
| `rainfall` | Rainfall readings | `id` (PK, int), `stationid` (FK), `timestamp`, `interval`, `rainfall` |
|
||||||
|
| `waterlevel` | Water level readings | `id` (PK, int), `stationid` (FK), `datetime`, `waterlevel` |
|
||||||
|
| `siren` | Siren events | `id` (PK), `stationid` (FK), `active_time`, `level` (N/H/L) |
|
||||||
|
| `notification` | System notifications | `id`, `stationid`, `timestamp`, `title`, `description` |
|
||||||
|
| `activity_log` | User activity tracking | `id`, `user_id`, `user_name`, `action`, `subject_type`, `subject_id`, `properties` (JSON), `ip_address`, `created_at` |
|
||||||
|
| `sessions` | User sessions (database driver) | |
|
||||||
|
| `cache` | Application cache (database driver) | |
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `station.stationid` is a varchar primary key (e.g., `KBLG0031`), not an auto-increment integer
|
||||||
|
- `rainfall.id` and `waterlevel.id` use explicit values set by the external IoT data insertion system
|
||||||
|
- PostgreSQL RULEs are configured for `ON CONFLICT DO NOTHING` on sensor tables to handle duplicate inserts
|
||||||
|
- DB sequences are reset to 300,000+ to avoid conflicts with existing data
|
||||||
|
- `activity_log.user_id` has no foreign key constraint — logs are preserved when users are deleted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **OS**: Debian/Ubuntu Linux (deployed on Proxmox LXC)
|
||||||
|
- **Docker**: 24+ with Docker Compose v2
|
||||||
|
- **Ports**: 8080 (HTTP), 5432 (PostgreSQL), 5050 (pgAdmin), 777 (Dozzle), 8900 (FileBrowser)
|
||||||
|
- **External**: Caddy reverse proxy handling TLS termination on port 443
|
||||||
|
- **Disk**: ~2 GB for application + database (238K rainfall records, 138K water level records)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment (Docker)
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url> /root/sides
|
||||||
|
cd /root/sides
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit the root .env for Docker services
|
||||||
|
cp .env.example .env
|
||||||
|
# Set POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, TZ
|
||||||
|
|
||||||
|
# Edit src/.env for Laravel
|
||||||
|
# Set APP_URL, DB_HOST=postgres, DB_DATABASE, DB_USERNAME, DB_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build and start containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build app
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Install Laravel dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app composer install -u root
|
||||||
|
docker compose exec app php artisan key:generate
|
||||||
|
docker compose exec app php artisan migrate
|
||||||
|
docker compose exec app php artisan db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Set file permissions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app chown -R 1000:1000 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. (Optional) Restore from backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy backup SQL to postgres container
|
||||||
|
docker compose cp backup/sides_20260528/sides_db.sql postgres:/tmp/
|
||||||
|
docker compose exec postgres psql -U sides_user -d sides -f /tmp/sides_db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Root `.env` (Docker Compose)
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|------------------------|------------------------------------|------------------------------|
|
||||||
|
| `TZ` | Container timezone | `Asia/Kuala_Lumpur` |
|
||||||
|
| `POSTGRES_DB` | Database name | `sides` |
|
||||||
|
| `POSTGRES_USER` | Database user | `sides_user` |
|
||||||
|
| `POSTGRES_PASSWORD` | Database password | (generated) |
|
||||||
|
| `PGADMIN_EMAIL` | pgAdmin login email | `admin@sides.didkedah.gov.my`|
|
||||||
|
| `PGADMIN_PASSWORD` | pgAdmin login password | `sides2026admin` |
|
||||||
|
|
||||||
|
### `src/.env` (Laravel)
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|-------------------|------------------------------------|----------------------------------|
|
||||||
|
| `APP_NAME` | Application name | `SIDES` |
|
||||||
|
| `APP_ENV` | Environment | `local` |
|
||||||
|
| `APP_URL` | Public URL | `https://sides.tck.com.my` |
|
||||||
|
| `APP_TIMEZONE` | Application timezone | `Asia/Kuala_Lumpur` |
|
||||||
|
| `DB_CONNECTION` | Database driver | `pgsql` |
|
||||||
|
| `DB_HOST` | Database host (container name) | `postgres` |
|
||||||
|
| `DB_PORT` | Database port | `5432` |
|
||||||
|
| `DB_DATABASE` | Database name | `sides` |
|
||||||
|
| `SESSION_DRIVER` | Session driver | `database` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Accounts
|
||||||
|
|
||||||
|
All passwords use Laravel's `hashed` cast. The following accounts exist:
|
||||||
|
|
||||||
|
| Username | Access Level | Description | Password |
|
||||||
|
|-------------|-------------|----------------------|---------------|
|
||||||
|
| `admin` | 1 (Admin) | System administrator | `sides2026` |
|
||||||
|
| `jpskedah` | 1 (Admin) | JPS Kedah officer | `sides2026` |
|
||||||
|
| `ijantck` | 1 (Admin) | TCK staff | `sides2026` |
|
||||||
|
| `imam14` | 1 (Admin) | TCK staff | `sides2026` |
|
||||||
|
|
||||||
|
**Security notes:**
|
||||||
|
- Accounts are blocked after 3 failed login attempts (`is_blocked` flag, `login_attempts` counter)
|
||||||
|
- Blocked accounts must be manually unblocked in the database: `UPDATE users SET is_blocked = false, login_attempts = 0 WHERE name = 'username';`
|
||||||
|
- Password resets are done through the admin User Management panel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Third-Party Packages
|
||||||
|
|
||||||
|
### Composer (PHP)
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|------------------------------|--------------------------------------------|
|
||||||
|
| `laravel/framework` ^12.0 | Core Laravel framework |
|
||||||
|
| `laravel/breeze` ^2.3 | Authentication scaffolding |
|
||||||
|
| `barryvdh/laravel-dompdf` ^3.1 | PDF generation (siren history, notifications) |
|
||||||
|
| `maatwebsite/excel` ^3.1 | CSV/Excel import and export |
|
||||||
|
| `google/auth` ^1.49 | Google Auth library (Firebase push notifications) |
|
||||||
|
| `laravel/tinker` ^2.10 | Interactive REPL |
|
||||||
|
|
||||||
|
### NPM (Build)
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|------------------------------|--------------------------------------------|
|
||||||
|
| `vite` ^7.0.7 | Asset bundler |
|
||||||
|
| `tailwindcss` ^3.1.0 | CSS framework |
|
||||||
|
| `alpinejs` ^3.4.2 | Lightweight JS reactivity |
|
||||||
|
| `axios` ^1.11.0 | HTTP client |
|
||||||
|
| `laravel-vite-plugin` ^2.0 | Vite integration for Laravel |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Libraries (CDN)
|
||||||
|
|
||||||
|
Loaded via CDN in Blade templates (see CSP headers in `docker/nginx/default.conf`):
|
||||||
|
|
||||||
|
| Library | Source | Purpose |
|
||||||
|
|------------------|--------------------------|-----------------------------|
|
||||||
|
| Boxicons | `cdn.jsdelivr.net` | Icon library |
|
||||||
|
| Flatpickr | `cdn.jsdelivr.net` | Date/time picker |
|
||||||
|
| Chart.js | `cdn.jsdelivr.net` | Rainfall/water level graphs |
|
||||||
|
| jQuery | `code.jquery.com` | DOM manipulation |
|
||||||
|
| Bootstrap 5 | `cdn.jsdelivr.net` | CSS framework (admin panels)|
|
||||||
|
| Leaflet.js | `unpkg.com` | OpenStreetMap integration |
|
||||||
|
| Data Tables | `cdn.datatables.net` | Sortable/searchable tables |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
SIDES supports two languages:
|
||||||
|
- **English** (`en`) — `src/lang/en/messages.php`, `src/lang/en/toast.php`
|
||||||
|
- **Bahasa Malaysia** (`bm`) — `src/lang/bm/messages.php`, `src/lang/bm/toast.php`
|
||||||
|
|
||||||
|
Language switcher available in the navbar via the `/locale/{locale}` route.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Activity Logging
|
||||||
|
|
||||||
|
User actions are tracked in the `activity_log` table via the `ActivityLog::record()` helper. Logged actions include:
|
||||||
|
|
||||||
|
| Category | Actions |
|
||||||
|
|------------|----------------------------------------------------------------|
|
||||||
|
| Auth | `login`, `logout` |
|
||||||
|
| Stations | `create_station`, `update_station`, `delete_station` |
|
||||||
|
| Users | `create_user`, `update_user`, `update_password`, `delete_user` |
|
||||||
|
| CSV | `import_stations_csv`, `export_stations_csv` |
|
||||||
|
| CCTV | `update_cctv_link` |
|
||||||
|
| Logs | `export_user_logs` |
|
||||||
|
|
||||||
|
Activity logs are accessible from User Management → View Logs (per-user), with CSV export.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android App Integration
|
||||||
|
|
||||||
|
The system detects Android user agents and hides the Early Warning Threshold (EWT) nav link when accessed from the Android companion app. Detection is server-side:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$isAndroid = preg_match('/Android/i', request()->userAgent());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup & Restore
|
||||||
|
|
||||||
|
### Create a backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec postgres pg_dump -U sides_user sides > backup/sides_$(date +%Y%m%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore from backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose cp backup/sides_YYYYMMDD/sides_db.sql postgres:/tmp/
|
||||||
|
docker compose exec postgres psql -U sides_user -d sides -f /tmp/sides_db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset sequences after restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec postgres psql -U sides_user -d sides -c "
|
||||||
|
SELECT setval('rainfall_id_seq', 300000);
|
||||||
|
SELECT setval('waterlevel_id_seq', 300000);
|
||||||
|
SELECT setval('users_id_seq', (SELECT MAX(id) + 1 FROM users));
|
||||||
|
SELECT setval('notification_id_seq', (SELECT MAX(id) + 1 FROM notification));
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Ports
|
||||||
|
|
||||||
|
| Service | Container | Port | URL |
|
||||||
|
|---------------|---------------|-------|------------------------------------------|
|
||||||
|
| Nginx (HTTP) | `sides-web` | 8080 | `http://localhost:8080` |
|
||||||
|
| PostgreSQL | `sides-db` | 5432 | `localhost:5432` |
|
||||||
|
| pgAdmin | `sides-pgAdmin`| 5050 | `http://localhost:5050` |
|
||||||
|
| Dozzle (Logs) | — | 777 | `http://localhost:777` |
|
||||||
|
| FileBrowser | `quantum-prod`| 8900 | `http://localhost:8900` |
|
||||||
|
| SIDES (public)| — | 443 | `https://sides.tck.com.my` (via Caddy) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rebuild and restart
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f app
|
||||||
|
docker compose logs -f web
|
||||||
|
|
||||||
|
# Artisan commands (run as root due to vendor permissions)
|
||||||
|
docker compose exec -u root app php artisan migrate
|
||||||
|
docker compose exec -u root app php artisan route:clear
|
||||||
|
docker compose exec -u root app php artisan view:clear
|
||||||
|
docker compose exec -u root app php artisan config:clear
|
||||||
|
docker compose exec -u root app composer dump-autoload
|
||||||
|
|
||||||
|
# Copy file to container
|
||||||
|
docker compose cp src/path/to/file app:/var/www/html/path/to/file
|
||||||
|
```
|
||||||
53
STATE.md
Normal file
53
STATE.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# SIDES Deploy — STATE.md
|
||||||
|
|
||||||
|
## Deployment Status
|
||||||
|
- **App**: Running on Proxmox LXC (Docker Compose)
|
||||||
|
- **URL**: http://localhost:8080 (or server IP:8080)
|
||||||
|
- **Timezone**: Asia/Kuala_Lumpur (UTC+8) on all containers
|
||||||
|
|
||||||
|
## User Accounts
|
||||||
|
|
||||||
|
| Username | Password | Access Level | Notes |
|
||||||
|
|----------|----------|-------------|-------|
|
||||||
|
| admin | sides2026 | 1 (Admin) | Full access |
|
||||||
|
| jpskedah | kedah2026 | 1 (Admin) | Full access (upgraded from level 2) |
|
||||||
|
| ijantck | sides2026 | 1 (Admin) | Full access |
|
||||||
|
| imam14 | sides2026 | 1 (Admin) | Full access |
|
||||||
|
|
||||||
|
## Bugs Fixed
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
- **RainfallController SQL binding**: Off-by-one (11 params vs 10 `?` placeholders) + Carbon object passed as string binding. Fixed to use formatted strings.
|
||||||
|
- **WaterLevelController displayDate**: Carbon object passed instead of string. Fixed.
|
||||||
|
- **Storage permissions**: `storage/` and `bootstrap/cache/` chowned to UID 1000 (`www`). Log file truncated.
|
||||||
|
- **PHP-FPM 502 Bad Gateway**: Nginx cached stale DNS for `app` container. Fixed by restarting nginx.
|
||||||
|
- **Asset URL generation**: Added `ForceRequestScheme` middleware + `TrustProxies(at: '*')` so `asset()` uses the client's actual host instead of hardcoded `localhost:8080`.
|
||||||
|
- **Nginx X-Forwarded headers**: Added to fastcgi params for proper proxy support.
|
||||||
|
- **JS null guards**: `script.js` — guard `#map` and flatpickr elements. `graph.js` — guard `#hourlyGraph`. `homemap.js` — no guard needed (public homepage always has `#map`).
|
||||||
|
- **graph.js comma typo**: `input:not([readonly]),,` → `input:not([readonly]),`
|
||||||
|
|
||||||
|
### Browser Cache Issues
|
||||||
|
- Added `?h=<md5>` cache-busting suffixes to JS/CSS URLs
|
||||||
|
- Added `Cache-Control: no-cache, no-store, must-revalidate` headers via nginx for `.js`/`.css`
|
||||||
|
- Added `Cache-Control: no-cache, no-store, must-revalidate` via ForceRequestScheme middleware for HTML
|
||||||
|
- Added `<meta http-equiv="Cache-Control/Pragma/Expires">` no-cache tags in header blade
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **Timezone**: `TZ=Asia/Kuala_Lumpur` in `.env`, applied to all containers via `docker-compose.yml`
|
||||||
|
- **APP_TIMEZONE**: `Asia/Kuala_Lumpur` in `src/.env`, used by `config/app.php`
|
||||||
|
|
||||||
|
### Data
|
||||||
|
- PostgreSQL 18: Data dir path fixed to `/var/lib/postgresql/data`, PGDATA set, non-empty password
|
||||||
|
- Backup data restored from `backup/sides_20260528` (users, stations, rainfall, waterlevel, notification)
|
||||||
|
- `users.email` made nullable (admin, jpskedah have NULL emails)
|
||||||
|
- DB sequences reset to 300K+ to avoid duplicate key errors
|
||||||
|
- PostgreSQL RULEs for `ON CONFLICT DO NOTHING` on sensor tables
|
||||||
|
- Admin password reset to `sides2026` (hash compatible with Laravel)
|
||||||
|
|
||||||
|
## Quick Tasks Completed
|
||||||
|
|
||||||
|
| Date | Slug | Description | Status |
|
||||||
|
|------|------|-------------|--------|
|
||||||
|
| 2026-05-29 | browser-cache-fix | Added cache-busting hashes, no-cache headers, HTML meta tags | Complete |
|
||||||
|
| 2026-05-29 | login-passwords | Set known passwords for all 4 users, upgraded jpskedah to admin | Complete |
|
||||||
|
| 2026-05-29 | timezone-config | Added TZ=Asia/Kuala_Lumpur to all containers via docker-compose | Complete |
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
# https://github.com/agungprsty/laravel-postgres-with-docker.git
|
|
||||||
|
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
sides_net:
|
|
||||||
name: sides_net
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
container_name: sides-app
|
container_name: sides-app
|
||||||
@@ -20,17 +11,38 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- sides_net
|
- sides_net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
|
|
||||||
|
queue:
|
||||||
|
container_name: sides-queue
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: ["php", "artisan"]
|
||||||
|
command: ["queue:work", "--sleep=3", "--tries=3", "--max-time=3600"]
|
||||||
|
volumes:
|
||||||
|
- ./src:/var/www/html
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- sides_net
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
container_name: sides-db
|
container_name: sides-db
|
||||||
image: postgres:18.1
|
image: postgres:18.1
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/postgres/data:/var/lib/postgres/data
|
- ./docker/postgres/data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=${POSTGRES_DB}
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
- POSTGRES_USER=${POSTGRES_USER}
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- PGDATA=/var/lib/postgresql/data
|
||||||
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
networks:
|
networks:
|
||||||
@@ -48,37 +60,26 @@ services:
|
|||||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||||
networks:
|
networks:
|
||||||
- sides_net
|
- sides_net
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
|
|
||||||
# Database management with pgAdmin
|
|
||||||
pgadmin:
|
pgadmin:
|
||||||
image: dpage/pgadmin4
|
image: dpage/pgadmin4
|
||||||
container_name: sides-pgAdmin
|
container_name: sides-pgAdmin
|
||||||
environment:
|
environment:
|
||||||
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
|
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
|
||||||
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
|
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
|
||||||
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
ports:
|
ports:
|
||||||
- "5050:80"
|
- "5050:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/var/lib/pgadmin/storage/tck68000_gmail.com/backup
|
- pgadmin_data:/var/lib/pgadmin
|
||||||
networks:
|
networks:
|
||||||
- sides_net
|
- sides_net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Database management with Adminer
|
|
||||||
adminer:
|
|
||||||
container_name: sides-adminer
|
|
||||||
image: adminer
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "6060:8080"
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- sides_net
|
|
||||||
|
|
||||||
# Run with docker compose up -d
|
|
||||||
dozzle:
|
dozzle:
|
||||||
image: amir20/dozzle:latest
|
image: amir20/dozzle:latest
|
||||||
volumes:
|
volumes:
|
||||||
@@ -86,14 +87,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 777:8080
|
- 777:8080
|
||||||
environment:
|
environment:
|
||||||
# Uncomment to enable container actions (stop, start, restart). See https://dozzle.dev/guide/actions
|
|
||||||
- DOZZLE_ENABLE_ACTIONS=true
|
- DOZZLE_ENABLE_ACTIONS=true
|
||||||
#
|
|
||||||
# Uncomment to allow access to container shells. See https://dozzle.dev/guide/shell
|
|
||||||
- DOZZLE_ENABLE_SHELL=true
|
- DOZZLE_ENABLE_SHELL=true
|
||||||
#
|
- TZ=${TZ:-Asia/Kuala_Lumpur}
|
||||||
# Uncomment to enable authentication. See https://dozzle.dev/guide/authentication
|
|
||||||
# - DOZZLE_AUTH_PROVIDER=simple
|
|
||||||
|
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: gtstef/filebrowser:stable
|
image: gtstef/filebrowser:stable
|
||||||
@@ -102,10 +98,16 @@ services:
|
|||||||
- 8900:80
|
- 8900:80
|
||||||
user: "0:0"
|
user: "0:0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# user: filebrowser
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./filebrowser-data:/home/filebrowser/data
|
- ./filebrowser-data:/home/filebrowser/data
|
||||||
- ./files:/files
|
- ./filebrowser-files:/files
|
||||||
- /root/sides:/sides # Add other sources
|
- /root/sides:/sides
|
||||||
environment:
|
environment:
|
||||||
- "FILEBROWSER_CONFIG=data/config.yaml" # using our config file at ./data/config.yaml
|
- FILEBROWSER_CONFIG=data/config.yaml
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sides_net:
|
||||||
|
name: sides_net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgadmin_data:
|
||||||
@@ -9,7 +9,8 @@ server {
|
|||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://fonts.googleapis.com; img-src 'self' data: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; connect-src 'self' https://*.tile.openstreetmap.org;" always;
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com https://code.jquery.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://fonts.googleapis.com; img-src 'self' data: https://tile.openstreetmap.org https://*.tile.openstreetmap.org https://cctv.tck.com.my; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://unpkg.com; connect-src 'self' https://tile.openstreetmap.org https://*.tile.openstreetmap.org https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com https://cctv.tck.com.my;" always;
|
||||||
|
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
||||||
@@ -24,6 +25,10 @@ server {
|
|||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
|
fastcgi_param X-Forwarded-Proto $scheme;
|
||||||
|
fastcgi_param X-Forwarded-Host $host;
|
||||||
|
fastcgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -31,6 +36,13 @@ server {
|
|||||||
gzip_static on;
|
gzip_static on;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css)$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
}
|
||||||
|
|
||||||
location ~ /\.(?!well-known).* {
|
location ~ /\.(?!well-known).* {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|||||||
1
docker/postgres/data/PG_VERSION
Normal file
1
docker/postgres/data/PG_VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
18
|
||||||
BIN
docker/postgres/data/base/1/112
Normal file
BIN
docker/postgres/data/base/1/112
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/113
Normal file
BIN
docker/postgres/data/base/1/113
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1247
Normal file
BIN
docker/postgres/data/base/1/1247
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1247_fsm
Normal file
BIN
docker/postgres/data/base/1/1247_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1247_vm
Normal file
BIN
docker/postgres/data/base/1/1247_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1249
Normal file
BIN
docker/postgres/data/base/1/1249
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1249_fsm
Normal file
BIN
docker/postgres/data/base/1/1249_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1249_vm
Normal file
BIN
docker/postgres/data/base/1/1249_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1255
Normal file
BIN
docker/postgres/data/base/1/1255
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1255_fsm
Normal file
BIN
docker/postgres/data/base/1/1255_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1255_vm
Normal file
BIN
docker/postgres/data/base/1/1255_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1259
Normal file
BIN
docker/postgres/data/base/1/1259
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1259_fsm
Normal file
BIN
docker/postgres/data/base/1/1259_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/1259_vm
Normal file
BIN
docker/postgres/data/base/1/1259_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13476
Normal file
BIN
docker/postgres/data/base/1/13476
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13476_fsm
Normal file
BIN
docker/postgres/data/base/1/13476_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13476_vm
Normal file
BIN
docker/postgres/data/base/1/13476_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13480
Normal file
BIN
docker/postgres/data/base/1/13480
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13481
Normal file
BIN
docker/postgres/data/base/1/13481
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13481_fsm
Normal file
BIN
docker/postgres/data/base/1/13481_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13481_vm
Normal file
BIN
docker/postgres/data/base/1/13481_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13485
Normal file
BIN
docker/postgres/data/base/1/13485
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13486
Normal file
BIN
docker/postgres/data/base/1/13486
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13486_fsm
Normal file
BIN
docker/postgres/data/base/1/13486_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13486_vm
Normal file
BIN
docker/postgres/data/base/1/13486_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13490
Normal file
BIN
docker/postgres/data/base/1/13490
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13491
Normal file
BIN
docker/postgres/data/base/1/13491
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13491_fsm
Normal file
BIN
docker/postgres/data/base/1/13491_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13491_vm
Normal file
BIN
docker/postgres/data/base/1/13491_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/13495
Normal file
BIN
docker/postgres/data/base/1/13495
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/174
Normal file
BIN
docker/postgres/data/base/1/174
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/175
Normal file
BIN
docker/postgres/data/base/1/175
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2187
Normal file
BIN
docker/postgres/data/base/1/2187
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2228
Normal file
BIN
docker/postgres/data/base/1/2228
Normal file
Binary file not shown.
0
docker/postgres/data/base/1/2328
Normal file
0
docker/postgres/data/base/1/2328
Normal file
0
docker/postgres/data/base/1/2336
Normal file
0
docker/postgres/data/base/1/2336
Normal file
BIN
docker/postgres/data/base/1/2337
Normal file
BIN
docker/postgres/data/base/1/2337
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2579
Normal file
BIN
docker/postgres/data/base/1/2579
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2600
Normal file
BIN
docker/postgres/data/base/1/2600
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2600_fsm
Normal file
BIN
docker/postgres/data/base/1/2600_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2600_vm
Normal file
BIN
docker/postgres/data/base/1/2600_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2601
Normal file
BIN
docker/postgres/data/base/1/2601
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2601_fsm
Normal file
BIN
docker/postgres/data/base/1/2601_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2601_vm
Normal file
BIN
docker/postgres/data/base/1/2601_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2602
Normal file
BIN
docker/postgres/data/base/1/2602
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2602_fsm
Normal file
BIN
docker/postgres/data/base/1/2602_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2602_vm
Normal file
BIN
docker/postgres/data/base/1/2602_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2603
Normal file
BIN
docker/postgres/data/base/1/2603
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2603_fsm
Normal file
BIN
docker/postgres/data/base/1/2603_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2603_vm
Normal file
BIN
docker/postgres/data/base/1/2603_vm
Normal file
Binary file not shown.
0
docker/postgres/data/base/1/2604
Normal file
0
docker/postgres/data/base/1/2604
Normal file
BIN
docker/postgres/data/base/1/2605
Normal file
BIN
docker/postgres/data/base/1/2605
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2605_fsm
Normal file
BIN
docker/postgres/data/base/1/2605_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2605_vm
Normal file
BIN
docker/postgres/data/base/1/2605_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2606
Normal file
BIN
docker/postgres/data/base/1/2606
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2606_fsm
Normal file
BIN
docker/postgres/data/base/1/2606_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2606_vm
Normal file
BIN
docker/postgres/data/base/1/2606_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2607
Normal file
BIN
docker/postgres/data/base/1/2607
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2607_fsm
Normal file
BIN
docker/postgres/data/base/1/2607_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2607_vm
Normal file
BIN
docker/postgres/data/base/1/2607_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2608
Normal file
BIN
docker/postgres/data/base/1/2608
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2608_fsm
Normal file
BIN
docker/postgres/data/base/1/2608_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2608_vm
Normal file
BIN
docker/postgres/data/base/1/2608_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2609
Normal file
BIN
docker/postgres/data/base/1/2609
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2609_fsm
Normal file
BIN
docker/postgres/data/base/1/2609_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2609_vm
Normal file
BIN
docker/postgres/data/base/1/2609_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2610
Normal file
BIN
docker/postgres/data/base/1/2610
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2610_fsm
Normal file
BIN
docker/postgres/data/base/1/2610_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2610_vm
Normal file
BIN
docker/postgres/data/base/1/2610_vm
Normal file
Binary file not shown.
0
docker/postgres/data/base/1/2611
Normal file
0
docker/postgres/data/base/1/2611
Normal file
BIN
docker/postgres/data/base/1/2612
Normal file
BIN
docker/postgres/data/base/1/2612
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2612_fsm
Normal file
BIN
docker/postgres/data/base/1/2612_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2612_vm
Normal file
BIN
docker/postgres/data/base/1/2612_vm
Normal file
Binary file not shown.
0
docker/postgres/data/base/1/2613
Normal file
0
docker/postgres/data/base/1/2613
Normal file
BIN
docker/postgres/data/base/1/2615
Normal file
BIN
docker/postgres/data/base/1/2615
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2615_fsm
Normal file
BIN
docker/postgres/data/base/1/2615_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2615_vm
Normal file
BIN
docker/postgres/data/base/1/2615_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2616
Normal file
BIN
docker/postgres/data/base/1/2616
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2616_fsm
Normal file
BIN
docker/postgres/data/base/1/2616_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2616_vm
Normal file
BIN
docker/postgres/data/base/1/2616_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2617
Normal file
BIN
docker/postgres/data/base/1/2617
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2617_fsm
Normal file
BIN
docker/postgres/data/base/1/2617_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2617_vm
Normal file
BIN
docker/postgres/data/base/1/2617_vm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2618
Normal file
BIN
docker/postgres/data/base/1/2618
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2618_fsm
Normal file
BIN
docker/postgres/data/base/1/2618_fsm
Normal file
Binary file not shown.
BIN
docker/postgres/data/base/1/2618_vm
Normal file
BIN
docker/postgres/data/base/1/2618_vm
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user