From 8bdc5c04cac5a07b57fc4e4b28854ef6bc4f92a6 Mon Sep 17 00:00:00 2001 From: mifi Date: Sat, 7 Feb 2026 11:07:04 -0300 Subject: [PATCH] Updates --- .woodpecker/ci.yml | 14 ++--- .woodpecker/deploy.yml | 84 ++++++++++---------------- AGENTS.md | 90 ++++++++++++++++++++++++++++ README.md | 42 ++++++++----- docker-compose.portainer.yml | 112 +++++++++++++++++++++++++++++++++++ package.json | 3 +- 6 files changed, 270 insertions(+), 75 deletions(-) create mode 100644 AGENTS.md create mode 100644 docker-compose.portainer.yml diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml index 90408c5..de76665 100644 --- a/.woodpecker/ci.yml +++ b/.woodpecker/ci.yml @@ -38,13 +38,13 @@ steps: depends_on: - lint - # build: - # image: node:22-bookworm-slim - # commands: - # - corepack enable && corepack prepare pnpm@latest --activate - # - pnpm run build - # depends_on: - # - test + build: + image: node:22-bookworm-slim + commands: + - corepack enable && corepack prepare pnpm@latest --activate + - pnpm run build + depends_on: + - test # build-full: # image: node:22-bookworm-slim diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml index 72b028c..09b629a 100644 --- a/.woodpecker/deploy.yml +++ b/.woodpecker/deploy.yml @@ -1,5 +1,5 @@ -# Deploy: build image, push to registry, trigger Portainer stack redeploy. -# Runs on push/tag/manual to main only, after ci workflow succeeds. +# Deploy: build qr-api and qr-web (multi-arch amd64 + arm64), push to registry, trigger Portainer stack redeploy. +# Runs on push/tag/manual to main only, after CI workflow succeeds. when: - branch: main event: [push, tag, manual] @@ -10,47 +10,15 @@ depends_on: - ci steps: - - name: Docker image build + - name: Docker image build (qr-api + qr-web, multi-arch) image: docker:latest environment: - REGISTRY_REPO: git.mifi.dev/mifi-holdings/shorty - DOCKER_API_VERSION: '1.43' - DOCKER_BUILDKIT: '1' - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - - set -e - - echo "=== Building Docker image (BuildKit) ===" - - 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"' - - 'echo "Registry repo: $REGISTRY_REPO"' - - | - build() { - docker build \ - --progress=plain \ - --tag $REGISTRY_REPO:${CI_COMMIT_SHA} \ - --tag $REGISTRY_REPO:latest \ - --label "git.commit=${CI_COMMIT_SHA}" \ - --label "git.branch=${CI_COMMIT_BRANCH}" \ - . - } - for attempt in 1 2 3; do - echo "Build attempt $attempt/3" - if build; then - echo "✓ Docker image built successfully" - exit 0 - fi - echo "Build attempt $attempt failed, retrying in 30s..." - sleep 30 - done - echo "All build attempts failed" - exit 1 - - - name: Push to registry - image: docker:latest - environment: - DOCKER_API_VERSION: '1.43' + DOCKER_API_VERSION: "1.43" + DOCKER_BUILDKIT: "1" + BUILDKIT_PROGRESS: "plain" REGISTRY_URL: git.mifi.dev - REGISTRY_REPO: git.mifi.dev/mifi-holdings/shorty + REGISTRY_REPO_API: git.mifi.dev/mifi-holdings/shorty-qr-api + REGISTRY_REPO_WEB: git.mifi.dev/mifi-holdings/shorty-qr-web REGISTRY_USERNAME: from_secret: gitea_registry_username REGISTRY_PASSWORD: @@ -59,18 +27,30 @@ steps: - /var/run/docker.sock:/var/run/docker.sock commands: - set -e - - echo "=== Pushing to registry ===" - - 'echo "Registry: $REGISTRY_URL"' - - 'echo "Repository: $REGISTRY_REPO"' + - echo "=== Multi-arch Docker build (amd64 + arm64) ===" + - 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"' - | - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ - -u "$REGISTRY_USERNAME" \ - --password-stdin - - docker push $REGISTRY_REPO:${CI_COMMIT_SHA} - - docker push $REGISTRY_REPO:latest - - echo "✓ Images pushed successfully" - depends_on: - - Docker image build + apk add --no-cache git + docker buildx version || true + docker buildx create --name shorty-builder --use --driver docker-container 2>/dev/null || docker buildx use shorty-builder + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USERNAME" --password-stdin + - | + build_push() { + local ctx=$1 + local repo=$2 + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --progress=plain \ + --tag $repo:${CI_COMMIT_SHA} \ + --tag $repo:latest \ + --label "git.commit=${CI_COMMIT_SHA}" \ + --label "git.branch=${CI_COMMIT_BRANCH}" \ + --push \ + "$ctx" + } + build_push ./qr-api $REGISTRY_REPO_API + build_push ./qr-web $REGISTRY_REPO_WEB + echo "✓ Images built and pushed (multi-arch)" - name: Trigger Portainer stack redeploy image: curlimages/curl:latest @@ -90,4 +70,4 @@ steps: fi echo "✓ Portainer redeploy triggered (HTTP $code)" depends_on: - - Push to registry + - Docker image build (qr-api + qr-web, multi-arch) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2af8675 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# Shorty — Agent-oriented project guide + +This file describes the repository layout, conventions, and workflows so LLM agents and humans can work in it accurately and efficiently. + +## What this repo is + +- **Shorty** is a self-hosted stack: **Kutt** (URL shortener) + **QR Designer** (qr-api + qr-web). +- **Kutt**: short links at `mifi.me`, admin at `link.mifi.me` (Postgres + Redis). +- **QR Designer**: Next.js app at `qr.mifi.dev` (BasicAuth); backend **qr-api** (Express, SQLite, uploads, shorten proxy to Kutt). qr-api is internal-only (backend network). + +## Repo layout (monorepo, pnpm workspace) + +``` +shorty/ +├── package.json # Root: scripts lint, format, format:check, test, build (all delegate to workspaces) +├── pnpm-workspace.yaml # packages: qr-api, qr-web +├── docker-compose.yml # Full stack with build (Kutt + qr-api + qr-web from Dockerfiles) +├── docker-compose.portainer.yml # Same stack using registry images; for Portainer + webhook redeploy +├── .woodpecker/ +│ ├── ci.yml # CI: install → format → lint → test → build +│ └── deploy.yml # Deploy: buildx qr-api + qr-web (multi-arch), push, Portainer webhook +├── qr-api/ # Express API (TS), SQLite, multer uploads, Kutt proxy +│ ├── Dockerfile +│ ├── .dockerignore +│ ├── src/ +│ └── package.json +└── qr-web/ # Next.js 15 (App Router), Mantine, qr-code-styling + ├── Dockerfile + ├── .dockerignore + ├── next.config.ts # output: 'standalone' + ├── src/ + └── package.json +``` + +- **No root Dockerfile.** Images are built from `qr-api/` and `qr-web/` only. +- **Lockfile:** `pnpm-lock.yaml` is committed; CI uses `pnpm install --frozen-lockfile`. + +## Key scripts (from repo root) + +| Command | Effect | +|-------------------|--------| +| `pnpm install` | Install deps for all workspaces | +| `pnpm run lint` | ESLint in qr-api and qr-web | +| `pnpm run format:check` | Prettier check (no write) | +| `pnpm run format` | Prettier write | +| `pnpm run test` | Vitest in qr-api and qr-web | +| `pnpm run build` | Build qr-api (tsc) and qr-web (next build) | + +Per-package: `pnpm --filter qr-api dev`, `pnpm --filter qr-web dev` (dev servers). + +## Environment and config + +- **Root:** `.env.example` documents vars; copy to `.env` for Docker Compose. `.env` is gitignored. +- **Devcontainer:** `.devcontainer/devcontainer.json` sets `DB_PATH`, `UPLOADS_PATH`, `KUTT_API_KEY`, `QR_API_URL` for in-container dev; no `.env` required for qr-api/qr-web dev. +- **Compose:** Kutt needs `DB_PASSWORD`, `JWT_SECRET`; qr-api optionally `KUTT_API_KEY`. Portainer stack can set `REGISTRY`, `IMAGE_TAG` for registry-based compose. + +## CI/CD (Woodpecker) + +- **CI** (runs on PR, push to main, tag, manual): Node 22, pnpm, then `format:check` → `lint` → `test` → `build`. No Docker in CI. +- **Deploy** (runs on main push/tag/manual after CI): Uses `docker buildx` to build **two** images from `./qr-api` and `./qr-web` with `--platform linux/amd64,linux/arm64`, tags `:latest` and `:${CI_COMMIT_SHA}`, pushes to `git.mifi.dev/mifi-holdings/shorty-qr-api` and `shorty-qr-web`, then POSTs `portainer_webhook_url` to trigger Portainer stack redeploy. +- **Secrets:** `gitea_registry_username`, `gitea_package_token`, `portainer_webhook_url`. + +## Portainer (production) + +- **Registry-based stack:** Use `docker-compose.portainer.yml`. Images: `${REGISTRY}/mifi-holdings/shorty-qr-api:${IMAGE_TAG}`, same for `shorty-qr-web`. Add stack webhook in Portainer and set that URL as `portainer_webhook_url` in Woodpecker. +- **Build-from-source:** Use `docker-compose.yml`; no registry, no webhook. + +## Code style and tooling + +- **TypeScript** only (qr-api and qr-web). +- **Prettier:** 4 spaces, single quotes, trailing comma all, semicolons (see `.prettierrc`). Check with `pnpm run format:check`. +- **ESLint:** Root `.eslintrc.cjs` for qr-api; qr-web has its own `.eslintrc.cjs` (Next + Prettier). No TSLint. +- **Ignores:** `.gitignore` (e.g. node_modules, .pnpm-store, .next, dist, .env, *.tsbuildinfo); `.prettierignore` and ESLint `ignorePatterns` aligned (coverage, build dirs, .pnpm-store). Each app has `.dockerignore` to keep build context small. + +## Where to change what + +- **API routes (qr-api):** `qr-api/src/routes/`, `qr-api/src/index.ts`. +- **Web app (qr-web):** `qr-web/src/app/` (App Router), `qr-web/src/components/`, `qr-web/src/contexts/`. +- **Shared types:** Defined in each package; no shared package. +- **Compose:** Kutt + qr services in `docker-compose.yml`; registry-only variant in `docker-compose.portainer.yml`. +- **Pipelines:** `.woodpecker/ci.yml`, `.woodpecker/deploy.yml`. Image names and registry are in `deploy.yml` env. + +## Testing and building locally + +- **Unit tests:** `pnpm run test` (Vitest in both packages). +- **Dev:** `pnpm --filter qr-api dev` and `pnpm --filter qr-web dev` (ports 8080 and 3000); devcontainer forwards them. +- **Full stack (Docker):** `docker compose up -d` (uses `docker-compose.yml`; needs `.env` with `DB_PASSWORD`, `JWT_SECRET`). +- **Multi-arch build (local):** Use `docker buildx build --platform linux/amd64,linux/arm64 -t : --push ./qr-api` (and same for `./qr-web`) after logging into the registry. + +This layout and these scripts are the source of truth for automation and agent-driven edits. diff --git a/README.md b/README.md index f8f9005..1794bbe 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,21 @@ Designed for Docker/Portainer with Traefik. Uses **pnpm** everywhere; no Tailwin ## Deploy (Portainer) -1. In Portainer: **Stacks → Add stack**. -2. Use the repo root `docker-compose.yml` (clone repo or paste content). -3. Set required env vars (at least): - - `DB_PASSWORD` — Postgres password for Kutt - - `JWT_SECRET` — Kutt JWT secret (generate a random string) - - `KUTT_API_KEY` — Kutt API key for qr-api (after creating it in Kutt UI) -4. Deploy. No ports are exposed; Traefik handles ingress. +**Option A — Registry + webhook (recommended for CI/CD)** +Use prebuilt images and redeploy on push via webhook: + +1. In Portainer: **Stacks → Add stack**. Use **docker-compose.portainer.yml** (paste or pull from repo). +2. Set env vars: + - **Required:** `DB_PASSWORD`, `JWT_SECRET` + - **Optional:** `REGISTRY` (default `git.mifi.dev`), `IMAGE_TAG` (default `latest`), `KUTT_API_KEY` +3. Deploy. Then in the stack: **Webhooks** → add webhook. Copy the URL and add it as secret `portainer_webhook_url` in Woodpecker (repo secrets). On each push to `main`, the pipeline builds multi-arch images, pushes to the registry, and triggers this webhook to redeploy the stack. + +**Option B — Build from source** +For one-off or local deploys without CI: + +1. In Portainer: **Stacks → Add stack**. Use **docker-compose.yml** (builds qr-api and qr-web from Dockerfiles). +2. Set required env vars: `DB_PASSWORD`, `JWT_SECRET`, and optionally `KUTT_API_KEY`. +3. Deploy. No ports are exposed; Traefik handles ingress. ## Env vars and .env.example @@ -71,10 +79,12 @@ Ports 3000 and 8080 are forwarded by the devcontainer. ## Repo structure -- `docker-compose.yml` — Root compose for Portainer (Kutt + qr-api + qr-web, Traefik labels). +- `docker-compose.yml` — Compose that builds qr-api and qr-web from source (local or one-off Portainer). +- `docker-compose.portainer.yml` — Compose that uses registry images; for Portainer + CI/CD webhook redeploys. - `qr-api/` — Node/TS Express API: SQLite projects, uploads, shorten proxy to Kutt. Not exposed via Traefik. - `qr-web/` — Next.js (App Router) + Mantine QR designer; proxies all API calls to qr-api server-side. -- `.woodpecker.yml` — CI: lint-and-test on PR/push to main; manual deploy with `depends_on` lint-and-test. +- `.woodpecker/ci.yml` — CI: install, format check, lint, test, build on PR/push/tag. +- `.woodpecker/deploy.yml` — Deploy: build qr-api and qr-web (multi-arch amd64/arm64), push to registry, trigger Portainer webhook. Runs on push/tag to `main` after CI. - `.devcontainer/` — Devcontainer for local dev. ## Security @@ -87,18 +97,20 @@ Ports 3000 and 8080 are forwarded by the devcontainer. The QR designer uses **qr-code-styling** for dots, corners, colors, and error correction. The optional **qr-border-plugin** (from [lefe.dev marketplace](https://lefe.dev/marketplace/qr-border-plugin)) adds border styling but depends on `@lefe-dev/lefe-verify-license`, which may involve licensing/watermark behavior. This stack uses qr-code-styling only by default; you can add qr-border-plugin from npm or GitHub if desired and document any license terms. -## Switching to prebuilt images (CI/CD) +## CI/CD (Woodpecker) -In `.woodpecker.yml`, the deploy pipeline has placeholder steps. To use prebuilt images: +- **CI** (`.woodpecker/ci.yml`): on every PR/push to `main`/tag — `pnpm install --frozen-lockfile`, format check, lint, test, build. Requires `pnpm-lock.yaml` committed. +- **Deploy** (`.woodpecker/deploy.yml`): on push/tag to `main` after CI — builds `shorty-qr-api` and `shorty-qr-web` as multi-arch (linux/amd64, linux/arm64), pushes to `git.mifi.dev/mifi-holdings/shorty-qr-api` and `shorty-qr-web`, then POSTs the Portainer webhook to redeploy the stack. -1. Build `qr-api` and `qr-web` in CI (e.g. `docker build -t $REGISTRY/shorty/qr-api:$CI_COMMIT_SHA ./qr-api`). -2. Push to your registry; set `REGISTRY` and `IMAGE_PREFIX` (or equivalent) as secrets. -3. In `docker-compose.yml`, replace `build: context: ./qr-api` with `image: $REGISTRY/shorty/qr-api:$TAG` (use env or a compose override for TAG). +**Secrets** (repo or org): `gitea_registry_username`, `gitea_package_token`, `portainer_webhook_url`. + +**Local build/push test (before committing):** +From repo root: build with buildx and push to your registry, or run `docker compose -f docker-compose.yml build` to verify builds. ## Code style and tooling - **TypeScript** everywhere. - **Prettier:** tabWidth 4, spaces (no tabs), singleQuote, trailingComma all, semi. - **ESLint** for TS/React/Next in qr-web; shared root config for qr-api. -- **pnpm** only; `pnpm-lock.yaml` is the single lockfile (no package-lock.json or yarn.lock). +- **pnpm** only; `pnpm-lock.yaml` is committed and is the single lockfile (no package-lock.json or yarn.lock). - **Tests:** Vitest (qr-api and qr-web). diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml new file mode 100644 index 0000000..a2dbf88 --- /dev/null +++ b/docker-compose.portainer.yml @@ -0,0 +1,112 @@ +# Portainer stack: registry-based images (no build). Use with CI/CD webhook redeploy. +# Set in Portainer stack env (or .env): REGISTRY, IMAGE_TAG (defaults below). +# Images: ${REGISTRY}/mifi-holdings/shorty-qr-api:${IMAGE_TAG}, shorty-qr-web:${IMAGE_TAG} + +services: + kutt_db: + image: postgres:16-alpine + restart: unless-stopped + networks: + - backend + volumes: + - /mnt/config/docker/kutt/postgres:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${DB_USER:-kutt} + POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD} + POSTGRES_DB: ${DB_NAME:-kutt} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-kutt} -d ${DB_NAME:-kutt}"] + interval: 10s + timeout: 5s + retries: 5 + + kutt_redis: + image: redis:7-alpine + restart: unless-stopped + networks: + - backend + volumes: + - /mnt/config/docker/kutt/redis:/data + command: redis-server --appendonly yes + + kutt: + image: kutt/kutt:latest + restart: unless-stopped + networks: + - marina-net + - backend + depends_on: + kutt_db: + condition: service_healthy + kutt_redis: + condition: service_started + environment: + DB_CLIENT: pg + DB_HOST: kutt_db + DB_PORT: "5432" + DB_USER: ${DB_USER:-kutt} + DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD} + DB_NAME: ${DB_NAME:-kutt} + REDIS_ENABLED: "true" + REDIS_HOST: kutt_redis + REDIS_PORT: "6379" + DEFAULT_DOMAIN: mifi.me + NODE_ENV: production + JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET} + labels: + - "traefik.enable=true" + - "docker.network=marina-net" + - "traefik.http.routers.kutt-mifi.rule=Host(`mifi.me`)" + - "traefik.http.routers.kutt-mifi.entrypoints=websecure" + - "traefik.http.routers.kutt-mifi.tls.certresolver=letsencrypt" + - "traefik.http.routers.kutt-mifi.service=kutt-short" + - "traefik.http.services.kutt-short.loadbalancer.server.port=3000" + - "traefik.http.routers.kutt-link.rule=Host(`link.mifi.me`)" + - "traefik.http.routers.kutt-link.entrypoints=websecure" + - "traefik.http.routers.kutt-link.tls.certresolver=letsencrypt" + - "traefik.http.routers.kutt-link.service=kutt" + - "traefik.http.services.kutt.loadbalancer.server.port=3000" + + qr_api: + image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-api:${IMAGE_TAG:-latest} + container_name: qr_api + restart: unless-stopped + networks: + - backend + volumes: + - /mnt/config/docker/qr/db:/data + - /mnt/config/docker/qr/uploads:/uploads + environment: + PORT: "8080" + DB_PATH: /data/db.sqlite + UPLOADS_PATH: /uploads + KUTT_API_KEY: ${KUTT_API_KEY:-} + KUTT_BASE_URL: http://kutt:3000 + SHORT_DOMAIN: https://mifi.me + + qr_web: + image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-web:${IMAGE_TAG:-latest} + restart: unless-stopped + networks: + - marina-net + - backend + depends_on: + - qr_api + environment: + QR_API_URL: http://qr_api:8080 + labels: + - "traefik.enable=true" + - "docker.network=marina-net" + - "traefik.http.routers.qr-web.rule=Host(`qr.mifi.dev`)" + - "traefik.http.routers.qr-web.entrypoints=websecure" + - "traefik.http.routers.qr-web.tls.certresolver=letsencrypt" + - "traefik.http.routers.qr-web.service=qr-web" + - "traefik.http.routers.qr-web.middlewares=qr-web-basicauth" + - "traefik.http.middlewares.qr-web-basicauth.basicauth.users=mifi:$$2y$$05$$TS20fkfrmJ3MLc.cgfM6OcuowOstcy/2DTOq0YfirUDU3b0vtNz." + - "traefik.http.services.qr-web.loadbalancer.server.port=3000" + +networks: + marina-net: + external: true + backend: + driver: bridge diff --git a/package.json b/package.json index 38b1b82..019d051 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "lint": "pnpm -r run lint", "format": "prettier --write .", "format:check": "prettier --check .", - "test": "pnpm -r run test" + "test": "pnpm -r run test", + "build": "pnpm -r run build" }, "devDependencies": { "prettier": "^3.4.2"