docs: rewrite all documentation to reflect current state

- Remove adminer references (service was removed)
- Remove mermaid diagrams (ASCII only)
- Remove hardcoded credentials (use env var references)
- Update all Docker references to 4-container setup (app, postgres, web, pgadmin)
- Document env-based admin credentials (ADMIN_EMAIL/ADMIN_PASSWORD)
- Document parameterized queries (SQL injection fixed)
- Document FCM topic routing by stationtype+level
- Document siren stationtype=3 fix in sidesdecode.py
- Document idempotent seeder (firstOrCreate)
- Document reverse proxy setup in deployment guide
- Remove Makefile references (Docker Compose only)
This commit is contained in:
root
2026-05-21 02:59:32 +08:00
parent c1b2a8d553
commit 6863f39a24
7 changed files with 1116 additions and 658 deletions

View File

@@ -1,131 +1,260 @@
# Database Schema
# Database
## Tables
SIDES uses **PostgreSQL 16** running in a Docker container. Laravel connects
via `pgsql` driver using the Docker service name as the host.
### `station`
## Connection Details
Primary table storing telemetry station metadata.
| Setting | Value | Source |
|-----------------|------------------------------|---------------------------|
| DB_CONNECTION | `pgsql` | `src/.env` |
| DB_HOST | `postgres` (Docker service) | `src/.env` |
| DB_PORT | `5432` | `src/.env` |
| DB_DATABASE | `${POSTGRES_DB}` | `src/.env` |
| DB_USERNAME | `${POSTGRES_USER}` | `src/.env` |
| DB_PASSWORD | `${POSTGRES_PASSWORD}` | `src/.env` |
| Column | Type | Description |
|--------|------|-------------|
| `stationid` | `varchar` (PK) | Unique station identifier (e.g., "KBLG0026") |
| `name` | `varchar(255)` | Station display name |
| `district` | `varchar(255)` | District location |
| `lng` | `float` | Longitude coordinate |
| `lat` | `float` | Latitude coordinate |
| `mainriverbasin` | `varchar(255)` | Main river basin name |
| `subriverbasin` | `varchar(255)` | Sub river basin name |
| `rainfall` | `integer` | Has rainfall sensor (1=yes, 0=no) |
| `waterlevel` | `integer` | Has water level sensor (1=yes, 0=no) |
| `siren` | `integer` (nullable) | Has siren (1=yes, 0=no) |
| `cctv_link` | `varchar(500)` (nullable) | URL to CCTV feed |
Default database name: **sides_db** (set by `POSTGRES_DB` in the root `.env`).
### `rainfall`
The container (`tckdev-db`) uses a named Docker volume **pgdata** for
persistent storage. Data survives container restarts and rebuilds.
Stores rainfall readings from telemetry stations.
| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` (PK, auto) | Auto-increment ID |
| `stationid` | `varchar` | Station identifier (FK to station) |
| `timestamp` | `timestamp` | Reading timestamp |
| `anncum` | `double` | Annual cumulative rainfall |
| `daily` | `double` | Daily cumulative rainfall |
| `hourly` | `double` | Hourly rainfall |
| `currentrf` | `double` | Current rainfall |
| `battery` | `double` | Battery voltage |
### `waterlevel`
Stores water level readings with threshold values.
| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` (PK, auto) | Auto-increment ID |
| `stationid` | `varchar` | Station identifier (FK to station) |
| `datetime` | `timestamp` | Reading timestamp |
| `waterlevel` | `double` | Current water level (meters) |
| `alert` | `double` | Alert threshold level |
| `warning` | `double` | Warning threshold level |
| `danger` | `double` | Danger threshold level |
### `siren`
Stores siren activation records.
| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` (PK, auto) | Auto-increment ID |
| `stationid` | `varchar` | Station identifier (FK to station) |
| `stationtype` | `varchar` | Station type identifier |
| `active_time` | `timestamp` | Siren activation time |
| `level` | `varchar` | Siren level (`H`=Danger, `L`=Warning, `N`=Normal) |
### `notification`
Stores threshold-exceeded alert records.
| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` (PK, auto) | Auto-increment ID |
| `stationid` | `varchar` | Station identifier (FK to station) |
| `timestamp` | `timestamp` | Alert timestamp |
| `stationtype` | `integer` | Type: 1=rainfall, 2=waterlevel, 3=siren |
| `level` | `varchar` | Alert level (Alert, Warning, Danger) |
| `active_time` | `timestamp` (nullable) | Activation time |
### `users`
Stores application users.
| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` (PK, auto) | Auto-increment ID |
| `name` | `varchar(255)` | Username |
| `email` | `varchar(255)` (unique) | Email address |
| `email_verified_at` | `timestamp` (nullable) | Email verification timestamp |
| `password` | `varchar(255)` | Bcrypt-hashed password |
| `access_level` | `integer` | 1=Admin, 2=User |
| `is_blocked` | `boolean` | Account blocked status |
| `login_attempts` | `integer` | Failed login attempt count |
| `remember_token` | `varchar` | Remember me token |
| `created_at`, `updated_at` | `timestamp` | Laravel timestamps |
### Laravel Standard Tables
- `password_reset_tokens` — Password reset tokens
- `sessions` — Database-backed sessions
- `cache` / `cache_locks` — Cache store
- `jobs` / `job_batches` / `failed_jobs` — Queue system
- `migrations` — Migration tracking
## Relationships
## Entity-Relationship Diagram
```
station (1) ──< (many) rainfall via stationid
station (1) ──< (many) waterlevel via stationid
station (1) ──< (many) siren via stationid
station (1) ──< (many) notification via stationid
station (PK: stationid)
|--------------|
| stationid |---+----------------+----------------+
| name | | | |
| district | | | |
| lng, lat | | | |
| mainriverbasin| | | |
| subriverbasin| | | |
| rainfall | | | |
| waterlevel | | | |
| siren | | | |
| cctv_link | | | |
|--------------| | | |
v v v
rainfall waterlevel siren
(PK: id) (PK: id) (PK: id)
|--------------| |--------------| |--------------|
| id | | id | | id |
| stationid | | stationid | | stationid |
| timestamp | | datetime | | stationtype |
| anncum | | waterlevel | | active_time |
| daily | | alert | | level |
| hourly | | warning | |--------------|
| currentrf | | danger |
| battery | |--------------|
|--------------|
| |
v v
notification (PK: id)
|--------------|
| id |
| stationid |
| timestamp |
| stationtype |
| level |
| active_time |
|--------------|
```
**Note**: No database-level foreign keys or constraints exist. All relationships are maintained at the application level.
**Note:** There are no database-level foreign keys between `station` and the
data tables. Relationships are enforced at the application level by matching
`stationid` values.
## Indexes
## Application Tables
- `users.email` — unique index
- `sessions.last_activity` — index
- `sessions.user_id` — index
- No additional indexes on data tables (potential performance concern)
### station
## Default Data
Primary reference table for all monitoring stations.
### Default Admin User
| Column | Type | Nullable | Notes |
|-----------------|---------------|----------|----------------------|
| stationid | string (PK) | no | External station ID |
| name | string | no | Station display name |
| district | string | no | |
| lng | float | no | Longitude |
| lat | float | no | Latitude |
| mainriverbasin | string | no | |
| subriverbasin | string | no | |
| rainfall | integer | no | Flag/counter |
| waterlevel | integer | no | Flag/counter |
| siren | integer | yes | Added by migration |
| cctv_link | string | yes | Added by migration |
Created via `DatabaseSeeder` and migration `2025_12_11_124201_add_default_user_to_users_table.php`:
Migration: `2025_11_06_071853_create_station_table.php`
Alter migrations: `2025_11_08_004548`, `2025_11_25_113158`
- **Username**: `admin` (seeder) / `admin` (migration)
- **Email**: `admin@example.com`
- **Password**: `password123`
- **Access Level**: 1 (Admin)
### rainfall
**Note**: The admin user is created in both the seeder AND a migration, which would cause a duplicate key error if both run.
Rainfall sensor readings per station.
| Column | Type | Nullable |
|------------|-------------|----------|
| id | bigint (PK) | no |
| stationid | string | no |
| timestamp | timestamp | no |
| anncum | double | no |
| daily | double | no |
| hourly | double | no |
| currentrf | double | no |
| battery | double | no |
Migration: `2025_11_06_072709_create_rainfall__table.php`
### waterlevel
Water level sensor readings per station.
| Column | Type | Nullable |
|-------------|-------------|----------|
| id | bigint (PK) | no |
| stationid | string | no |
| datetime | timestamp | no |
| waterlevel | double | no |
| alert | double | no |
| warning | double | no |
| danger | double | no |
Migration: `2025_11_06_072738_create_waterlevel_table.php`
### siren
Siren activation records.
| Column | Type | Nullable |
|--------------|-------------|----------|
| id | bigint (PK) | no |
| stationid | string | no |
| stationtype | string | no |
| active_time | timestamp | no |
| level | string | no |
Migration: `2025_11_07_031601_create_siren__table.php`
The `stationtype` column is always `3` for siren inserts (see stationtype
values below).
### notification
Notification log for alerts across all station types.
| Column | Type | Nullable |
|--------------|-------------|----------|
| id | bigint (PK) | no |
| stationid | string | no |
| timestamp | timestamp | no |
| stationtype | integer | no |
| level | string | no |
| active_time | timestamp | yes |
Migration: `2025_11_07_024940_create_notification_table.php`
### users
Application users with access control and blocking support.
| Column | Type | Nullable | Default | Notes |
|--------------------|-------------|----------|---------|-------------------|
| id | bigint (PK) | no | auto | |
| name | string | no | | |
| email | string | no | | unique |
| email_verified_at | timestamp | yes | | |
| password | string | no | | bcrypt hashed |
| access_level | integer | no | 2 | 1=admin, 2=user |
| is_blocked | boolean | no | false | |
| login_attempts | integer | no | 0 | |
| remember_token | string | yes | | |
| created_at | timestamp | yes | | |
| updated_at | timestamp | yes | | |
Base: `0001_01_01_000000_create_users_table.php`
Alter: `2025_11_07_063825`, `2025_11_07_065418`
## stationtype Values
The `stationtype` field is used in the `notification` and `siren` tables to
identify the source station type.
| Value | Type |
|-------|-------------|
| 1 | Rainfall |
| 2 | Water level |
| 3 | Siren |
## Laravel Standard Tables
These tables are created by Laravel's built-in migrations and support
framework features (sessions, caching, queue jobs).
| Table | Purpose |
|-----------------------|-------------------------------|
| password_reset_tokens | Password reset tokens |
| sessions | Database-backed sessions |
| cache | Application cache store |
| cache_locks | Atomic cache locks |
| jobs | Queue job queue |
| job_batches | Batched job tracking |
| failed_jobs | Failed job records |
| migrations | Migration tracking |
## Default Admin User
The `DatabaseSeeder` creates a default admin user on first run using
`firstOrCreate`, making it idempotent -- safe to re-run without duplicates.
```php
User::firstOrCreate(
['email' => env('ADMIN_EMAIL', 'admin@example.com')],
[
'name' => 'Admin',
'password' => Hash::make(env('ADMIN_PASSWORD', str()->random(32))),
'access_level' => 1,
]
);
```
- **Email**: set by `ADMIN_EMAIL` env var (default: `admin@example.com`)
- **Password**: set by `ADMIN_PASSWORD` env var (default: random 32-char string)
- **access_level**: `1` (admin)
The seeder is triggered automatically when `RUN_SEEDER=true` in the Docker
environment (see `docker/entrypoint.sh`).
The migration `2025_12_11_124201_add_default_user_to_users_table.php` exists
but is a no-op (empty `up()` and `down()`) -- admin creation is handled
exclusively by the seeder.
## Seeding and Migrations
Migrations and seeding run automatically at container startup via
`docker/entrypoint.sh`:
```bash
# Runs when RUN_MIGRATIONS=true
php artisan migrate --force
# Runs when RUN_SEEDER=true
php artisan db:seed --force
```
Both `RUN_MIGRATIONS` and `RUN_SEEDER` default to `true` in
`docker-compose.yml`.
## Design Notes
**No foreign keys.** The `stationid` column in `rainfall`, `waterlevel`,
`siren`, and `notification` references `station.stationid` at the application
level only. There are no database-level `REFERENCES` constraints. This means
orphaned data rows can exist if a station is deleted without cleaning
associated records.
**No additional indexes.** Beyond primary keys and Laravel's default indexes
(on `sessions.user_id`, `sessions.last_activity`, `jobs.queue`), no custom
indexes exist on the data tables. This is a potential performance concern as
the `rainfall`, `waterlevel`, and `notification` tables grow -- queries
filtering by `stationid` or ordering by timestamp will perform full table
scans.