Docker / build-and-push (push) Successful in 4m53s
Copied from latent/.gitea/workflows/docker.yml unchanged — it derives the image name/registry host from repo context, so it works for postcard as-is. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
3.9 KiB
Markdown
87 lines
3.9 KiB
Markdown
# postcard
|
|
|
|
Turn a phone photo into a sendable postcard. Upload a photo, it gets
|
|
composited onto a vintage postcard template (border, stamp corner,
|
|
handwriting-style caption) entirely in the browser, then either download
|
|
it right away or upload it for a shareable link.
|
|
|
|
The caption is auto-drafted by assembling real fragments of that day
|
|
(git activity, what was playing, a note title) via `server/src/caption.js`
|
|
— never a free-text model call — and stays editable. With no day-context
|
|
source configured it falls back to a small set of generic postcard-voice
|
|
lines.
|
|
|
|
Structurally this is a copy of [`latent`](../latent)'s upload → transform
|
|
→ deliver shape and server (`db.js`/`ids.js`/`cleanup.js`/`ratelimit.js`/
|
|
`routes.js`), with the transform swapped for postcard compositing. See
|
|
[PROPOSAL.md](PROPOSAL.md) for the original design writeup.
|
|
|
|
## Two finishing paths off one editor
|
|
|
|
1. **Instant** — composite, then download directly, entirely client-side.
|
|
Optionally generate a share link (explicit action, not automatic).
|
|
2. **Send** — uploads the composited image to server storage, returns a
|
|
link immediately (no delayed-reveal gate — that's `latent`'s gimmick,
|
|
not this one's).
|
|
|
|
## Running locally
|
|
|
|
```
|
|
npm install --prefix server
|
|
npm start --prefix server
|
|
```
|
|
|
|
Then open http://localhost:3098. `server`'s `start`/`dev` scripts load
|
|
`../.env` automatically via Node's `--env-file-if-exists` — copy
|
|
`.env.example` to `.env` at the repo root and adjust. `DATA_DIR` defaults
|
|
to `./server/data` (gitignored) for the SQLite db and image blobs.
|
|
|
|
## Deploying
|
|
|
|
`docker compose up` using the provided `docker-compose.yml` (copy
|
|
`.env.example` to `.env` and adjust — `IMAGE`/`HOST_PORT` are only read by
|
|
compose, the rest are passed through to the container). CI builds and
|
|
pushes the image on push to `main` via `.gitea/workflows/docker.yml` (needs
|
|
a repo secret `TKNTKN` — a Gitea PAT with `packages:write` scope).
|
|
|
|
## Config
|
|
|
|
All via env vars:
|
|
|
|
- `TTL_HOURS` — link retention (default 72h; shorter than `latent`'s,
|
|
since a postcard link is meant to be opened once, not browsed later).
|
|
- `MAX_UPLOAD_BYTES`, `RATE_LIMIT_PER_HOUR` — basic abuse limits; no
|
|
accounts or auth on any endpoint, the link itself is the credential.
|
|
- `DAY_SUMMARY_URL` — optional, no code-level default (set it in `.env`).
|
|
If set, `GET {url}?date=YYYY-MM-DD` is expected to return
|
|
`{ git?, spotify?, note? }` fragments for that date. `.env` here points
|
|
it at `https://goonk.se/api/day-summary`, goonk's live endpoint
|
|
(confirmed returning `{ git, spotify? }` — no `note` field observed yet).
|
|
A response body containing an `error` key (goonk's shape for a
|
|
missing/malformed date) is treated the same as no summary. Left unset,
|
|
captions fall back to generic lines — no invented personal data.
|
|
- `NOMINATIM_URL` — reverse-geocoding endpoint for the stamp corner's
|
|
location line (only queried when a photo has EXIF GPS). Defaults to the
|
|
public `nominatim.openstreetmap.org`; `server/src/geocode.js` throttles
|
|
to 1 req/sec and caches by rounded coordinate per that API's usage
|
|
policy. No API key needed.
|
|
|
|
## Notable implementation details
|
|
|
|
- EXIF is parsed client-side with no dependency (`exif.js`) — just enough
|
|
to read `DateTimeOriginal` and GPS lat/lon out of a JPEG. Parse
|
|
failures/missing tags fall back to upload date / no location stamp,
|
|
never invented data.
|
|
- The caption's handwriting font is self-hosted (`fonts/caveat-latin.woff2`,
|
|
Caveat, SIL OFL — see `fonts/OFL.txt`), not loaded from
|
|
fonts.googleapis.com at runtime.
|
|
- The upload card offers separate "take a photo" (camera capture) and
|
|
"choose from library" buttons rather than relying on the OS's file
|
|
picker to offer both — some mobile browsers skip the chooser and go
|
|
straight to the camera when `capture` is set on a single input.
|
|
|
|
## Non-goals
|
|
|
|
No accounts, no "your postcards" history, no delayed-reveal mechanic, no
|
|
free-text LLM-generated captions, no collaborative/multi-recipient cards.
|