diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bc9aef2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# PostConvert + +Image/PDF-to-JPEG conversion microservice. Single-file Node.js/Express app (`server.js`). + +## Tech Stack + +- Node.js 18+ (ES modules), Express +- Sharp (image processing), libheif-js (HEIC/HEIF via WASM) +- pdftoppm from Poppler (PDF rendering), archiver (ZIP output) +- Docker (node:20-bookworm-slim), deployed on Fly.io (sjc region) + +## Endpoints + +- `GET /health` — health check +- `POST /convert` — image or first PDF page → JPEG +- `POST /convert/pdf` — all PDF pages → ZIP of JPEGs + +## Auth + +Bearer token via `CONVERTER_TOKEN` env var on all endpoints. + +## Key Design + +- Single-flight concurrency (1 conversion at a time, 429 if busy) +- Resize/quality via headers: `x-jpeg-quality`, `x-max-dimension`, `x-pdf-dpi`, `x-without-enlargement` +- Pixel limit 200M, bounded /tmp, no stack traces leaked, request timeouts +- Fly.io auto-scaling 0–10 machines, 2 concurrent connection hard limit + +## Environment Variables + +- `PORT` (default 8080) +- `CONVERTER_TOKEN` (required) +- `REQ_TIMEOUT_MS` (default 120s, range 5–600s) +- `REQ_TIMEOUT_PDF_MS` (default 5m, range 10s–30m) + +## Development + +```sh +npm install +node server.js +``` + +Docker: +```sh +docker build -t postconvert . +docker run -e CONVERTER_TOKEN=secret -p 8080:8080 postconvert +``` + +## Fly.io Deployment + +Tokens are in `.env` (gitignored). Load with `source .env` or use `dotenv`. + +Deploy: `FLY_API_TOKEN="$FLY_API_TOKEN" fly deploy` diff --git a/server.js b/server.js index bb8b990..bae867f 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,6 @@ const libheif = libheifModule?.default ?? libheifModule; const app = express(); app.use(express.raw({ type: "*/*", limit: "30mb" })); -app.get("/", (_req, res) => res.status(200).send("postconvert: ok")); app.get("/health", (_req, res) => res.status(200).send("ok")); /* ------------------------------------------------------------------ */ @@ -275,6 +274,167 @@ async function withConvertSingleFlight(req, res, fn) { /* Routes */ /* ------------------------------------------------------------------ */ +/* ------------------------------------------------------------------ */ +/* Web UI */ +/* ------------------------------------------------------------------ */ + +app.get("/", (_req, res) => { + res.setHeader("Content-Type", "text/html"); + res.send(` + +
+ + +Drop a photo here or tap to choose
+ +