A self-hosted PHP control panel and content app for a Visionect e-ink frame, packaged with Docker Compose and built around a live admin UI. Supports multiple modules, module previews, scheduling, manual overrides, and Home Assistant / Node-RED automation hooks.
This project is made for the Visionect Place & Play 32 frame and a portrait rendering target of 1440 × 2560 however you can change the target resolution in the setting to accomedate your frame size. I've been running this since 2020, just finally got to making an admin ui for it.
Initial module design, the original project idea, and the early cron-based content pipeline are credited to Arjan Haverkamp big thanks to him for sharing original concept and code. This project was built and iterated with some AI assistance. Some of the codebase, admin workflows, setup flow, documentation, and UI refinements were developed through with Claude Code and Codex.
- Clone this repo onto the machine that will host the stack.
- Copy
.env.exampleto.env. - Make sure Docker and Docker Compose are installed.
- Start the stack:
docker compose up -d --build- Open:
- Visionect server admin (only needed if you are running Visionect frame):
http://YOUR_HOST:8081 - Display manager admin (Configuration panel):
http://YOUR_HOST:4412/admin/ - Public frame shell (Point your eInk display here):
http://YOUR_HOST:4412/
- On first visit to
/admin, create the single admin account. - Open the module panels and configure any API keys or sources you need.
- For
Newspaper,Comics, andAiNews, use the manual run buttons in/adminonce so the first content is generated immediately. If using goComics you will need to get the initial cookie from your browser and paste it in the settings for that module. - In
Generalsettings, keep the frame resolution set to your real panel size. For the Visionect Place & Play 32 used here, that is1440 × 2560. - If you already use the frame's own UI sleep schedule, mirror that same
Wake timeandSleep timeinGeneralsettings so the dashboard knows when the screen is asleep and can avoid misleading live actions.
Out-of-the-box demo behavior:
Art,Haynesmann,Quotes, andComicsship with sample imagesNewspaperships with two example paper selections and bundled sample front pagesAiNewsships with sample story data and example images so the module can render before you add provider keys
- live frame health, current module, and exact last-served asset tracking
- a
Current framepreview based on the actual URL the frame last requested - a
Current modulepreview tool so you can test modules without waiting for the schedule - per-module enable toggles in both the sidebar and module panels
- manual cron buttons for
Newspaper,Comics, andAiNews - local-first automation hooks through
control.php Generalsettings for frame resolution and mirrored frame sleep timesHome Assistantsettings for presence pause
The sleep schedule in this app does not put the panel to sleep by itself. It mirrors the sleep window you already configured on the physical frame so the admin UI can show the correct state, avoid pretending that reload or jump actions will take effect while the panel is asleep, and show wake time as the next update target while asleep.
Use:
visionect/visionect-server-v3:7.6.5-arm
Do not casually upgrade the vss container image beyond that.
Later Visionect Server versions after 7.6.5-arm require a subscription. This repo intentionally keeps the Docker Compose example pinned to visionect/visionect-server-v3:7.6.5-arm.
app/- the PHP app, module code, admin UI, scheduler worker, and runtime helpers
docker-compose.yml- full stack example for Visionect Server, PostgreSQL, Redis, and the custom PHP/Apache app
docker/php-apache/Dockerfile- the image used for the custom web app / scheduler / websocket worker
If you hand this GitHub repo to an AI assistant, the minimum setup context it should follow is:
- Start Docker Compose from the repo root.
- Keep the
vssimage pinned tovisionect/visionect-server-v3:7.6.5-arm. - Use
/adminfirst-run setup to create the admin account. - Use
/adminto configure:- General frame settings
- Home Assistant presence pause
- AiNews provider keys
- Newspaper list
- Schedule and module enablement
- Use the manual module-run buttons to fetch initial content.
- Use
http://HOST:4412/control.phpfor Node-RED / Home Assistant automations instead of the raw websocket.
- Docker Engine
- Docker Compose
- an ARM host that can run
visionect/visionect-server-v3:7.6.5-arm - access to a Visionect frame / Visionect Server environment
- outbound internet access if you want:
- newspaper fetching
- comics fetching
- AiNews RSS + provider image generation
- Home Assistant API integration
The repo is intentionally usable before you configure secrets.
Works right away:
- Clock
- Art
- Haynesmann
- Quotes
- Comics
- Newspaper using bundled sample front pages and two example configured papers
- AiNews using bundled example stories and images
Useful without credentials but with manual refresh:
Newspapercan rotate bundled sample front pagesComicscan render the bundled sample strips immediatelyAiNewscan show bundled sample stories even before you add any provider keys
Needs credentials only for regeneration or live integrations:
- AiNews image generation providers
- Home Assistant pause integration
Create .env from .env.example:
TZ=America/Phoenix
PUID=1000
PGID=1000These values are used by the containers for timezone and file ownership.
The Compose stack includes:
vss- Visionect Server
postgres_db- PostgreSQL for Visionect Server
redis- Redis for Visionect Server
php-apache- the custom display manager app, cron runner, and websocket worker
cookie-refresh- GoComics BunnyCDN bypass service (see below)
Published ports:
8081- Visionect Server admin (only needed if you have Visionect Frame)
4412- public display shell and admin UI (point your eink display frame here)
12345- websocket runtime server used by the frame shell and admin
Module crons (newspaper, comics, ainews) do not run when the container starts, so restarts are fast and never spend paid API credits. Schedule them from the host instead, for example in the root crontab:
# daily content refresh, 06:05 local time
5 6 * * * docker exec visionect-web-content php /app/cli/cron.php --run >/dev/null 2>&1
# optional: weekly restart for the long-running websocket worker (runs no crons)
40 3 * * 1 docker restart visionect-web-content >/dev/null 2>&1cli/cron.php options:
- no arguments: does nothing (this is what the container runs at startup)
--run: runs every module enabled inPREFS.json(ainews last); modules missing fromPREFS.jsonare treated as disabled--run <module>: runs one module--dry-run: lists what would run
Exit codes and output are recorded in app/config/runtime_status.json. You can also use the per-module "Run now" button in /admin.
The public repo does not ship an admin account or secret key.
At runtime, the app generates or creates:
app/config/admin_account.jsonapp/config/secret_key.b64app/config/runtime_status.jsonapp/config/remote_control.jsonapp/config/ha_integration.jsonapp/config/general_settings.json
Reference examples included in the repo:
app/config/ha_integration.example.jsonapp/config/general_settings.example.json
The first time you visit /admin:
- if no admin account exists, you will see the first-run setup screen
- the first-run screen creates the single admin account
- passwords are hashed
- Home Assistant tokens and AiNews provider keys are encrypted at rest once entered
![]() ![]() |
|
![]() |
Just a gallery for your art, add whatever you want. If the image is color it will still show up as grayscale
|
![]() |
Just another gallery, shows design of the day from Haynesmann
|
![]() |
Essentically the same as photo gallery, I just made it for quotes for myself.
|
GoComics is served behind BunnyCDN, and plain curl requests are blocked. You need to create a free GoComics account then get the session cookie using a chrome extension (you only have to do this once)
The cookie-refresh Docker service auto refreshes the cookie for you:
- On container start and daily at 05:55 local time (the
TZfrom.env), it runsdocker/docker-cookie-refresh/refresh.py - The script performs the challenge handshake and receives a valid session cookie
- The cookie set is written to
app/config/gocomics_auth.json(gitignored) - The PHP comics cron reads
gocomics_auth.jsonand injects the cookies into GoComics requests - If
gocomics_auth.jsonis missing or expired when the PHP cron runs, it waits up to 3 minutes for the refresh service to complete
The refresh only touches cookies (it never writes comic images), and runs 10 minutes before the 06:05 daily cron suggested above. If you schedule the cron at another time, adjust entrypoint.sh to match.
If auto-refresh fails, you can paste fresh cookies manually / how to get cookie for the first time:
- Create a free account at gocomics.com if you don't have one
- Log in to your GoComics account in Chrome
- Install the Get cookies.txt Locally Chrome extension
- While logged in, click the extension → Copy All
- In the admin UI, open the Comics panel → Cookie settings button → paste and save
A free GoComics account is sufficient — no paid subscription needed. Being logged in ensures the full cookie set is present when copying.
Allows you to send commands from HA or Node-Red to your Visionect. Do not use the raw websocket directly for external automation.
Use:
http://HOST:4412/control.php
Supported tasks:
setPagereloadCurrentresumeSchedulepauseunpausereloadPrefsrefreshActivity
Optional token: if you set a Control token in the admin's General settings, every request must send it as an X-Control-Token header or a token= parameter. Leave it empty to allow plain LAN GET requests.
Examples:
http://HOST:4412/control.php?task=setPage&page=arthttp://HOST:4412/control.php?task=setPage&page=newspaperhttp://HOST:4412/control.php?task=reloadCurrenthttp://HOST:4412/control.php?task=resumeSchedule
setPage respects module enablement, so disabled modules are ignored.
The control endpoint is intended for local-network automations such as:
- Home Assistant button actions
- Node-RED flows
- simple LAN-only curl shortcuts
Example Node-RED payload:
{"task":"setPage","page":"newspaper"}Example Node-RED function node:
var task = msg.payload.task;
var page = msg.payload.page || "";
if (!task) {
node.error("Missing payload.task", msg);
return null;
}
msg.method = "GET";
msg.url = "http://YOUR_HOST:4412/control.php?task=" + encodeURIComponent(task);
if (page) {
msg.url += "&page=" + encodeURIComponent(page);
}
return msg;Use that before a standard Node-RED http request node with:
- Method:
use - URL: blank
- Return: parsed JSON
- Auth: none
- schedule slots control which modules are eligible at different times of day, multiple modules can be configured at the same time.
- manual
Go now/setPageactions create a temporary override - manual overrides return to schedule automatically after 30 minutes
- if the system is paused, that 30-minute override timer is effectively frozen until unpaused
- if mirrored frame sleep is enabled, the runtime reports
sleepduring the configured window and live control actions should be considered queued or ineffective until wake time - while asleep, the admin UI treats the next wake time as the real
Next update
- Keep the Visionect Server image pinned to
visionect/visionect-server-v3:7.6.5-arm. (future versions require subscription from Visionect) - The app is built around PHP
7.4in the current Dockerfile. - The repo intentionally includes sample module content so a fresh clone can render something immediately without secrets.
- HTTPS/network fetches should use
curl, not plainfile_get_contents(). - Image output should remain grayscale
.jpgfor the frame. - Wrap Imagick work with
ob_start()/ob_end_clean()to avoid corrupting logs. - Use the shared helpers in
app/lib/:http.php(curl GET/POST that report the HTTP status),image.php(greyscale + frame-size conversion, atomic save) andsecurity.php(visionect_write_json_atomic,visionect_mutate_runtime_statusfor locked read-modify-write). Alwaysrequire_oncethem. - Write files atomically (temp file + rename) so the frame and admin never read a half-written image or JSON file.
- See
app/htdocs/VISIONECT_NOTES.mdfor the full developer guide. - GoComics is served behind BunnyCDN with an Argon2id proof-of-work anti-bot challenge. The
cookie-refreshservice handles this automatically. Without it, GoComics pulls will be blocked. status.phpand runtime helpers should always read config through/app/config, not relative paths from/var/www/html.
/admin/- admin UI
/- public frame shell, point your frame to this URL
/status.php- Home Assistant status integration and display websocket bootstrap helper
/control.php- local automation endpoint
app/cli/visionectd.php- websocket worker and scheduler
app/cli/cron.php- master cron runner, this is the cron that your server will run to get latest updates for modules that need it (newspaper, comics etc)












