-`POST /api/restore` — move a trashed item (`id`) back to `content/`, stripping the timestamp. Refuses with `409` if a live post already holds that slug.
-`POST /api/trash/empty` — **hard delete** trashed items: specific `id`s (repeated), or everything with `all=1`. The only non-reversible delete in the system.
The manage page has a Trash panel (header toggle) for restore / delete-forever /
empty. `trash.go` handles these; filenames parse as `<slug>.<stamp><ext>` (slug
is dot-free and the stamp is a fixed `20060102-150405`, so parts peel off
unambiguously), and `id`s are validated the same way to block traversal.
Each non-trash mutation triggers the same async rebuild as upload. Sidecars are written
atomically (temp + rename) so a concurrent build never sees a half-written
file. Slugs are validated (`resolveSlug`: bare filename only, must name an
existing content image) which closes path traversal and dotfile access.
Every state-changing POST (`edit`, `delete`, `tags`, `restore`, `trash/empty`)
first passes `sameOrigin` (`server.go`): because Basic-auth credentials are
replayed by the browser on every request to the host, a cross-site page could
otherwise drive a logged-in user into a delete (CSRF). The check honours
`Sec-Fetch-Site`, falls back to comparing `Origin` against `-base-url`/`Host`,
and allows header-less non-browser clients (curl). Cross-site → `403`. (Ported
from the independent `/manage` on `plex/trunk-ng`.)
**Delete relies on `Build`'s prune step** (`generator.go`): a rebuild drops the
derived HTML trees (`photo/ day/ album/ page/ grid/page/`) and regenerates them,
and `pruneRenditions` removes `images|thumbs|medium|og` files with no backing
photo. Without this a deleted post would linger at its old URL and in stale
day/album pages. This also cleans up after a sidecar removed by hand.
- **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** (`/`, `/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).
- **Grid** (`/grid/`, `/grid/page/2/`, …) is the flat square grid, linked from the profile header.
- 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.
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.