diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7ef2ea1 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(git checkout:*)", + "Bash(git add:*)", + "WebFetch(domain:localhost)", + "Bash(curl -s http://localhost:8000/api/settings/api-keys | head -50)", + "Bash(docker ps:*)", + "Bash(docker compose:*)", + "Bash(sleep 3 && curl -s http://localhost:3000/api/settings/api-keys | head -c 200)", + "Bash(sleep 4 && curl -s http://localhost:3000/api/settings/api-keys | head -c 200)", + "Bash(git commit:*)", + "WebSearch" + ] + } +} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index e797abd..96eaa5f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -13,17 +13,29 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - build-and-push-frontend: - runs-on: ubuntu-latest + build-frontend: + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write id-token: write + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Lowercase image name + run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 @@ -35,6 +47,69 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-frontend + + - name: Build and push Docker image by digest + id: build + uses: docker/build-push-action@v5.0.0 + with: + context: ./frontend + platforms: ${{ matrix.platform }} + push: ${{ github.event_name != 'pull_request' }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=frontend-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=frontend-${{ matrix.platform }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-frontend,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests/frontend + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/frontend/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-frontend-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} + path: /tmp/digests/frontend/* + if-no-files-found: error + retention-days: 1 + + merge-frontend: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + needs: build-frontend + permissions: + contents: read + packages: write + + steps: + - name: Lowercase image name + run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests/frontend + pattern: digests-frontend-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5.0.0 @@ -45,29 +120,36 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5.0.0 - with: - context: ./frontend - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Create and push manifest + working-directory: /tmp/digests/frontend + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-frontend@sha256:%s ' *) - build-and-push-backend: - runs-on: ubuntu-latest + build-backend: + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write id-token: write + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Lowercase image name + run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 @@ -79,6 +161,69 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend + + - name: Build and push Docker image by digest + id: build + uses: docker/build-push-action@v5.0.0 + with: + context: ./backend + platforms: ${{ matrix.platform }} + push: ${{ github.event_name != 'pull_request' }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=backend-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=backend-${{ matrix.platform }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests/backend + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/backend/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-backend-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} + path: /tmp/digests/backend/* + if-no-files-found: error + retention-days: 1 + + merge-backend: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + needs: build-backend + permissions: + contents: read + packages: write + + steps: + - name: Lowercase image name + run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests/backend + pattern: digests-backend-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5.0.0 @@ -89,14 +234,9 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5.0.0 - with: - context: ./backend - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Create and push manifest + working-directory: /tmp/digests/backend + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend@sha256:%s ' *) diff --git a/.gitignore b/.gitignore index ca4993a..87dc843 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ clean_zip.py zip_repo.py refactor_cesium.py jobs.json + +.claude +.mise.local.toml diff --git a/.mise.local.toml b/.mise.local.toml new file mode 100644 index 0000000..1eeeb78 --- /dev/null +++ b/.mise.local.toml @@ -0,0 +1,5 @@ +[env] +AIS_API_KEY="***REMOVED***" +OPENSKY_CLIENT_ID="***REMOVED***" +OPENSKY_CLIENT_SECRET="***REMOVED***" +BACKEND_URL="http://localhost:8000" \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 225f635..8616154 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,16 +9,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* -# Install dependencies +# Install Python dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +# Install Node.js dependencies (ws module for AIS WebSocket proxy) +# Copy manifests first so this layer is cached unless deps change +COPY package*.json ./ +RUN npm install --omit=dev + # Copy source code COPY . . -# Install Node.js dependencies (ws module for AIS WebSocket proxy) -RUN npm install --omit=dev - # Create a non-root user for security RUN adduser --system --uid 1001 backenduser \ && chown -R backenduser /app diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2b3fd73..82bd2f7 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -11,10 +11,6 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED 1 -# NEXT_PUBLIC_* vars must exist at build time for Next.js to inline them. -# Default empty = auto-detect from browser hostname at runtime. -ARG NEXT_PUBLIC_API_URL="" -ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL RUN npm run build FROM base AS runner diff --git a/frontend/next.config.ts b/frontend/next.config.ts index bd8c994..6741f71 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,9 +1,9 @@ import type { NextConfig } from "next"; -// BACKEND_URL is a plain (non-NEXT_PUBLIC_) env var read at server startup — -// not baked at build time — so it can be set in docker-compose `environment`. -// Defaults to localhost for local dev where both services run on the same host. -const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8000"; +// /api/* requests are proxied to the backend by the catch-all route handler at +// src/app/api/[...path]/route.ts, which reads BACKEND_URL at request time. +// Do NOT add rewrites for /api/* here — next.config is evaluated at build time, +// so any URL baked in here ignores the runtime BACKEND_URL env var. const nextConfig: NextConfig = { transpilePackages: ['react-map-gl', 'mapbox-gl', 'maplibre-gl'], @@ -14,14 +14,6 @@ const nextConfig: NextConfig = { eslint: { ignoreDuringBuilds: true, }, - async rewrites() { - return [ - { - source: "/api/:path*", - destination: `${backendUrl}/api/:path*`, - }, - ]; - }, }; export default nextConfig;