135 lines
7.2 KiB
Markdown
135 lines
7.2 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```dotenv
|
||
ROSTER_ALLOWED_HOSTS=roster.example.com
|
||
```
|
||
|
||
Then a minimal Caddy route is:
|
||
|
||
```caddyfile
|
||
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/`](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`:
|
||
|
||
```dotenv
|
||
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/`:
|
||
|
||
```dotenv
|
||
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:
|
||
|
||
```bash
|
||
venv/bin/python server.py
|
||
```
|
||
|
||
Or create a fresh environment:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
venv/bin/python main.py --input input/original.pdf --month "July 2026"
|
||
```
|
||
|
||
## Tests
|
||
|
||
Run the standard-library test suite with:
|
||
|
||
```bash
|
||
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.
|