updates to digiserver app server
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# 03 · Data Model
|
||||
|
||||
DigiServer v2 uses **SQLAlchemy 2.0** with **SQLite** (production: `instance/dashboard.db`, dev: `instance/dev.db`, tests: in-memory). All models live in `app/models/` and are re-exported from `app/models/__init__.py`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entity-Relationship Overview
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
user ||--o{ server_log : "writes"
|
||||
player ||--o{ player_feedback : "sends (cascade)"
|
||||
player ||--o{ player_edit : "edits (cascade)"
|
||||
player }o--o| playlist : "assigned_to"
|
||||
content ||--o{ player_edit : "edited (cascade)"
|
||||
content ||--o{ player_feedback : "playing"
|
||||
playlist ||--o{ content : "playlist_content M2M (position,duration,muted,edit_on_player_enabled)"
|
||||
content }o--o{ group : "group_content M2M (legacy)"
|
||||
player_user ||--o{ player_edit : "user_code"
|
||||
https_config ||--o| https_config : "singleton row"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Tables & Columns
|
||||
|
||||
### `user` — human accounts
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `username` | String(80) | unique, NOT NULL, indexed |
|
||||
| `password` | String(120) | bcrypt hash, NOT NULL |
|
||||
| `role` | String(20) | default `'user'`; also `'admin'`, `'viewer'`; indexed |
|
||||
| `theme` | String(20) | default `'light'` |
|
||||
| `created_at` | DateTime | NOT NULL |
|
||||
| `last_login` | DateTime | nullable |
|
||||
|
||||
Key methods: `is_admin` (property), `update_last_login()`. Mixin: `UserMixin` (Flask-Login).
|
||||
|
||||
### `player` — signage devices
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `name` | String(255) | NOT NULL |
|
||||
| `hostname` | String(255) | unique, NOT NULL, indexed |
|
||||
| `location` | String(255) | nullable |
|
||||
| `auth_code` | String(255) | unique, NOT NULL, indexed (legacy Bearer auth) |
|
||||
| `password_hash` | String(255) | NOT NULL (bcrypt) |
|
||||
| `quickconnect_code` | String(255) | nullable, bcrypt-hashed |
|
||||
| `orientation` | String(16) | default `'Landscape'` |
|
||||
| `status` | String(50) | default `'offline'`, indexed |
|
||||
| `last_seen` | DateTime | indexed |
|
||||
| `last_heartbeat` | DateTime | indexed |
|
||||
| `created_at` | DateTime | NOT NULL |
|
||||
| `playlist_id` | Integer | FK → `playlist.id` ON DELETE SET NULL, indexed |
|
||||
| `deployment_status` | String(50) | default `'pending'`; pending/deploying/deployed/failed |
|
||||
| `last_deployment_at` | DateTime | nullable |
|
||||
| `last_deployment_status` | String(50) | nullable |
|
||||
| `last_deployment_message` | Text | nullable |
|
||||
|
||||
Relationships: `playlist`, `feedback` (→ PlayerFeedback, cascade delete-orphan).
|
||||
Methods: `is_online` (5-min window), `update_status()`, `set_password()/check_password()`, `set_quickconnect_code()/check_quickconnect_code()`, static `authenticate(hostname, password, quickconnect_code)`.
|
||||
|
||||
### `content` — media items (also weblinks)
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `filename` | String(255) | unique, NOT NULL, indexed; may point to `edited_media/<id>/...` after a player edit |
|
||||
| `original_filename` | String(255) | nullable, indexed — pristine upload name |
|
||||
| `content_type` | String(50) | NOT NULL, indexed; image/video/pdf/pptx/weblink/other |
|
||||
| `duration` | Integer | default 10, nullable |
|
||||
| `file_size` | BigInteger | nullable |
|
||||
| `url` | String(2048) | nullable — target URL for `weblink` content |
|
||||
| `description` | Text | nullable |
|
||||
| `uploaded_at` | DateTime | NOT NULL, indexed |
|
||||
|
||||
Relationships: `playlists` (M2M via `playlist_content`), `groups` (M2M via `group_content`).
|
||||
Properties/methods: `file_size_mb`, `group_count`, `original_display_name`, `original_media_path`, `current_media_path`, `is_image()/is_video()/is_pdf()/is_weblink()`, `has_player_edits`.
|
||||
|
||||
### `playlist` — ordered collections of content
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `name` | String(100) | unique, NOT NULL, indexed |
|
||||
| `description` | Text | nullable |
|
||||
| `orientation` | String(20) | default `'Landscape'` |
|
||||
| `version` | Integer | default 1, NOT NULL — **sync detection** |
|
||||
| `is_active` | Boolean | default True |
|
||||
| `created_at` | DateTime | NOT NULL |
|
||||
| `updated_at` | DateTime | NOT NULL, onupdate |
|
||||
|
||||
Methods: `player_count`, `content_count`, `total_duration` (properties), `increment_version()`, `get_content_ordered()`.
|
||||
|
||||
### `playlist_content` — association table (M2M playlist ↔ content)
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `playlist_id` | Integer | FK → `playlist.id` CASCADE, composite PK |
|
||||
| `content_id` | Integer | FK → `content.id` CASCADE, composite PK |
|
||||
| `position` | Integer | default 0 — ordering |
|
||||
| `duration` | Integer | default 10 — per-playlist override |
|
||||
| `muted` | Boolean | default True |
|
||||
| `edit_on_player_enabled` | Boolean | default False — whether player-side editing allowed |
|
||||
|
||||
### `player_edit` — on-player media edits
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `player_id` | Integer | FK → `player.id` CASCADE, indexed |
|
||||
| `content_id` | Integer | FK → `content.id` CASCADE, indexed |
|
||||
| `original_name` | String(255) | NOT NULL |
|
||||
| `new_name` | String(255) | NOT NULL |
|
||||
| `version` | Integer | default 1 |
|
||||
| `user` | String(255) | nullable (user code) |
|
||||
| `time_of_modification` | DateTime | nullable |
|
||||
| `metadata_path` | String(512) | nullable |
|
||||
| `edited_file_path` | String(512) | NOT NULL |
|
||||
| `created_at` | DateTime | NOT NULL, indexed |
|
||||
|
||||
Relationships: `player`, `content`. Method: `to_dict()`.
|
||||
|
||||
### `player_feedback` — status messages from players
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `player_id` | Integer | FK → `player.id`, NOT NULL, indexed |
|
||||
| `status` | String(50) | default `'unknown'` |
|
||||
| `current_content_id` | Integer | FK → `content.id`, nullable |
|
||||
| `message` | Text | nullable |
|
||||
| `error` | Text | nullable |
|
||||
| `timestamp` | DateTime | NOT NULL, indexed |
|
||||
|
||||
Properties/classmethod: `is_error`, `age_seconds`, `get_latest_for_player()`.
|
||||
|
||||
### `player_user` — mapping of player edit user codes
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `user_code` | String(255) | **globally unique**, NOT NULL, indexed |
|
||||
| `user_name` | String(255) | nullable |
|
||||
| `created_at` | DateTime | NOT NULL |
|
||||
| `updated_at` | DateTime | NOT NULL, onupdate |
|
||||
|
||||
Method: `to_dict()`. *(Migrated from a per-player table — see `migrate_player_user_global.py`.)*
|
||||
|
||||
### `server_log` — DB-backed audit log
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `level` | String(20) | NOT NULL, indexed, default `'info'` |
|
||||
| `message` | Text | NOT NULL |
|
||||
| `timestamp` | DateTime | NOT NULL, indexed |
|
||||
|
||||
Classmethods: `log_info`, `log_warning`, `log_error`.
|
||||
|
||||
### `https_config` — HTTPS settings (singleton row)
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `https_enabled` | Boolean | default False |
|
||||
| `hostname` | String(255) | nullable |
|
||||
| `domain` | String(255) | nullable |
|
||||
| `ip_address` | String(45) | nullable (IPv6-capable) |
|
||||
| `email` | String(255) | nullable (Let's Encrypt contact) |
|
||||
| `port` | Integer | default 443 |
|
||||
| `created_at` | DateTime | NOT NULL |
|
||||
| `updated_at` | DateTime | NOT NULL, onupdate |
|
||||
| `updated_by` | String(255) | nullable |
|
||||
|
||||
Classmethods: `get_config()` (first row), `create_or_update(...)`. Method: `to_dict()`.
|
||||
|
||||
### `group` + `group_content` — **ARCHIVED / LEGACY**
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | Integer | PK |
|
||||
| `name` | String(100) | unique, NOT NULL, indexed |
|
||||
| `description` | Text | nullable |
|
||||
| `created_at` / `updated_at` | DateTime | NOT NULL |
|
||||
|
||||
`group_content(group_id, content_id)` composite-PK M2M. The current `Player` model has **no** `group_id` column — group features are archived (see [09 · Legacy](09-legacy-and-migrations.md)).
|
||||
|
||||
---
|
||||
|
||||
## 3. Relationship Summary
|
||||
|
||||
| Relationship | Cardinality | FK / Mechanism |
|
||||
|---|---|---|
|
||||
| `playlist` → `content` | M:N | `playlist_content` (positioned, with extras) |
|
||||
| `group` → `content` | M:N | `group_content` (legacy) |
|
||||
| `player` → `playlist` | N:1 | `player.playlist_id` (ON DELETE SET NULL) |
|
||||
| `player` → `player_feedback` | 1:N | `player_feedback.player_id` (cascade) |
|
||||
| `player` → `player_edit` | 1:N | `player_edit.player_id` (cascade) |
|
||||
| `content` → `player_edit` | 1:N | `player_edit.content_id` (cascade) |
|
||||
| `player_user` → `player_edit` | 1:N | `player_edit.user` = `player_user.user_code` (logical) |
|
||||
| `https_config` | 1 row | singleton via `get_config()` |
|
||||
|
||||
---
|
||||
|
||||
> Next: [04 · Application Core](04-application-core.md)
|
||||
Reference in New Issue
Block a user