# 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`) ## Release helper (git + docker publish) This repo includes a small helper to: 1) ask for a **commit message** and **version** 2) commit + push to the `openslide` git remote 3) build + push Docker images: - `git.alphen.cloud/bramval/openslide:` - `git.alphen.cloud/bramval/openslide:latest` Run (interactive): ```bash python scripts/release.py ``` Run (non-interactive): ```bash python scripts/release.py --version 1.2.3 --message "Release 1.2.3" ``` Dry-run (prints commands only): ```bash python scripts/release.py --version 1.2.3 --message "Release 1.2.3" --dry-run ``` ## Notes - SQLite DB is stored at `instance/signage.sqlite`. - Uploaded files go to `app/static/uploads/`. ## Display player Open: - `http:///display/` for live playback (counts towards the concurrent display limit) - `http:///display/?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//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. ## Ticker tape (RSS headlines) Each display can optionally show a **bottom ticker tape** with scrolling news headlines. Configure it as a company user via: - **Dashboard → Displays → Configure display → Ticker tape** Options: - Enable/disable ticker entirely - RSS feed URL (public http/https) - Text color (picker) - Background color + opacity - Font (dropdown) - Font size - Speed Implementation notes: - Headlines are fetched server-side via `GET /api/display//ticker` and cached briefly. - The player applies styles and refreshes headlines periodically. ## 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).