7.2 KiB
Roster Formatter
A local browser app that turns one or two Subsplash roster PDFs into one formatted PNG. Upload the full roster by itself, or upload it together with an optional nursery-only roster in the same drop zone. The web app and command-line tool share the production renderer.
Start with Docker Compose
From this folder:
docker compose up -d --build
Open http://127.0.0.1:8787, choose or drop one or two roster PDFs together, confirm the month, and select Format roster.
Common commands:
docker compose restart # restart it later
docker compose logs -f app # watch logs
docker compose down # stop it
docker compose up -d --build # rebuild after code changes
The Compose service restarts automatically unless you explicitly stop it. Its direct browser port binds only to 127.0.0.1.
The service also joins the existing external tabernet Docker network, so Caddy can reach it at roster-formatter:8787. For a Caddy hostname, add that hostname to .env:
ROSTER_ALLOWED_HOSTS=roster.example.com
Then a minimal Caddy route is:
roster.example.com {
reverse_proxy roster-formatter:8787
}
The tabernet network must already exist. Because roster images contain personal information, add authentication in Caddy before exposing this service outside your private network.
Upload modes
- One PDF: the formatter uses the byte-compatible established single-roster path. It treats the upload as the main roster and does not require classification.
- Two PDFs: upload one full/main roster and one nursery-only roster in either order. The formatter normalizes their extracted text and identifies them from ministry-role headings. Several broader roles such as Media, Greeters, Ushers, Prayer, or River Kid indicate the main roster; Nursery/Nursery Workers without broader roles indicates the nursery-only roster.
Classification never uses filenames, dates, or weekdays. A Wednesday/Thursday conference export with the normal ministry roles is therefore still a main roster. If the pair does not contain exactly one confidently identified main roster and one nursery-only roster, the app reports the ambiguity instead of guessing.
Both modes retain the existing per-file PDF validation, host allowlist, same-origin POST checks, and browser security headers.
In combined mode, the main roster is placed above a compact nursery section. Their useful table bounds are clipped and composed from the original vector PDF content, then the finished page is rasterized once. The logo and month label are reduced only in combined mode to leave more table space; a one-PDF result keeps its established header and rendering geometry.
Saved files and automatic cleanup
Every successful PNG is returned to the browser with a Download PNG button and saved in output/. The Compose container mounts that folder, so saved PNGs survive rebuilds and restarts.
Layout defaults are persistent too. Change values under Advanced adjustments, then select Save as defaults to prefill them for future rosters. Reset changes restores the currently saved values, while Use original values loads the production geometry without saving it. The server stores these defaults in output/.roster-layout-defaults.json, inside the same mounted volume, and the roster cleanup never removes that settings file.
Existing version-1 defaults files remain supported. Their saved single-roster, logo, and date values are retained, while the new main/nursery fields receive their factory defaults. Saving defaults again writes version 2. You can also adjust an unusual month and format it without saving, leaving the global defaults unchanged.
On startup, every six hours, and after each successful format, the app removes only filenames matching its roster pattern, such as MARCH 2026 Roster.png, when either its roster month or saved-file age is outside the three-calendar-month window. The file-age check prevents an accidentally future-dated filename from bypassing cleanup. Other files are not removed. Cleanup is permanent.
Change the retention window or browser port with a .env file beside compose.yaml:
ROSTER_RETENTION_MONTHS=6
ROSTER_PORT=8788
ROSTER_MAX_UPLOAD_MB=20
TZ=America/Chicago
ROSTER_MAX_UPLOAD_MB is the limit for each PDF; the request limit automatically allows two files plus multipart overhead. Set ROSTER_RETENTION_MONTHS=0 to disable cleanup.
The image runs as UID/GID 1000:1000, matching this machine. On a host with a different checkout owner, set ROSTER_UID and ROSTER_GID in .env, then rebuild so the container can write to output/:
ROSTER_UID=1001
ROSTER_GID=1001
Layout guarantees
The canvas is always 792 × 612 PDF points (11 × 8.5 inches, landscape), and every PNG is always 1650 × 1275 pixels at 150 DPI. Neither value is editable.
Factory layout values are:
| Mode/section | Setting | Default |
|---|---|---|
| One PDF | Vertical shift | 100 |
| One PDF | Bottom trim | 60 |
| Header | Logo position / scale | X 50, Y -10 / 0.27 |
| Header | Month position / font size | X 460, Y 88 / 48 |
| Combined main | Position / scale | X 66, Y 120 / 0.86 |
| Combined nursery | Position / scale | X 71, Y 519 / 0.85 |
The collapsed Advanced adjustments section provides independent X, Y, and scale controls for the main and nursery sections, so changing one does not move or resize the other. It also retains the single-PDF shift/trim and shared logo/month controls. Combined mode automatically renders the logo at 76% of its selected scale and the month text at five-sixths of its selected font size; those reductions are not applied in one-PDF mode. Use original values restores all factory values shown above.
The page theme defaults to System and follows the browser/operating-system color preference. The System / Light / Dark control remembers the choice in that browser. Theme changes affect only the webpage and never the generated PNG.
Every input is independently required to be a non-empty, genuine, one-page, unrotated 11 × 8.5 inch landscape PDF. This prevents either upload from silently changing the established geometry.
Run without Docker
The existing venv already contains the formatter dependencies. Start the web app with:
venv/bin/python server.py
Or create a fresh environment:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python server.py
The original single-roster CLI workflow remains available and accepts an explicit input path:
venv/bin/python main.py --input input/original.pdf --month "July 2026"
Tests
Run the standard-library test suite with:
PYTHONDONTWRITEBYTECODE=1 venv/bin/python -m unittest discover -v
Uploads and intermediate PDFs are created in isolated temporary directories and removed immediately after formatting. Only the final PNG is retained in output/.
The legacy visual regression uses the real input/original.pdf. Combined-layout tests currently use a representative synthetic nursery-only export because the repository does not yet contain a real nursery-only Subsplash fixture; the factory combined positions may benefit from final tuning when the first real export is available.