Files
photog/CLAUDE.md
T

156 lines
7.0 KiB
Markdown
Raw Normal View History

2026-07-02 00:53:17 +02:00
# photogallery
Personal static photo gallery with Atom feed and upload API. Single-user.
No database, no framework — just a Go binary that watches a directory and regenerates a static site.
An Android app in `android/` posts to the same upload API.
2026-07-02 00:53:17 +02:00
## Architecture
```
content/ # images + .toml sidecars (source of truth)
public/ # generated output (served by nginx / --serve flag)
templates/ # embedded HTML templates
static/ # embedded CSS + JS
android/ # Kotlin/Compose uploader app, own flake
2026-07-02 00:53:17 +02:00
```
The binary does three things at once:
- Watches `content/` via fsnotify and rebuilds on any change
- Serves a multipart upload API at `POST /upload`
- In dev mode (`--serve`), also serves `public/` as static files
Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /health`.
In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint. Note `/health` is **not** proxied — nginx only forwards `location /upload`, so anything outside the box can only reach `/upload`.
2026-07-02 00:53:17 +02:00
## Stack
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`
- **Templates** — `html/template`, embedded via `embed.FS`
- **Images** — three renditions per photo: original copy, 600×600 centre-cropped square thumbnail, and a 1600px-longest-side "medium" for the detail page. Catmull-Rom resize. Outputs are skipped when newer than the source.
2026-07-02 00:53:17 +02:00
- **Feed** — Atom (`encoding/xml`)
- **NixOS** — `flake.nix` + `module.nix`, exports `nixosModules.default`
## Sidecar format
```toml
caption = "Tempelhof at sunrise"
date = "2026-06-14"
```
Missing sidecar → filename as caption, today as date. Photos sort newest first by that date.
2026-07-02 00:53:17 +02:00
## Design
- Gruvbox dark hard colour scheme
- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url`
- Manjari for Malayalam text (bio line), via `-ml-family`
- 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.
- Single photo page with sidebar and newer/older navigation
### Photo page navigation
`static/photo-nav.js` handles arrow keys and touch swipe. Both read their
destinations from the `[data-nav="newer"]` / `[data-nav="older"]` anchors the
template renders, so a photo with no sibling in that direction has nothing to
find and the gesture rubber-bands. If the script fails to load the links still
work.
Swipe left → older, right → newer, matching ArrowRight/ArrowLeft. Constraints
worth knowing before touching it:
- Swipes starting within 24px of a screen edge are ignored — that strip is the browser's own back-gesture.
- Only `pointerType === "touch"`, so desktop drag-to-save is untouched.
- Axis locks once after 10px so vertical drags stay scrolls; `touch-action: pan-y` tells the compositor the same.
- The image is wrapped in an `<a>` to the full-size original, so the click trailing a swipe is suppressed or it opens a new tab.
- `EXIT_MS` in the JS must stay in sync with the transition duration in `style.css`.
Anything added under `static/` is embedded and copied to `public/static/`
automatically — `assets.go` embeds the whole directory and `copyStatic()` walks
every entry, so new CSS/JS needs no generator change.
2026-07-02 00:53:17 +02:00
## Running locally
```bash
nix develop
go mod tidy
go run . --serve
# gallery: http://localhost:8080
# upload: http://localhost:8080/upload
```
## Android app
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:
```bash
cd android
nix develop
./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk
```
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.
2026-07-02 00:53:17 +02:00
## NixOS module usage
```nix
inputs.photogallery.url = "github:youruser/photogallery";
# in modules:
inputs.photogallery.nixosModules.default
# in configuration.nix:
services.photogallery = {
enable = true;
baseURL = "https://photos.yourdomain.tld";
nginx = {
enable = true;
domain = "photos.yourdomain.tld";
htpasswdFile = "/etc/nginx/.htpasswd-gallery";
};
};
```
## Bootstrap (first nix build)
```bash
go mod tidy # generates go.sum — commit it
nix build # fails, prints correct vendorHash
# paste hash into flake.nix
nix build # succeeds
```
## Key files
| File | Purpose |
|---|---|
| `main.go` | Flags, wires generator + watcher + server |
| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs/mediums |
2026-07-02 00:53:17 +02:00
| `server.go` | Upload API + optional static file serving |
| `watcher.go` | fsnotify watcher with 500ms debounce |
| `assets.go` | `//go:embed` for templates and static |
| `templates/timeline.html` | Site root: photos grouped by day, paginated |
| `templates/grid.html` | Flat square grid at `/grid/`, paginated |
| `templates/day.html` | Single-day page at `/day/<date>/` |
| `templates/photo.html` | Single photo page, neighbour links and prefetch |
| `static/photo-nav.js` | Arrow-key and swipe navigation |
2026-07-02 00:53:17 +02:00
| `module.nix` | NixOS module (systemd service, tmpfiles, nginx vhost) |
| `flake.nix` | `buildGoModule` package + devShell + nixosModules |
| `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.
2026-07-02 00:53:17 +02:00
## What's not done yet
- ActivityPub federation (planned later — start with RSS/Atom)
- Multi-image upload
- Delete/edit via UI
- Any auth beyond nginx htpasswd
- The Android app has never been run on real hardware
- Photo-page swipe has never been tested on a real touchscreen; the iOS Safari edge-gesture interaction is the untested part