# Gallery

Static photo gallery for logging meals and food memories.

The site is based on the HTML5 UP Lens template. The front-end remains a mostly static HTML/CSS/JavaScript site, and the local Node server now also exposes a tiny rankings sync API for shared Elo persistence.

## Repo Layout

- `templates/index.html`: source template for the main gallery page
- `templates/rankings.html`: source template for the rankings page
- `index.html`: generated static gallery page
- `rankings.html`: generated static rankings page
- `assets/`: site CSS, JavaScript, fonts, and audio
- `images/fulls/`: full-size gallery images
- `images/thumbs/`: gallery thumbnails
- `data/meals.json`: source of truth for gallery entries
- `data/elo.json`: Elo ratings, record totals, and ranking settings
- `scripts/build.js`: renders static pages from templates and data
- `scripts/check.js`: validates data, image assets, and generated pages
- `scripts/generate-thumbnails.js`: regenerates thumbnails from the full-size images
- `scripts/ingest-meal.js`: ingests a new meal image and metadata in one command
- `scripts/serve.js`: serves the generated site and the rankings sync API
- `scripts/lib/elo.js`: validates and syncs Elo data against the meal list
- `scripts/lib/rankings-state.js`: normalizes and persists the shared rankings state
- `package.json`: minimal Node build entrypoint

## Run Locally

Install dependencies:

```sh
npm install
```

Build the site and validate the generated output:

```sh
npm run build
```

Serve it locally:

```sh
npm run serve
```

Then open `http://127.0.0.1:4321`.

By default, rankings sync state is written to `.runtime/rankings-state.json`.
Override that path with `RANKINGS_STATE_PATH=/absolute/path/to/rankings-state.json`.

If you want a single command that builds and serves, run:

```sh
npm start
```

To validate the repo state without rebuilding thumbnails or pages, run:

```sh
npm run check
```

## Content Workflow

Gallery entries live in `data/meals.json`, and the build generates both `index.html` and `rankings.html` from the template and data files.

After editing content or templates, rebuild the site with:

```sh
npm run build
```

The gallery build keeps the existing Lens thumbnail markup intact, so the current client-side viewer code continues to work.

To ingest a new meal image and update the site in one command, run:

```sh
npm run ingest -- --image /path/to/photo.jpg --title "meal title" --description "notes"
```

Optional ingestion flags:

- `--position "left center"` sets the viewer image alignment
- `--focus-x 0.35 --focus-y 0.45` sets the thumbnail crop focal point

If you only need to regenerate thumbnails, run:

```sh
npm run build:thumbs
```

To force a full thumbnail rebuild, run:

```sh
npm run build:thumbs:force
```

## Rankings Data

`data/elo.json` stores the seed rating, Elo `kFactor`, and a win-loss record for each meal.
The page build keeps this file aligned with `data/meals.json`, so new meals automatically appear in `rankings.html` with the default seed rating.

The interactive voting flow on `rankings.html` now prefers the same-origin API exposed by `scripts/serve.js`:

- `GET /api/rankings`: load the shared rankings state
- `POST /api/rankings/vote`: apply one head-to-head result on the server
- `POST /api/rankings/reset`: reset the shared board back to the seeded state

The server persists the shared board to `.runtime/rankings-state.json` by default, or to `RANKINGS_STATE_PATH` if you set it.
That makes rankings persist across reloads, sessions, browsers, and devices as long as they are hitting the same deployed site.

If the API is unavailable, the page falls back to browser `localStorage`.
In that fallback mode, votes still persist across reloads in the same browser profile, but they do not sync across browsers or devices.
Use the reset button on the rankings page if you want to clear the current saved board and go back to the seeded state.

## Deployment Notes

For Docker/VPS deployment, mount a persistent volume and point `RANKINGS_STATE_PATH` at it so rankings survive container rebuilds and restarts.

Example:

```sh
RANKINGS_STATE_PATH=/data/rankings-state.json npm start
```

In a containerized setup, mount `/data` as a named volume or bind mount.
If you reverse-proxy the app through Caddy on the same domain, the rankings page will use the shared API automatically with no extra CORS setup.

The current server deployment lives one directory up from this repo in `~/docker/websites/docker-compose.yml` and uses this `gallery` service definition:

```yaml
services:
  gallery:
    build:
      context: ./gallery-src
    container_name: gallery
    environment:
      HOST: 0.0.0.0
      PORT: 80
      RANKINGS_STATE_PATH: /data/rankings-state.json
    volumes:
      - gallery-rankings:/data
    restart: unless-stopped
    networks:
      - web

networks:
  web:
    external: true
    name: web

volumes:
  gallery-rankings:
```

## Image Conventions

- Full-size images and thumbnails share the same numeric ID
- Full-size images live at `images/fulls/<id>.jpg`
- Thumbnails live at `images/thumbs/<id>.jpg`
- `position` controls the full-screen viewer image alignment
- `thumbnail.focus` optionally overrides the default center crop for generated thumbnails

## Thumbnail Focus

Thumbnails are generated from `images/fulls` with `sharp` at `240x320`.
The generator auto-rotates images using EXIF orientation, skips unchanged files by default, and removes stale thumbnail `.jpg` files that no longer map to a meal entry.

For images that should crop away from the center, add optional thumbnail focus metadata to the meal entry:

```json
{
  "id": "34",
  "title": "example",
  "description": "example",
  "thumbnail": {
    "focus": {
      "x": 0.35,
      "y": 0.45
    }
  }
}
```

The `x` and `y` values are normalized from `0` to `1`, where `0.5, 0.5` is the center of the image.
