In production nginx sits in front: serves `public/` directly, proxies `/upload` and `/manage` to localhost, htpasswd on both. Note `/health` is **not** proxied — nginx only forwards `location /upload` and `location /manage`, so anything outside the box can only reach those two.
### Managing photos
`manage.go` holds the `/manage` handlers, kept apart from the upload code in `server.go`. They are only registered when the binary runs with `-manage` (NixOS `services.photogallery.manage.enable`, which also adds the nginx location); without it the paths 404 and there is no edit or delete code path at all. The pages are rendered on request from the sidecars (not generated into `public/`), and nothing on the public site links to them. Constraints worth knowing before touching it:
- An edit only rewrites the `.toml` sidecar. Any key other than `caption` and `date` in a hand-written sidecar is dropped, same as on upload.
- Slugs from the URL are matched against the photos actually in `content/`, never joined into a path.
-`sameOrigin` compares the `Origin` header against `-base-url`, because behind nginx the `Host` header is the upstream address. A wrong `-base-url` therefore breaks saving in browsers that send no `Sec-Fetch-Site`.
- Delete is permanent and takes two steps: `GET /manage/delete/{slug}` only shows a confirmation page, the `POST` does the work. `Generator.Delete` removes the image and sidecar from `content/` first, then the four renditions and `public/photo/<slug>/`, all under the build mutex. There is no trash folder — recovery means restoring from a backup.
- Listing pages are put right by the rebuild that follows, which also prunes what a delete or a date change leaves behind: `renderDayPages` drops `public/day/<date>/` for days without photos, and `prunePages` drops pager pages past the last one. Renditions of photos removed from `content/` by hand (not via `/manage`) are still **not** pruned.
- **Images** — four renditions per photo: original copy, 600×600 centre-cropped square thumbnail, a 1600px-longest-side "medium" for the detail page, and a 1200px "og" for link previews. Catmull-Rom resize. Outputs are skipped when newer than the source.
- Two views of the same photos, both 3-column square grid → 2-column on mobile, both with infinite-scroll load-more. Page size is configurable via `-page-size` (default 60; NixOS `services.photogallery.pageSize`):
- **Timeline is the site root by default** (`/`, `/page/2/`, …). Photos grouped by calendar day: one ISO date heading (`2026-06-14`) + count per day, then the square grid. `groupByDay` collapses the newest-first list into consecutive same-day runs; `chunkDayGroups` packs whole days into pages targeting `pageSize`, never splitting a day across a page boundary (so appended pages never duplicate a heading — the load-more JS relies on this).
- Each timeline date heading links to a **single-day page** (`/day/<YYYY-MM-DD>/`) holding just that day's photos. `renderDayPages` writes one per `groupByDay` run; the ISO label doubles as the URL slug.
- Which view owns the site root is set by `-default-view timeline|grid` (NixOS `services.photogallery.defaultView`). The other view moves under `/<name>/`: with `-default-view grid` the grid is `/` and the timeline is `/timeline/`. `viewBase` and `pageURL` in `generator.go` derive every page path and pager link from that, and the templates take the cross-links as `GridURL` / `TimelineURL` — never hardcode `/grid/` or `/`. On each build the directory named after the default view is removed, since it can only be a leftover from before a switch.
Kotlin + Jetpack Compose uploader: pick or share a photo, add caption and date, post. Credentials (HTTP Basic) are sealed with an Android Keystore AES-GCM key. See `android/README.md` for the detail.
Its own flake, deliberately separate from the root one so NixOS consumers of `nixosModules.default` don't pull the Android SDK into their lock:
The server needs no changes to serve it — every gap (slug collisions, EXIF, HEIC, size caps) is closed client-side. Release builds need `android/keystore.properties`; without it the signing config is skipped and only debug builds work.
| `android/flake.nix` | Android SDK dev shell (separate from the root flake) |
## Gotchas
-`.gitignore` patterns here must be anchored. An unanchored `photogallery` also matches the Kotlin package directory `android/app/src/main/java/ws/inflo/photogallery/` and silently swallows every source file in it.