Add optional nursery roster composition

This commit is contained in:
Media
2026-08-30 20:04:00 -05:00
parent 3eab672c6c
commit 7651a3ed9d
12 changed files with 1578 additions and 116 deletions
+33 -17
View File
@@ -1,6 +1,6 @@
# Roster Formatter
A local browser app that turns the current one-page roster PDF into the established formatted PNG. The web app and command-line tool use the same renderer.
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
@@ -10,7 +10,7 @@ From this folder:
docker compose up -d --build
```
Open <http://127.0.0.1:8787>, upload the roster PDF, choose its month, and select **Format roster**.
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:
@@ -39,12 +39,25 @@ roster.example.com {
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`:
@@ -52,10 +65,11 @@ Change the retention window or browser port with a `.env` file beside `compose.y
```dotenv
ROSTER_RETENTION_MONTHS=6
ROSTER_PORT=8788
ROSTER_MAX_UPLOAD_MB=20
TZ=America/Chicago
```
Set `ROSTER_RETENTION_MONTHS=0` to disable cleanup.
`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/`:
@@ -66,24 +80,24 @@ ROSTER_GID=1001
## Layout guarantees
With the advanced fields untouched, the renderer preserves the original production values:
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.
| Setting | Default |
| --- | ---: |
| Canvas | 792 × 612 PDF points |
| PNG export | 1650 × 1275 pixels at 150 DPI |
| Roster vertical shift | 100 |
| Bottom trim | 60 |
| Logo position | X 50, Y -10 |
| Logo scale | 0.27 |
| Date position | X 460, Y 88 |
| Date font size | 48 |
Factory layout values are:
The web page exposes the layout values in a collapsed **Advanced adjustments** section. Only an intentional field change—or explicitly saving that change as the new default—alters logo/date scale or placement. The canvas size and PNG DPI are not editable. The original production values remain available through **Use original values**.
| 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.
Input is deliberately limited to a one-page, unrotated, 11 × 8.5 inch landscape PDF. This prevents a differently sized upload from silently changing the established geometry.
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
@@ -101,7 +115,7 @@ python3 -m venv .venv
.venv/bin/python server.py
```
The original CLI workflow is still available and now accepts an explicit input path:
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"
@@ -116,3 +130,5 @@ 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.