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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user