Files
openslide/README.md
2026-01-26 15:34:52 +01:00

7.2 KiB
Raw Blame History

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)

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:

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'

Thats 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:

# 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

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.

docker compose up --build

Create an admin on startup (recommended):

$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:

docker compose up -d --build

Stop:

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:

docker build -t signage:latest .

Run (with persistent SQLite DB + uploads):

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):

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:<version>
    • git.alphen.cloud/bramval/openslide:latest

Run (interactive):

python scripts/release.py

Run (non-interactive):

python scripts/release.py --version 1.2.3 --message "Release 1.2.3"

Dry-run (prints commands only):

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://<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.

Ticker tape (RSS headlines)

Each display can optionally show a bottom ticker tape with scrolling news headlines.

Configure RSS + styling as a company user via:

  • My Company → Ticker tape (RSS)

Company-level options:

  • RSS feed URL (public http/https)
  • Text color (picker)
  • Background color + opacity
  • Font (dropdown)
  • Font size
  • Speed

Per-display option:

  • Enable/disable ticker on that display (Dashboard → Displays → Configure display)

Implementation notes:

  • Headlines are fetched server-side via GET /api/display/<token>/ticker and cached in-memory.
  • The player reads the company ticker settings via GET /api/display/<token>/playlist.
  • The player auto-refreshes headlines without restart on a long interval (default: 12 hours, override via ?ticker_poll=seconds).
  • Server-side cache TTL defaults to 6 hours (override via env var TICKER_CACHE_TTL_SECONDS).

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:

copy .env.example .env

Example

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 isnt filtering it (spam/quarantine).