237 lines
5.7 KiB
Markdown
237 lines
5.7 KiB
Markdown
# Flask Digital Signage (simple)
|
||
|
||
Lightweight digital signage platform using **Flask + SQLite**.
|
||
|
||
## Features
|
||
|
||
- Central **admin** can manage companies, users, displays.
|
||
- Admin can **impersonate** any company user (no password).
|
||
- Company users can:
|
||
- Create playlists
|
||
- Add slides (image/video/webpage)
|
||
- Assign playlists to displays
|
||
- Displays are public **16:9 player webpages** suitable for kiosk browsers.
|
||
|
||
## Quickstart (Windows)
|
||
|
||
```bat
|
||
python -m venv .venv
|
||
.venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
|
||
set FLASK_APP=app
|
||
flask init-db --admin-email beheer@alphen.cloud --admin-pass admin
|
||
flask run --debug
|
||
```
|
||
|
||
If Flask can't discover the app automatically, use:
|
||
|
||
```bat
|
||
set FLASK_APP=app:create_app
|
||
flask run --debug
|
||
```
|
||
|
||
Open http://127.0.0.1:5000
|
||
|
||
## Production (WSGI)
|
||
|
||
This repo includes a `wsgi.py` entrypoint for production WSGI servers.
|
||
|
||
### Important (Windows)
|
||
|
||
If you try to run Gunicorn directly on Windows you will see an error like:
|
||
|
||
```
|
||
ModuleNotFoundError: No module named 'fcntl'
|
||
```
|
||
|
||
That’s expected: **Gunicorn is Unix-only**. On Windows, run the app via:
|
||
|
||
- **Docker** (recommended) so Gunicorn runs inside a Linux container, or
|
||
- **WSL2/Linux** (Gunicorn works), or
|
||
- use a Windows-native WSGI server (e.g. Waitress) instead of Gunicorn.
|
||
|
||
Examples:
|
||
|
||
```bash
|
||
# gunicorn (Linux)
|
||
gunicorn -w 4 -b 0.0.0.0:8000 wsgi:app
|
||
|
||
# uWSGI
|
||
uwsgi --http :8000 --wsgi-file wsgi.py --callable app
|
||
```
|
||
|
||
Note: unlike `flask run`, WSGI servers typically don't auto-load `.env` / `.flaskenv`.
|
||
`wsgi.py` attempts to load `.env` (best-effort), but for real production you should set
|
||
environment variables via your process manager / secrets.
|
||
|
||
## Docker
|
||
|
||
### Docker Compose (recommended)
|
||
|
||
This repo includes a `docker-compose.yml` for a one-command startup.
|
||
|
||
On first run, the container will ensure the SQLite schema exists.
|
||
If you provide `ADMIN_PASS`, it will also create/update the initial admin user.
|
||
|
||
```powershell
|
||
docker compose up --build
|
||
```
|
||
|
||
Create an admin on startup (recommended):
|
||
|
||
```powershell
|
||
$env:ADMIN_EMAIL="you@example.com"
|
||
$env:ADMIN_PASS="YourStrongPassword"
|
||
docker compose up --build
|
||
```
|
||
|
||
Or put these in a `.env` file used by Compose.
|
||
|
||
Run in the background:
|
||
|
||
```powershell
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Stop:
|
||
|
||
```powershell
|
||
docker compose down
|
||
```
|
||
|
||
Data persistence:
|
||
|
||
- SQLite DB is mounted to `./instance` on your host
|
||
- uploads are mounted to `./app/static/uploads` on your host
|
||
|
||
Build:
|
||
|
||
```bash
|
||
docker build -t signage:latest .
|
||
```
|
||
|
||
Run (with persistent SQLite DB + uploads):
|
||
|
||
```bash
|
||
docker run --rm -p 8000:8000 \
|
||
-e SECRET_KEY="change-me" \
|
||
-v %cd%/instance:/app/instance \
|
||
-v %cd%/app/static/uploads:/app/app/static/uploads \
|
||
signage:latest
|
||
```
|
||
|
||
PowerShell variant (sometimes volume path quoting is easier):
|
||
|
||
```powershell
|
||
docker run --rm -p 8000:8000 `
|
||
-e SECRET_KEY="change-me" `
|
||
-v "${PWD}/instance:/app/instance" `
|
||
-v "${PWD}/app/static/uploads:/app/app/static/uploads" `
|
||
signage:latest
|
||
```
|
||
|
||
Then open: http://127.0.0.1:8000
|
||
|
||
Notes:
|
||
|
||
- The container starts with Gunicorn using `wsgi:app`.
|
||
- You can override Gunicorn settings via env vars:
|
||
- `GUNICORN_WORKERS` (default: 2)
|
||
- `GUNICORN_BIND` (default: `0.0.0.0:8000`)
|
||
|
||
## Notes
|
||
|
||
- SQLite DB is stored at `instance/signage.sqlite`.
|
||
- Uploaded files go to `app/static/uploads/`.
|
||
|
||
## Display player
|
||
|
||
Open:
|
||
|
||
- `http://<host>/display/<token>` for live playback (counts towards the concurrent display limit)
|
||
- `http://<host>/display/<token>?preview=1` for preview (does not count towards the concurrent display limit)
|
||
|
||
### Live updates
|
||
|
||
The player keeps itself up-to-date automatically:
|
||
|
||
- It listens to `GET /api/display/<token>/events` (Server-Sent Events) and reloads the playlist immediately when it changes.
|
||
- It also does a fallback playlist refresh every 5 minutes for networks/proxies that block SSE.
|
||
|
||
## SMTP / Forgot password
|
||
|
||
This project includes a simple **forgot password** flow. SMTP configuration is read from environment variables.
|
||
|
||
You can also configure SMTP settings from the UI: **Admin → Settings**.
|
||
Environment variables still take precedence over the database settings.
|
||
|
||
### Public domain for emails
|
||
|
||
If your app runs behind a reverse proxy (or the internal hostname differs from the public hostname),
|
||
set **Admin → Settings → Public domain** to e.g. `signage.example.com` so links in password reset
|
||
emails point to the correct address.
|
||
|
||
Recommended: put these in a local `.env` file in the repo root. Flask (via `python-dotenv`) will auto-load it on startup. `.env` is already gitignored.
|
||
|
||
You can start from `.env.example`:
|
||
|
||
```bat
|
||
copy .env.example .env
|
||
```
|
||
|
||
### Example
|
||
|
||
```bat
|
||
REM Option A: set env vars in the same terminal where you run `flask run`
|
||
set SMTP_HOST=smtp.strato.de
|
||
set SMTP_PORT=587
|
||
set SMTP_USERNAME=beheer@alphen.cloud
|
||
set SMTP_PASSWORD=***
|
||
set SMTP_FROM=beheer@alphen.cloud
|
||
set SMTP_STARTTLS=1
|
||
set SMTP_DEBUG=1
|
||
|
||
REM Option B: put the same keys/values in a .env file instead
|
||
```
|
||
|
||
Security note: do **not** commit SMTP passwords to the repo. Prefer secrets management and rotate leaked credentials.
|
||
|
||
Note on the "From" address: some SMTP providers enforce that the authenticated mailbox
|
||
(`SMTP_USERNAME`) is used as the actual sender (envelope-from), even if a different
|
||
`SMTP_FROM` is provided. In that case the app sets a `Reply-To` header so replies still
|
||
go to `SMTP_FROM`, but the provider may still show the username address as the sender.
|
||
|
||
### Troubleshooting mail delivery
|
||
|
||
If the reset email is not received:
|
||
|
||
1. Set `SMTP_DEBUG=1` and request a reset again.
|
||
2. Watch the Flask console output for SMTP responses / errors.
|
||
3. Verify:
|
||
- `SMTP_USERNAME` and `SMTP_FROM` are allowed by your provider.
|
||
- You are using STARTTLS (port 587).
|
||
- The recipient mailbox isn’t filtering it (spam/quarantine).
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|