Self-hosted digital signage that runs in any modern browser. No dedicated player software needed — point a browser at a URL and it displays your content. Manage multiple screens and zones from a single web-based admin panel.
- Multiple displays — each screen gets its own URL (
/display/<slug>) - Multi-zone layouts — split a display into up to 4 zones (7 presets), each with its own playlist
- Playlist per display/zone — drag-and-drop ordering, per-item duration
- Media types: PDF (multi-page), image, image gallery, local video, YouTube embed, website (via proxy)
- Server-side PDF rendering — PDFs pre-rendered to images via poppler-utils, no client-side PDF engine needed; parallel rendering across CPU cores
- PDF spread view — side-by-side page pairs (book/magazine layout), configurable per playlist item
- Image galleries — batch upload, sortable, shown as slideshow
- Website proxy — embeds external pages by stripping X-Frame-Options/CSP headers
- Admin panel — upload, rename, delete media; manage playlists; configure per-display settings
- Authentication — single admin user with bcrypt-hashed password
- Auto-updating display — polls for changes every 10 seconds, no manual refresh needed
- Backend: Flask 3 (Python)
- WSGI Server: Waitress (production-ready)
- Authentication: Flask-Login + bcrypt
- Database: SQLite
- PDF Rendering: poppler-utils (
pdftoppm) - Frontend: Vanilla JS + SortableJS for drag-and-drop
git clone https://github.com/patrickfiedler/infoboard.git
cd infoboard
./deploy.shThe deployment script will guide you through setup interactively.
-
Clone and install dependencies
git clone https://github.com/patrickfiedler/infoboard.git cd infoboard pip install -r requirements.txt -
Configure
python3 generate_password_hash.py # generate bcrypt hash for your password cp config.json.example config.json # edit config.json with your settings
-
Run
python3 app.py
See DEPLOYMENT.md for systemd service setup and update procedures.
- Python 3.8+
poppler-utilsfor PDF rendering:apt install poppler-utils
config.json (created from config.json.example):
| Key | Default | Description |
|---|---|---|
admin_username |
admin |
Admin login username |
admin_password_hash |
— | Bcrypt hash (generate with generate_password_hash.py) |
secret_key |
— | Flask session secret — change this! |
port |
8080 |
Server port |
host |
0.0.0.0 |
Bind address |
upload_folder |
uploads |
Media storage directory |
render_workers |
null |
Max parallel PDF render workers (null = all CPU cores) |
After starting the server:
- Display:
http://localhost:8080/display/<slug>— open fullscreen in a browser - Admin:
http://localhost:8080/admin— manage content and displays
The display polls for updates every 10 seconds. No refresh needed when you change content in the admin panel.
./firefox-kiosk.sh http://localhost:8080/display/<slug>infoboard/
├── app.py # All routes and business logic
├── models.py # Database operations
├── migrate.py # Migration runner
├── config.py # Configuration loader
├── migrations/ # Numbered migration scripts
├── templates/
│ ├── display.html # Fullscreen display view
│ ├── admin.html # Admin panel
│ └── login.html # Login page
├── static/js/
│ └── sortable.min.js # Drag-and-drop (SortableJS)
├── deploy.sh # Automated deployment
├── update.sh # Automated update
├── firefox-kiosk.sh # Launch Firefox in kiosk mode
├── cookie_hide.conf # CSS selectors to hide on proxied pages
├── config.json.example # Config template
├── generate_password_hash.py # Generate bcrypt password hash
└── infoboard.service # Systemd service template
- Admin password stored as bcrypt hash (cost factor 12)
- File uploads restricted to allowed extensions
- Uploaded files stored with UUID filenames (prevents path traversal)
- Session management via Flask-Login
cd /path/to/infoboard
./update.shThe update script creates a backup tag, pulls the latest code, updates dependencies and restarts the service. See DEPLOYMENT.md for rollback instructions.
MIT