```
├── .env.example (omitted)
├── .github/
├── ISSUE_TEMPLATE/
├── Ã¥ÂÂè½許é¡Â.md
├── Ã¥ÂÂå ±-bug.md
├── workflows/
├── ci.yml (400 tokens)
├── codeql.yml (900 tokens)
├── deploy.yml (1700 tokens)
├── .gitignore (100 tokens)
├── .kiro/
├── specs/
├── deploy-to-eks/
├── design.md (3.5k tokens)
├── requirements.md (4.1k tokens)
├── spec.json (100 tokens)
├── tasks.md (4.6k tokens)
├── steering/
├── AGENTS.md (600 tokens)
├── commit.md (400 tokens)
├── linus-review.md (1200 tokens)
├── product.md (600 tokens)
├── security-check.md (400 tokens)
├── structure.md (2.4k tokens)
├── tdd-guideline.md (1500 tokens)
├── tech.md (1500 tokens)
├── .spectral.yaml
├── CLAUDE.md (600 tokens)
├── Dockerfile.backend (400 tokens)
├── Dockerfile.frontend (400 tokens)
├── README.md (1300 tokens)
├── api-spec/
├── openapi.yaml (6.4k tokens)
├── api/
├── openapi.yaml (5.7k tokens)
├── build.sh (1000 tokens)
├── components.json (100 tokens)
├── docker-compose.yml (100 tokens)
├── docs/
├── AGENTS.md (1300 tokens)
├── CONTRIBUTING.md (200 tokens)
├── DEPLOYMENT_RUNBOOK.md (3.2k tokens)
├── DEPLOY_WORKFLOW_PARAMETERIZATION.md (2k tokens)
├── GITHUB_SECRETS_SETUP.md (1800 tokens)
├── SECURITY.md (300 tokens)
├── eslint.config.js (300 tokens)
├── index.html (100 tokens)
├── jsconfig.json
├── k8s/
├── base/
├── backend-deployment.yaml (600 tokens)
├── backend-service.yaml (100 tokens)
├── backend-targetgroupbinding.yaml
├── frontend-deployment.yaml (400 tokens)
├── frontend-service.yaml (100 tokens)
├── frontend-targetgroupbinding.yaml
├── ingress.yaml (400 tokens)
├── kustomization.yaml (100 tokens)
├── namespace.yaml
├── serviceaccount.yaml (100 tokens)
├── netlify.toml (100 tokens)
├── nginx.conf (200 tokens)
├── package-lock.json (omitted)
├── package.json (800 tokens)
├── packages/
├── backend/
├── .env.example (200 tokens)
├── package.json (200 tokens)
├── scripts/
├── import-base44.ts (2.8k tokens)
├── import-grids-csv.ts (1700 tokens)
├── seed.ts (2.8k tokens)
├── src/
├── index.ts (1000 tokens)
├── lib/
├── db-init.ts (1500 tokens)
├── db.ts (400 tokens)
├── etag.ts (300 tokens)
├── line-messaging.ts (400 tokens)
├── line.ts
├── middlewares/
├── AuditLogMiddleware.ts (500 tokens)
├── modules/
├── audit-logs/
├── audit-log.repo.ts (200 tokens)
├── audit-log.service.ts (100 tokens)
├── audit-log.types.ts (100 tokens)
├── disaster-areas/
├── repo.ts (700 tokens)
├── routes/
├── announcements.ts (1300 tokens)
├── auth-line.ts (1100 tokens)
├── disaster-areas.ts (600 tokens)
├── functions.ts (400 tokens)
├── grid-discussions.ts (900 tokens)
├── grids.ts (2k tokens)
├── legacy.ts
├── line-webhook.ts (500 tokens)
├── supply-donations.ts (500 tokens)
├── users.ts (1600 tokens)
├── volunteer-registrations.ts (1700 tokens)
├── volunteers.ts (1300 tokens)
├── types/
├── fastify.d.ts (omitted)
├── tsconfig.json (100 tokens)
├── shared-types/
├── package.json
├── src/
├── openapi.ts (10k tokens)
├── tsconfig.json (100 tokens)
├── postcss.config.js
├── public/
├── _redirects
├── src/
├── App.css
├── App.jsx
├── api/
├── base44Client.js (100 tokens)
├── entities.js (100 tokens)
├── functions.js (100 tokens)
├── integrations.js (100 tokens)
├── rest/
├── client.js (300 tokens)
├── entities.js (500 tokens)
├── functions.js (400 tokens)
├── index.js (100 tokens)
├── components/
├── admin/
├── AddAreaModal.jsx (1700 tokens)
├── AddGridModal.jsx (3.4k tokens)
├── EditGridModal.jsx (1400 tokens)
├── GridImportExportButtons.jsx (600 tokens)
├── icons/
├── GithubIcon.jsx (300 tokens)
├── login/
├── AskForLoginModal.jsx (200 tokens)
├── map/
├── AnnouncementModal.jsx (1700 tokens)
├── AnnouncementPanel.jsx (1900 tokens)
├── GridDetailModal.jsx (6.2k tokens)
├── MarkerClusterGroup.jsx (200 tokens)
├── supplies/
├── AddSupplyRequestModal.jsx (1600 tokens)
├── ui/
├── accordion.jsx (300 tokens)
├── alert-dialog.jsx (700 tokens)
├── alert.jsx (300 tokens)
├── aspect-ratio.jsx
├── avatar.jsx (200 tokens)
├── badge.jsx (200 tokens)
├── breadcrumb.jsx (500 tokens)
├── button.jsx (300 tokens)
├── calendar.jsx (600 tokens)
├── card.jsx (300 tokens)
├── carousel.jsx (1000 tokens)
├── chart.jsx (1700 tokens)
├── checkbox.jsx (200 tokens)
├── collapsible.jsx (100 tokens)
├── command.jsx (800 tokens)
├── context-menu.jsx (1200 tokens)
├── dialog.jsx (600 tokens)
├── drawer.jsx (500 tokens)
├── dropdown-menu.jsx (1200 tokens)
├── form.jsx (600 tokens)
├── hover-card.jsx (200 tokens)
├── input-otp.jsx (400 tokens)
├── input.jsx (100 tokens)
├── label.jsx (100 tokens)
├── menubar.jsx (1400 tokens)
├── navigation-menu.jsx (800 tokens)
├── pagination.jsx (500 tokens)
├── popover.jsx (200 tokens)
├── progress.jsx (100 tokens)
├── radio-group.jsx (200 tokens)
├── resizable.jsx (300 tokens)
├── scroll-area.jsx (300 tokens)
├── select.jsx (900 tokens)
├── separator.jsx (100 tokens)
├── sheet.jsx (700 tokens)
├── sidebar.jsx (4.1k tokens)
├── skeleton.jsx
├── slider.jsx (200 tokens)
├── sonner.jsx (200 tokens)
├── switch.jsx (200 tokens)
├── table.jsx (400 tokens)
├── tabs.jsx (300 tokens)
├── textarea.jsx (100 tokens)
├── toast.jsx (800 tokens)
├── toaster.jsx (200 tokens)
├── toggle-group.jsx (300 tokens)
├── toggle.jsx (300 tokens)
├── tooltip.jsx (200 tokens)
├── use-toast.jsx (700 tokens)
├── context/
├── AuthContext.jsx (400 tokens)
├── hooks/
├── use-breakpoint.jsx (500 tokens)
├── use-map-data.jsx (300 tokens)
├── use-mobile.jsx (200 tokens)
├── use-supplies-data.jsx (400 tokens)
├── image/
├── Shovel_Hero_Logo.png
├── index.css (800 tokens)
├── lib/
├── utils.js (200 tokens)
├── main.jsx (100 tokens)
├── pages/
├── About.jsx (1700 tokens)
├── Admin.jsx (7.7k tokens)
├── GridMonitor.jsx (2.6k tokens)
├── Layout.jsx (3.5k tokens)
├── LineReturn.jsx (400 tokens)
├── Map.jsx (8.4k tokens)
├── NotFound.jsx (200 tokens)
├── Privacy.jsx (600 tokens)
├── RequestHelp.jsx (1700 tokens)
├── Supplies.jsx (4.6k tokens)
├── Volunteers.jsx (4.1k tokens)
├── index.jsx (500 tokens)
├── test/
├── example.test.tsx (100 tokens)
├── setup.ts (200 tokens)
├── utils/
├── index.ts
├── tailwind.config.js (500 tokens)
├── vite.config.js (100 tokens)
```
## /.github/ISSUE_TEMPLATE/Ã¥ÂÂè½許é¡Â.md
---
name: 功能許願
about: 鏟子英雄/功能許願回報單
title: ''
labels: enhancement
assignees: ''
---
**請描述想要的新功能**
**是來自現場的嗎?**
如果可以,請找其他人幫忙按表情讓我們知道這個功能真的很多人需要。
## /.github/ISSUE_TEMPLATE/Ã¥ÂÂå ±-bug.md
---
name: 回報 Bug
about: 鏟子英雄/功能許願回報單
title: ''
labels: bug
assignees: ''
---
**請描述bug **
(有圖的話可在下方補充)
**重現步驟**
## /.github/workflows/ci.yml
```yml path="/.github/workflows/ci.yml"
name: CI
on:
push:
branches: [ main, master, develop, feature/**, hotfix/** ]
pull_request:
branches: [ main, master, develop ]
permissions:
contents: read
# (add others like packages: write if later needed)
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-lint:
name: Install, Lint & Build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate OpenAPI types (codegen)
run: npm run codegen
- name: Check for codegen drift
run: |
if ! git diff --quiet --exit-code; then
echo "❌ Generated files are not committed. Run 'npm run codegen' locally and commit the changes." >&2
git diff --name-only >&2
exit 1
fi
- name: Lint (ESLint)
run: npm run lint
- name: OpenAPI Lint (Spectral)
run: npm run openapi:lint
- name: Bundle OpenAPI (Redocly)
run: npm run openapi:bundle
- name: Build backend (tsc)
run: npm run build:api
- name: Build frontend (Vite)
run: npm run build
- name: Upload OpenAPI bundle artifact
uses: actions/upload-artifact@v4
with:
name: openapi-bundle
path: api-spec/dist/openapi.bundle.yaml
if-no-files-found: warn
# Placeholder for future tests (unit/integration)
# tests:
# runs-on: ubuntu-latest
# needs: build-and-lint
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: 22
# cache: npm
# - run: npm ci
# - run: npm test --workspaces
```
## /.github/workflows/codeql.yml
```yml path="/.github/workflows/codeql.yml"
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '18 3 * * 2'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
```
## /.github/workflows/deploy.yml
```yml path="/.github/workflows/deploy.yml"
name: Deploy to EKS
on:
push:
branches:
- main
workflow_dispatch:
inputs:
image_tag:
description: 'Docker image tag to deploy (e.g., v1.0.0)'
required: false
type: string
env:
AWS_REGION: ap-east-2
EKS_CLUSTER_NAME: eks-shovel-heros
ECR_BACKEND_REPO: shovel-heros-staging-shovel-heroes-backend
ECR_FRONTEND_REPO: shovel-heros-staging-shovel-heroes-frontend
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
name: Build and Deploy
runs-on: ubuntu-latest
# Note: Cannot use secrets in job-level if conditions (GitHub Actions limitation)
# Using step-level conditionals instead
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check if AWS credentials are configured
id: check-secrets
env:
AWS_ROLE_ARN: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }}
run: |
if [ -z "$AWS_ROLE_ARN" ]; then
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "⚠️ AWS credentials not configured - skipping deployment"
else
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "✅ AWS credentials found - proceeding with deployment"
fi
- name: Configure AWS credentials via OIDC
if: steps.check-secrets.outputs.has_secrets == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
if: steps.check-secrets.outputs.has_secrets == 'true'
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set image tag
if: steps.check-secrets.outputs.has_secrets == 'true'
id: set-tag
run: |
if [ -n "${{ inputs.image_tag }}" ]; then
echo "IMAGE_TAG=${{ inputs.image_tag }}" >> $GITHUB_OUTPUT
else
echo "IMAGE_TAG=${{ github.sha }}" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
if: steps.check-secrets.outputs.has_secrets == 'true'
uses: docker/setup-buildx-action@v3
- name: Build and push backend image
if: steps.check-secrets.outputs.has_secrets == 'true'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ steps.set-tag.outputs.IMAGE_TAG }}
run: |
docker build -f Dockerfile.backend \
-t $ECR_REGISTRY/$ECR_BACKEND_REPO:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_BACKEND_REPO:latest \
.
docker push $ECR_REGISTRY/$ECR_BACKEND_REPO:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_BACKEND_REPO:latest
- name: Build and push frontend image
if: steps.check-secrets.outputs.has_secrets == 'true'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ steps.set-tag.outputs.IMAGE_TAG }}
run: |
docker build -f Dockerfile.frontend \
--build-arg VITE_API_BASE=https://api.shovel-heroes.cc \
-t $ECR_REGISTRY/$ECR_FRONTEND_REPO:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_FRONTEND_REPO:latest \
.
docker push $ECR_REGISTRY/$ECR_FRONTEND_REPO:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_FRONTEND_REPO:latest
- name: Install kubectl
if: steps.check-secrets.outputs.has_secrets == 'true'
uses: azure/setup-kubectl@v4
with:
version: 'v1.31.0'
- name: Update kubeconfig for EKS
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ env.EKS_CLUSTER_NAME }}
- name: Create namespace and secrets
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
# Create namespace if it doesn't exist
kubectl create namespace shovel-heroes --dry-run=client -o yaml | kubectl apply -f - --validate=false
# Get secrets from AWS Secrets Manager
DB_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_DB_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
APP_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_APP_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
# Get JWT secret
JWT_SECRET_OBJ=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_JWT_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
# Extract values
DATABASE_URL=$(echo $DB_SECRET | jq -r '.url')
JWT_SECRET=$(echo $JWT_SECRET_OBJ | jq -r '.secret')
API_KEY="null"
LINE_CHANNEL_ID=$(echo $APP_SECRET | jq -r '.line_channel_id')
LINE_CHANNEL_SECRET=$(echo $APP_SECRET | jq -r '.line_channel_secret')
TURNSTILE_SECRET_KEY=$(echo $APP_SECRET | jq -r '.turnstile_secret_key')
# Create Kubernetes secret
kubectl create secret generic shovel-heroes-secrets \
--from-literal=database-url="$DATABASE_URL" \
--from-literal=jwt-secret="$JWT_SECRET" \
--from-literal=api-key="$API_KEY" \
--from-literal=line-channel-id="$LINE_CHANNEL_ID" \
--from-literal=line-channel-secret="$LINE_CHANNEL_SECRET" \
--from-literal=turnstile-secret-key="$TURNSTILE_SECRET_KEY" \
-n shovel-heroes \
--dry-run=client -o yaml | kubectl apply -f - --validate=false
- name: Deploy to Kubernetes
if: steps.check-secrets.outputs.has_secrets == 'true'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ steps.set-tag.outputs.IMAGE_TAG }}
BACKEND_POD_ROLE_ARN: ${{ secrets.BACKEND_POD_ROLE_ARN }}
BACKEND_TARGET_GROUP_ARN: ${{ secrets.BACKEND_TARGET_GROUP_ARN }}
FRONTEND_TARGET_GROUP_ARN: ${{ secrets.FRONTEND_TARGET_GROUP_ARN }}
run: |
# Replace IRSA role ARN placeholder in base manifests
sed -i "s|BACKEND_POD_ROLE_ARN|$BACKEND_POD_ROLE_ARN|g" k8s/base/serviceaccount.yaml
# Update images in manifests
sed -i "s|BACKEND_ECR_URL:latest|$ECR_REGISTRY/$ECR_BACKEND_REPO:$IMAGE_TAG|g" k8s/base/backend-deployment.yaml
sed -i "s|FRONTEND_ECR_URL:latest|$ECR_REGISTRY/$ECR_FRONTEND_REPO:$IMAGE_TAG|g" k8s/base/frontend-deployment.yaml
# Replace Target Group ARNs in TargetGroupBinding manifests
sed -i "s|BACKEND_TARGET_GROUP_ARN|$BACKEND_TARGET_GROUP_ARN|g" k8s/base/backend-targetgroupbinding.yaml
sed -i "s|FRONTEND_TARGET_GROUP_ARN|$FRONTEND_TARGET_GROUP_ARN|g" k8s/base/frontend-targetgroupbinding.yaml
# Apply the manifests (exclude kustomization.yaml)
kubectl apply -f k8s/base/namespace.yaml
kubectl apply -f k8s/base/serviceaccount.yaml
kubectl apply -f k8s/base/backend-deployment.yaml
kubectl apply -f k8s/base/frontend-deployment.yaml
kubectl apply -f k8s/base/backend-service.yaml
kubectl apply -f k8s/base/frontend-service.yaml
kubectl apply -f k8s/base/backend-targetgroupbinding.yaml
kubectl apply -f k8s/base/frontend-targetgroupbinding.yaml
kubectl apply -f k8s/base/ingress.yaml
# Wait for rollout to complete
kubectl rollout status deployment/backend -n shovel-heroes --timeout=5m
kubectl rollout status deployment/frontend -n shovel-heroes --timeout=5m
- name: Verify deployment
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
echo "=== Deployment Status ==="
kubectl get deployments -n shovel-heroes
kubectl get pods -n shovel-heroes
kubectl get services -n shovel-heroes
kubectl get ingress -n shovel-heroes
- name: Run health checks
if: steps.check-secrets.outputs.has_secrets == 'true'
run: |
echo "=== Health Checks ==="
echo "Waiting for pods to be ready..."
sleep 10
echo "Backend health check:"
kubectl get pods -n shovel-heroes -l app.kubernetes.io/component=backend
echo "Frontend health check:"
kubectl get pods -n shovel-heroes -l app.kubernetes.io/component=frontend
```
## /.gitignore
```gitignore path="/.gitignore"
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
secrets.json
setup-secrets.sh
```
## /.kiro/specs/deploy-to-eks/design.md
# Technical Design Document
## Project: deploy-to-eks
**Project Name:** Shovel Heroes EKS Deployment Pipeline
**Architecture:** GitHub Actions CI/CD with Kubernetes Deployment
**Infrastructure:** AWS EKS (Kubernetes 1.31), ECR, ALB, RDS
Generated on: 2025-10-03T14:45:36.394Z
## Architecture Overview
### System Architecture
GitHub Actions-based Continuous Deployment pipeline deploying containerized frontend and backend applications to AWS EKS with path-based ALB routing.
```
GitHub Actions Workflow
↓
├─ Build Backend Docker Image (Node.js 20 + Fastify)
├─ Build Frontend Docker Image (Node.js 20 + Nginx)
↓
Push to ECR (existing repositories)
↓
Deploy to EKS via kubectl
↓
ALB Path-Based Routing
├─ /* → Frontend Service (Nginx:80)
└─ /api/* → Backend Service (Fastify:8787)
```
### Key Components
#### 1. Docker Images
- **Backend**: Multi-stage Node.js 20 → Fastify compiled app
- **Frontend**: Multi-stage Node.js 20 build → Nginx static serving
#### 2. GitHub Actions Workflows
- **deploy-staging.yml**: Auto-deploy on push to `main`
- **deploy-production.yml**: Manual trigger with approval gate
#### 3. Kubernetes Resources
- **Namespace**: `shovel-heroes`
- **ServiceAccount**: IRSA-enabled for AWS secrets access
- **Deployments**: Backend (2 replicas) + Frontend (2 replicas)
- **Services**: ClusterIP for internal routing
- **Ingress**: ALB Ingress Controller with path-based routing
#### 4. AWS Integration
- **ECR**: Container registry (existing repos)
- **EKS**: Kubernetes cluster (existing)
- **ALB**: Application Load Balancer with target groups
- **Secrets Manager**: Database credentials via IRSA
- **IAM**: GitHub OIDC provider + deployment role
## Implementation Details
### 1. Dockerfile Specifications
#### Backend Dockerfile (`Dockerfile.backend`)
```dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY packages/backend/package*.json ./packages/backend/
COPY packages/shared-types/ ./packages/shared-types/
RUN npm ci --workspace=packages/backend --workspace=packages/shared-types
COPY . .
RUN npm run build:api
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/packages/backend/dist ./packages/backend/dist
COPY --from=builder /app/packages/backend/package*.json ./packages/backend/
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 8787
CMD ["node", "packages/backend/dist/index.js"]
```
#### Frontend Dockerfile (`Dockerfile.frontend`)
```dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
#### Nginx Configuration (`nginx.conf`)
```nginx
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA routing - fallback to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
```
### 2. Kubernetes Manifests (Kustomize Structure)
#### Base Manifests (`k8s/base/`)
**namespace.yaml**
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: shovel-heroes
```
**serviceaccount.yaml**
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: shovel-heroes-backend
namespace: shovel-heroes
annotations:
eks.amazonaws.com/role-arn: ${BACKEND_POD_ROLE_ARN}
```
**backend-deployment.yaml**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: shovel-heroes
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
serviceAccountName: shovel-heroes-backend
containers:
- name: backend
image: ${ECR_BACKEND_URL}:${IMAGE_TAG}
ports:
- containerPort: 8787
name: http
env:
- name: PORT
value: "8787"
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-credentials
key: database_url
livenessProbe:
httpGet:
path: /healthz
port: 8787
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8787
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
```
**frontend-deployment.yaml**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: shovel-heroes
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: ${ECR_FRONTEND_URL}:${IMAGE_TAG}
ports:
- containerPort: 80
name: http
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
```
**backend-service.yaml**
```yaml
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: shovel-heroes
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 8787
targetPort: 8787
protocol: TCP
```
**frontend-service.yaml**
```yaml
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: shovel-heroes
spec:
type: ClusterIP
selector:
app: frontend
ports:
- port: 80
targetPort: 80
protocol: TCP
```
**ingress.yaml**
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shovel-heroes-ingress
namespace: shovel-heroes
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: backend
port:
number: 8787
- path: /healthz
pathType: Exact
backend:
service:
name: backend
port:
number: 8787
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
```
**kustomization.yaml**
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- serviceaccount.yaml
- backend-deployment.yaml
- backend-service.yaml
- frontend-deployment.yaml
- frontend-service.yaml
- ingress.yaml
```
#### Environment Overlays
**staging/kustomization.yaml**
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base
patchesStrategicMerge:
- replica-patch.yaml
namePrefix: staging-
```
**production/kustomization.yaml**
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base
patchesStrategicMerge:
- replica-patch.yaml
namePrefix: prod-
```
### 3. GitHub Actions Workflow Design
#### Workflow: deploy-staging.yml
```yaml
name: Deploy to EKS Staging
on:
push:
branches: [main]
workflow_dispatch:
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-east-2
EKS_CLUSTER_NAME: shovel-heroes-staging-eks
ENVIRONMENT: staging
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push backend image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -f Dockerfile.backend -t $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:$IMAGE_TAG .
docker tag $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:$IMAGE_TAG \
$ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:latest
docker push $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:$IMAGE_TAG
docker push $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:latest
- name: Build and push frontend image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -f Dockerfile.frontend -t $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:$IMAGE_TAG .
docker tag $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:$IMAGE_TAG \
$ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:latest
docker push $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:$IMAGE_TAG
docker push $ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:latest
- name: Configure kubectl
run: |
aws eks update-kubeconfig --name ${{ env.EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }}
- name: Create namespace if not exists
run: |
kubectl create namespace shovel-heroes --dry-run=client -o yaml | kubectl apply -f -
- name: Retrieve database credentials from Secrets Manager
id: get-secrets
run: |
DB_SECRET=$(aws secretsmanager get-secret-value --secret-id ${{ secrets.DATABASE_SECRET_ARN }} --query SecretString --output text)
DB_URL=$(echo $DB_SECRET | jq -r '.database_url')
echo "::add-mask::$DB_URL"
echo "database_url=$DB_URL" >> $GITHUB_OUTPUT
- name: Create/Update Kubernetes secrets
run: |
kubectl create secret generic database-credentials \
--from-literal=database_url="${{ steps.get-secrets.outputs.database_url }}" \
-n shovel-heroes \
--dry-run=client -o yaml | kubectl apply -f -
- name: Deploy to EKS using Kustomize
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
cd k8s/overlays/staging
kustomize edit set image \
backend=$ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-backend:$IMAGE_TAG \
frontend=$ECR_REGISTRY/shovel-heroes-staging-shovel-heroes-frontend:$IMAGE_TAG
kubectl apply -k .
- name: Wait for deployments
run: |
kubectl rollout status deployment/backend -n shovel-heroes --timeout=300s
kubectl rollout status deployment/frontend -n shovel-heroes --timeout=300s
- name: Verify pod health
run: |
kubectl wait --for=condition=ready pod -l app=backend -n shovel-heroes --timeout=300s
kubectl wait --for=condition=ready pod -l app=frontend -n shovel-heroes --timeout=300s
- name: Get deployment info
run: |
kubectl get pods -n shovel-heroes
kubectl get svc -n shovel-heroes
kubectl get ingress -n shovel-heroes
```
### 4. Terraform Prerequisites (GitHub OIDC)
New file to add to terraform repo: `iam_github_actions.tf`
```hcl
# GitHub OIDC Provider
data "tls_certificate" "github" {
url = "https://token.actions.githubusercontent.com"
}
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
tags = local.common_tags
}
# IAM Role for GitHub Actions
resource "aws_iam_role" "github_actions" {
name_prefix = "${var.project_name}-${local.env_type}-github-actions-"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:YOUR_GITHUB_ORG/shovel-heroes-k8s:*"
}
}
}]
})
tags = local.common_tags
}
# Policies for GitHub Actions role
resource "aws_iam_role_policy_attachment" "github_actions_ecr" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.ecr_write.arn
}
resource "aws_iam_role_policy" "github_actions_eks" {
name = "eks-access"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"eks:DescribeCluster",
"eks:ListClusters",
"eks:AccessKubernetesApi"
]
Resource = module.eks[0].cluster_arn
}
]
})
}
resource "aws_iam_role_policy" "github_actions_secrets" {
name = "secrets-read"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = [
aws_secretsmanager_secret.database[0].arn,
aws_secretsmanager_secret.application.arn
]
}
]
})
}
# Output for GitHub Secrets
output "github_actions_role_arn" {
description = "IAM role ARN for GitHub Actions (add to GitHub Secrets)"
value = aws_iam_role.github_actions.arn
}
```
## Configuration
### Environment Variables
#### GitHub Secrets (Repository Level)
- `AWS_ROLE_ARN_STAGING`: IAM role for staging deployment
- `AWS_ROLE_ARN_PROD`: IAM role for production deployment
#### GitHub Secrets (Environment Level)
- `DATABASE_SECRET_ARN`: ARN of database credentials in Secrets Manager
- `BACKEND_POD_ROLE_ARN`: IRSA role ARN for backend pods
#### Application Environment Variables (Kubernetes)
- `PORT`: 8787 (backend)
- `NODE_ENV`: production
- `DATABASE_URL`: Retrieved from Secrets Manager via IRSA
### Build Configuration
- **Backend Build**: `npm run build:api` → `packages/backend/dist/`
- **Frontend Build**: `npm run build` → `dist/`
- **Docker BuildKit**: Enabled for multi-stage builds
## Dependencies
### Production Dependencies
- Node.js 20 (runtime)
- Nginx alpine (frontend serving)
- PostgreSQL client libraries (backend)
### Build/Deploy Dependencies
- Docker
- kubectl
- kustomize
- AWS CLI
- GitHub Actions runners
## Testing Strategy
### Pre-deployment Validation
1. Docker image builds successfully
2. Container health checks pass locally
3. Kubernetes manifests validate with `kubectl apply --dry-run`
### Post-deployment Validation
1. Pod readiness checks (liveness/readiness probes)
2. Service endpoint accessibility
3. Database connectivity via `/healthz`
4. ALB target health checks
### Rollback Criteria
- Pod health checks fail after 5 minutes
- Backend `/healthz` returns non-200 status
- Frontend returns 5xx errors
## Security Considerations
### OIDC Authentication
- GitHub Actions authenticate via OIDC (no static AWS keys)
- Temporary credentials with scoped permissions
- Repository-specific trust relationship
### Secrets Management
- Database credentials in AWS Secrets Manager
- IRSA for pod-level AWS access
- Kubernetes secrets for runtime environment
### Container Security
- Multi-stage builds (minimal attack surface)
- Non-root user execution (to be added)
- Image vulnerability scanning via ECR
### Network Security
- Private EKS subnets (no public IPs)
- ALB in public subnets (internet-facing)
- Security groups restrict pod-to-RDS traffic
## Monitoring and Observability
### Application Logs
- stdout/stderr captured by EKS
- Forwarded to CloudWatch Logs
- Queryable via `kubectl logs`
### Metrics
- Kubernetes pod metrics (CPU, memory)
- ALB target health metrics
- Custom application metrics (future)
### Alerts
- Pod restart count > 5
- Deployment rollout failure
- ALB unhealthy target count
## Rollback Procedure
### Automated Rollback
```bash
kubectl rollout undo deployment/backend -n shovel-heroes
kubectl rollout undo deployment/frontend -n shovel-heroes
```
### Manual Rollback
1. Identify last known good image tag
2. Update deployment image tag
3. Apply manifest: `kubectl apply -k k8s/overlays/{env}`
4. Verify health checks pass
## /.kiro/specs/deploy-to-eks/requirements.md
# Requirements Document: AWS EKS Deployment Pipeline
## Introduction
This document outlines requirements for implementing a GitHub Actions CD pipeline to deploy the Shovel Heroes disaster relief platform to AWS EKS (Elastic Kubernetes Service).
**Project**: Shovel Heroes
**Feature**: GitHub Actions CD Pipeline for AWS EKS Deployment
**Version**: 0.1.0
**Infrastructure Repository**: `/Users/yihuang/workspace/shovel-heroes-terraform`
Generated on: 2025-10-03T14:08:18.713Z
## Project Context
Shovel Heroes is a full-stack monorepo disaster relief coordination platform with:
- **Frontend**: Vite + React + Tailwind (static build output)
- **Backend**: Fastify + Node.js + PostgreSQL (containerized service)
- **Architecture**: Monorepo with shared TypeScript types via OpenAPI
## Existing Infrastructure (Terraform-managed)
The following AWS resources are **already provisioned** via Terraform:
### Compute & Networking
- **EKS Cluster**: Kubernetes 1.31 with Amazon Linux 2023 nodes
- **Cluster Name**: `shovel-heroes-{env}-eks` (e.g., `shovel-heroes-prod-eks`)
- **Region**: `ap-east-2` (Asia Pacific - Hong Kong)
- **VPC**: 10.0.0.0/16 with public/private subnets across 2-3 AZs
- **Node Groups**: SPOT instances (t3.small/medium for non-prod, t3.medium for prod)
- **IRSA Enabled**: IAM Roles for Service Accounts configured
### Container Registry
- **ECR Repositories** (already exist):
- `shovel-heroes-{env}-shovel-heroes-backend`
- `shovel-heroes-{env}-shovel-heroes-frontend` (will be used for Nginx-based frontend)
- **Image Scanning**: Enabled on push
- **Lifecycle Policy**: Retains last 30 images (prod) / 10 images (non-prod)
### Database
- **RDS PostgreSQL 16.9**
- **Instance**: db.t3.micro (non-prod) / db.t3.small (prod)
- **Multi-AZ**: Enabled for production
- **Encryption**: KMS-encrypted at rest
- **Backup Retention**: 1 day (non-prod) / 7 days (prod)
- **Credentials**: Stored in AWS Secrets Manager
### Load Balancing
- **ALB**: Application Load Balancer with multiple target groups
- **Backend Target Group**: Port 8787 (health check: `/healthz`)
- **Frontend Target Group**: Port 80 (health check: `/` or `/index.html`)
- **Path-Based Routing**:
- `/api/*` → Backend service (port 8787)
- `/*` → Frontend service (port 80)
### Security & Secrets
- **AWS Secrets Manager**: Database credentials, JWT secrets, application config
- **Security Groups**: Pre-configured for ALB, backend pods, and RDS
- **IAM Roles**:
- `backend-pods` role with IRSA for accessing Secrets Manager, S3, CloudWatch
- ECR read/write policies already attached
### Monitoring
- **CloudWatch Logs**: EKS cluster logs, application logs
- **CloudWatch Alarms**: CPU, RDS, ALB health monitoring
- **Retention**: 3 days (non-prod) / 30 days (prod)
## Functional Requirements
### FR-1: GitHub Actions Workflow Configuration
**Objective:** Create automated CD pipeline triggered by main branch commits
#### Acceptance Criteria
1. WHEN code is pushed to `main` branch THEN GitHub Actions workflow SHALL trigger automatically
2. WHEN pull request is merged to `main` THEN deployment pipeline SHALL execute
3. IF workflow is manually triggered THEN it SHALL support workflow_dispatch event
4. WHILE workflow runs IT SHALL provide clear status indicators and logs
### FR-2: Docker Image Build and Push to Existing ECR
**Objective:** Build and push containerized images for both backend and frontend
#### Acceptance Criteria
1. Backend Docker image SHALL:
- Create `Dockerfile.backend` in repository root
- Use multi-stage build for optimized size (Node.js 20 base)
- Install workspace dependencies (`npm install`)
- Build backend (`npm run build:api`)
- Run `node packages/backend/dist/index.js` as entrypoint
- Expose port 8787
- Be tagged with:
- Git commit SHA: `{ecr-repo}:{github.sha}`
- Latest tag: `{ecr-repo}:latest`
2. Frontend Docker image SHALL:
- Create `Dockerfile.frontend` in repository root
- Use multi-stage build:
- Stage 1: Node.js 20 to build assets (`npm run build` → `dist/`)
- Stage 2: Nginx alpine to serve static files
- Copy built assets from `dist/` to Nginx `/usr/share/nginx/html`
- Configure Nginx for SPA routing (fallback to `index.html`)
- Expose port 80
- Be tagged with:
- Git commit SHA: `{ecr-repo}:{github.sha}`
- Latest tag: `{ecr-repo}:latest`
3. Images SHALL be pushed to existing ECR repositories:
- Backend: `shovel-heroes-{env}-shovel-heroes-backend`
- Frontend: `shovel-heroes-{env}-shovel-heroes-frontend`
- Use AWS ECR login via GitHub Actions: `aws-actions/amazon-ecr-login@v2`
- Leverage existing lifecycle policies (auto-cleanup)
### FR-3: Kubernetes Manifests for Existing EKS Cluster
**Objective:** Define Kubernetes resources for deployment to pre-existing EKS cluster
#### Acceptance Criteria
1. Namespace SHALL:
- Use namespace: `shovel-heroes`
- Create if not exists via pipeline
2. ServiceAccount SHALL:
- Name: `shovel-heroes-backend`
- Annotate with IRSA role ARN from Terraform outputs: `backend_pod_role_arn`
- Enable access to AWS Secrets Manager, S3, CloudWatch
3. Backend Deployment SHALL:
- Name: `backend`
- Replicas: 1 (non-prod) / 2 (prod) - align with `local.backend_desired_replicas`
- Container image from ECR: `{ecr-url}/shovel-heroes-{env}-shovel-heroes-backend:{tag}`
- Container port: 8787
- Environment variables:
- `PORT=8787`
- `NODE_ENV=production`
- `DATABASE_URL` (from Kubernetes Secret or AWS Secrets Manager via CSI driver)
- Liveness probe: `GET /healthz` (initial delay 30s, period 10s)
- Readiness probe: `GET /healthz` (initial delay 10s, period 5s)
- Resource requests: `cpu=500m, memory=512Mi`
- Resource limits: `cpu=1000m, memory=1Gi`
- Rolling update strategy: `maxSurge=1, maxUnavailable=0` (zero downtime)
4. Backend Service SHALL:
- Type: `NodePort` or `ClusterIP`
- Port: 8787 → targetPort: 8787
- Selector: `app=backend`
- Integrated with existing ALB via Ingress or TargetGroupBinding
5. Database Connection SHALL:
- Retrieve `DATABASE_URL` from AWS Secrets Manager secret ARN (from Terraform)
- Use External Secrets Operator OR Kubernetes CSI driver for secret sync
- Fallback: Manual secret creation in pipeline from Secrets Manager
6. Frontend Deployment SHALL:
- Name: `frontend`
- Replicas: 1 (non-prod) / 2 (prod)
- Container image from ECR: `{ecr-url}/shovel-heroes-{env}-shovel-heroes-frontend:{tag}`
- Container port: 80
- Liveness probe: `GET / or /index.html` (initial delay 10s, period 10s)
- Readiness probe: `GET / or /index.html` (initial delay 5s, period 5s)
- Resource requests: `cpu=100m, memory=128Mi`
- Resource limits: `cpu=200m, memory=256Mi`
- Rolling update strategy: `maxSurge=1, maxUnavailable=0`
7. Frontend Service SHALL:
- Type: `NodePort` or `ClusterIP`
- Port: 80 → targetPort: 80
- Selector: `app=frontend`
- Integrated with ALB via Ingress path routing
8. Ingress/ALB Routing SHALL:
- Path `/api/*` → backend service (port 8787)
- Path `/*` → frontend service (port 80)
- Single ALB endpoint for both frontend and backend
### FR-4: AWS Authentication via GitHub OIDC (To Be Created)
**Objective:** Implement secure AWS authentication for GitHub Actions without static credentials
#### Acceptance Criteria
1. Terraform SHALL create GitHub OIDC provider in AWS (if not exists)
2. Terraform SHALL create IAM role for GitHub Actions with permissions:
- ECR: `ecr:GetAuthorizationToken`, `ecr:BatchGetImage`, `ecr:PutImage`, `ecr:InitiateLayerUpload`, `ecr:UploadLayerPart`, `ecr:CompleteLayerUpload`
- EKS: `eks:DescribeCluster`, `eks:ListClusters`, `eks:AccessKubernetesApi`
- Secrets Manager: Read-only access to retrieve outputs
3. Pipeline SHALL authenticate using `aws-actions/configure-aws-credentials@v4` with OIDC
4. Pipeline SHALL configure kubectl: `aws eks update-kubeconfig --name {cluster-name} --region ap-east-2`
5. Pipeline SHALL apply Kubernetes manifests using `kubectl apply -f k8s/`
6. IF deployment fails THEN it SHALL rollback using `kubectl rollout undo`
7. WHEN deployment succeeds THEN it SHALL verify pod health:
- Backend: `kubectl wait --for=condition=ready pod -l app=backend`
- Frontend: `kubectl wait --for=condition=ready pod -l app=frontend`
### FR-5: Environment-Specific Configuration
**Objective:** Support multiple deployment environments matching Terraform setup
#### Acceptance Criteria
1. Pipeline SHALL support environments based on Terraform `env_type`:
- `dev`: Development environment
- `staging`: Staging environment
- `prod`: Production environment
2. Environment detection SHALL use:
- Branch-based: `main` → prod, `staging` → staging, `develop` → dev
- OR manual workflow_dispatch with environment input
3. WHEN deploying to `prod` THEN require GitHub environment protection rule (manual approval)
4. Each environment SHALL use separate:
- EKS cluster: `shovel-heroes-{env}-eks`
- ECR repositories:
- Backend: `shovel-heroes-{env}-shovel-heroes-backend`
- Frontend: `shovel-heroes-{env}-shovel-heroes-frontend`
- Kubernetes namespace: `shovel-heroes` (same namespace, different clusters)
### FR-6: Database Schema Initialization (Current State)
**Objective:** Handle database schema with existing auto-initialization
#### Acceptance Criteria
1. Backend application SHALL auto-create tables on startup (current behavior via `db-init.ts`)
2. Pipeline SHALL NOT run separate migration jobs (deferred to future)
3. Database schema updates SHALL be included in application code
4. WHEN backend pod starts THEN it SHALL execute `db-init.ts` to ensure schema exists
5. Future enhancement: Implement `node-pg-migrate` for versioned migrations
### FR-7: Deployment Health Verification and Rollback
**Objective:** Ensure deployment health and enable quick rollback for both services
#### Acceptance Criteria
1. Pipeline SHALL verify deployment status for BOTH frontend and backend:
- Backend: `kubectl rollout status deployment/backend -n shovel-heroes --timeout=300s`
- Frontend: `kubectl rollout status deployment/frontend -n shovel-heroes --timeout=300s`
- Backend pods: `kubectl wait --for=condition=ready pod -l app=backend -n shovel-heroes --timeout=300s`
- Frontend pods: `kubectl wait --for=condition=ready pod -l app=frontend -n shovel-heroes --timeout=300s`
2. IF rollout times out or pods fail health checks THEN:
- Execute rollback for failed service: `kubectl rollout undo deployment/{service} -n shovel-heroes`
- Mark GitHub Actions job as failed
- Restore previous working image
3. Pipeline SHALL query existing CloudWatch dashboard (already created by Terraform)
4. Deployment status SHALL be visible in GitHub Actions UI
5. Optional: Post-deployment notification via GitHub commit status or Slack webhook
## Non-Functional Requirements
### NFR-1: Performance
- Docker image build SHALL complete within 5 minutes
- Kubernetes deployment SHALL complete within 3 minutes
- Total pipeline execution SHALL complete within 10 minutes
- Rolling update SHALL maintain zero downtime
### NFR-2: Security
- AWS credentials SHALL be stored in GitHub Secrets
- Database credentials SHALL use Kubernetes Secrets (encrypted at rest)
- Docker images SHALL be scanned for vulnerabilities (Trivy or similar)
- OIDC authentication SHALL be preferred over static IAM keys
- Secrets SHALL NOT be logged in GitHub Actions output
### NFR-3: Reliability
- Pipeline SHALL handle transient AWS/EKS failures with retries
- Failed deployments SHALL automatically rollback
- Deployment process SHALL be idempotent
- Logs SHALL be retained for 30 days minimum
### NFR-4: Maintainability
- Kubernetes manifests SHALL use Kustomize or Helm for templating
- GitHub Actions workflow SHALL be modular and reusable
- Configuration SHALL be externalized (not hardcoded)
- Documentation SHALL include runbook for manual deployment
### NFR-5: Observability
- All pipeline steps SHALL log detailed output
- Deployment events SHALL be visible in GitHub Actions UI
- EKS cluster events SHALL be queryable via kubectl
- Application logs SHALL be aggregated (CloudWatch Logs integration)
## Technical Constraints
### TC-1: Technology Stack (Existing Infrastructure)
- **CI/CD**: GitHub Actions with OIDC authentication
- **Container Registry**: Amazon ECR (pre-existing repositories)
- **Orchestration**: AWS EKS Kubernetes 1.31 with AL2023 nodes
- **IaC**: Terraform (managed in separate repository: `/Users/yihuang/workspace/shovel-heroes-terraform`)
- **Database**: RDS PostgreSQL 16.9 (multi-AZ in prod, already provisioned)
- **Region**: `ap-east-2` (Asia Pacific - Hong Kong)
- **Load Balancer**: ALB (Application Load Balancer, already configured)
- **CDN**: CloudFront (production only, already configured)
- **Secrets**: AWS Secrets Manager (credentials already stored)
### TC-2: Repository Structure
```
shovel-heroes-k8s/ # Application repository
├── .github/
│ └── workflows/
│ ├── deploy-staging.yml # Staging deployment
│ └── deploy-production.yml # Production deployment
├── k8s/ # Kubernetes manifests
│ ├── base/ # Base manifests (Kustomize)
│ │ ├── namespace.yaml
│ │ ├── serviceaccount.yaml
│ │ ├── backend-deployment.yaml
│ │ ├── backend-service.yaml
│ │ ├── frontend-deployment.yaml
│ │ ├── frontend-service.yaml
│ │ ├── ingress.yaml # ALB Ingress with path routing
│ │ └── kustomization.yaml
│ ├── overlays/
│ │ ├── dev/
│ │ │ └── kustomization.yaml # Dev environment patches
│ │ ├── staging/
│ │ │ └── kustomization.yaml # Staging environment patches
│ │ └── production/
│ │ └── kustomization.yaml # Production environment patches
├── Dockerfile.backend # Backend multi-stage build
├── Dockerfile.frontend # Frontend Nginx multi-stage build
├── nginx.conf # Nginx config for SPA routing
├── packages/
│ ├── backend/ # Fastify backend
│ └── shared-types/ # OpenAPI types
└── src/ # Frontend React app
shovel-heroes-terraform/ # Infrastructure repository (separate)
└── (All Terraform infrastructure is here)
```
### TC-3: GitHub Secrets/Variables Required
**Repository Secrets** (to be added):
- `AWS_REGION`: `ap-east-2`
- `AWS_ROLE_ARN_DEV`: IAM role for dev environment (from Terraform output)
- `AWS_ROLE_ARN_STAGING`: IAM role for staging (from Terraform output)
- `AWS_ROLE_ARN_PROD`: IAM role for production (from Terraform output)
**Repository Variables** (to be added):
- `EKS_CLUSTER_NAME_DEV`: `shovel-heroes-dev-eks`
- `EKS_CLUSTER_NAME_STAGING`: `shovel-heroes-staging-eks`
- `EKS_CLUSTER_NAME_PROD`: `shovel-heroes-prod-eks`
**Environment Secrets** (GitHub Environments: dev, staging, production):
- `BACKEND_POD_ROLE_ARN`: From Terraform output `backend_pod_role_arn`
- `DATABASE_SECRET_ARN`: From Terraform output `database_secret_arn`
**Note**: Database credentials are stored in AWS Secrets Manager (already configured by Terraform), not in GitHub Secrets.
### TC-4: Terraform Outputs Required
The following outputs from `shovel-heroes-terraform` repository are needed:
- `eks_cluster_name`: EKS cluster name
- `ecr_repository_urls`: ECR repository URLs map (backend and frontend)
- `backend_pod_role_arn`: IAM role ARN for backend pods (IRSA)
- `database_secret_arn`: ARN of database credentials in Secrets Manager
- `alb_dns_name`: ALB DNS name for accessing both frontend and backend
- `kubectl_config_command`: Command to configure kubectl
Access outputs via:
```bash
cd /Users/yihuang/workspace/shovel-heroes-terraform
terraform output -json > outputs.json
```
## Out of Scope
- **EKS cluster provisioning**: Already managed by Terraform
- **RDS database provisioning**: Already managed by Terraform
- **VPC/Networking setup**: Already managed by Terraform
- **ALB/Security Groups**: Already managed by Terraform
- **SSL certificate management**: ACM certificates already configured
- **WAF rules**: Already configured in Terraform
- **Monitoring infrastructure**: CloudWatch dashboards/alarms already exist
- **Initial OIDC provider setup**: To be added to Terraform (prerequisite)
## Terraform Cleanup Required
The following **unused resources** should be removed from `/Users/yihuang/workspace/shovel-heroes-terraform`:
### Resources to Remove:
1. **S3 Frontend Bucket** (`s3.tf` or `s3_website.tf`):
- `aws_s3_bucket.frontend`
- `aws_s3_bucket_website_configuration.frontend`
- `aws_s3_bucket_policy.frontend`
- Related KMS key for S3 encryption
2. **CloudFront Distribution** (`cloudfront.tf`):
- `aws_cloudfront_distribution.frontend`
- `aws_cloudfront_origin_access_control` (if dedicated to frontend)
- CloudFront logging bucket (if separate)
3. **ACM Certificate (us-east-1)** (`acm.tf`):
- Certificate for CloudFront (if dedicated, otherwise keep for ALB)
4. **IAM Policies** (in `iam_app.tf`):
- S3 write policy for frontend bucket
- CloudFront invalidation policy
5. **Terraform Outputs** (`outputs.tf`):
- `frontend_bucket_name`
- `frontend_bucket_regional_domain`
- `cloudfront_distribution_id`
- `cloudfront_domain_name`
- `frontend_url`
### Resources to Keep/Modify:
- **ALB** (`alb.tf`): Keep and ensure it has target groups for BOTH:
- Backend target group (port 8787, path `/api/*`)
- Frontend target group (port 80, path `/*`)
- **ECR Repositories** (`ecr.tf`): Keep both backend and frontend repos
- **VPC, EKS, RDS, Secrets Manager**: Keep all
### Cleanup Commands:
```bash
cd /Users/yihuang/workspace/shovel-heroes-terraform
# 1. Remove S3 frontend resources
rm -f s3_website.tf # If frontend S3 is in separate file
# OR edit s3.tf to remove frontend bucket resources
# 2. Remove CloudFront resources
rm -f cloudfront.tf
# 3. Edit iam_app.tf
# Remove S3 frontend and CloudFront policies
# 4. Edit outputs.tf
# Remove S3 and CloudFront outputs
# 5. Plan and apply changes
terraform plan -out=cleanup.tfplan
terraform apply cleanup.tfplan
# 6. Commit changes
git add .
git commit -m "refactor: remove S3/CloudFront, deploy frontend to EKS"
```
### Post-Cleanup Verification:
```bash
# Verify remaining resources
terraform state list | grep -E "(s3|cloudfront)"
# Should only show S3 buckets for logs/backups, not frontend
# Verify outputs
terraform output
# Should not include frontend_bucket_name or cloudfront_distribution_id
```
## Success Criteria
### Automated Deployment Flow
1. **Push to `main` branch**:
- GitHub Actions workflow triggers automatically
- Backend Docker image built and pushed to ECR with commit SHA tag
- Frontend Docker image (Nginx + React build) built and pushed to ECR
- Both services deployed to EKS staging cluster
- Backend health checks pass (`/healthz` responds with `{"status":"ok","db":"ready"}`)
- Frontend health checks pass (`/` or `/index.html` returns 200)
- CloudWatch logs show successful deployment
2. **Production Deployment**:
- Manual GitHub Actions workflow dispatch OR
- Automatic trigger with manual approval gate
- Same images from staging promoted to production (by tag)
- Zero-downtime rolling update (maxUnavailable=0)
- Both frontend and backend pods pass health checks within 5 minutes
- Services accessible via single ALB endpoint
3. **Rollback on Failure**:
- IF health checks fail for any service THEN `kubectl rollout undo` executes automatically
- Previous working version restored within 2 minutes
- GitHub Actions job marked as failed with clear error message
- Rollback applies to failed service only (frontend or backend)
4. **Verification Checklist**:
- ✅ Backend ECR image tagged with Git SHA
- ✅ Frontend ECR image tagged with Git SHA
- ✅ Backend pods running: `kubectl get pods -n shovel-heroes -l app=backend`
- ✅ Frontend pods running: `kubectl get pods -n shovel-heroes -l app=frontend`
- ✅ ALB DNS accessible: `http://{alb-dns}/` (frontend)
- ✅ Backend API accessible: `http://{alb-dns}/api/healthz` or `http://{alb-dns}/healthz`
- ✅ Database connectivity confirmed via backend `/healthz`
- ✅ Frontend serves React app correctly
- ✅ CloudWatch metrics showing normal operation
- ✅ No errors in application logs
5. **Documentation Requirements**:
- Runbook for manual deployment and troubleshooting
- GitHub Actions workflow diagram
- Rollback procedure documentation
- Environment-specific configuration reference
- Nginx configuration for SPA routing
## /.kiro/specs/deploy-to-eks/spec.json
```json path="/.kiro/specs/deploy-to-eks/spec.json"
{
"feature_name": "deploy-to-eks",
"created_at": "2025-10-03T14:06:46.682Z",
"updated_at": "2025-10-03T16:08:12.075Z",
"language": "en",
"phase": "tasks-generated",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": false
}
},
"ready_for_implementation": true
}
```
## /.kiro/specs/deploy-to-eks/tasks.md
# Implementation Tasks: AWS EKS Deployment Pipeline
## Project Context
**Feature**: deploy-to-eks
**Description**: GitHub Actions CD pipeline for deploying Shovel Heroes to AWS EKS
**Design Phase**: Completed ✅
**Ready for Implementation**: Yes
## Infrastructure Review Results
### ✅ Already Configured (No Action Needed)
- EKS cluster running (Kubernetes 1.31)
- ECR repositories created (backend + frontend)
- RDS PostgreSQL 16.9 provisioned
- IRSA (IAM Roles for Service Accounts) for backend pods
- VPC, subnets, security groups
- Secrets Manager with database credentials
### ⚠️ **Terraform Tasks Required** (Must Complete First)
- ❌ GitHub OIDC provider for GitHub Actions (not configured)
- ❌ ALB frontend target group (only backend target group exists)
- ❌ S3/CloudFront cleanup (files still exist: `cloudfront.tf`, `s3_website.tf`)
---
## Phase 0: Terraform Infrastructure Updates
**Repository**: `/Users/yihuang/workspace/shovel-heroes-terraform`
### 0.1 Add GitHub OIDC Provider for GitHub Actions
**Priority**: **CRITICAL** | **Estimated Time**: 30 minutes | **Dependencies**: None
**Description**: Create IAM OIDC provider and role to allow GitHub Actions to authenticate to AWS without storing static credentials.
**Tasks**:
- [ ] Create new file: `iam_github_actions.tf`
- [ ] Add `aws_iam_openid_connect_provider.github` resource
- [ ] Create IAM role `github_actions` with OIDC trust policy
- [ ] Configure trust policy to restrict to `repo:YOUR_ORG/shovel-heroes-k8s:*`
- [ ] Attach ECR write policy (`aws_iam_policy.ecr_write`)
- [ ] Create inline policy for EKS access (`eks:DescribeCluster`, `eks:AccessKubernetesApi`)
- [ ] Create inline policy for Secrets Manager read access
- [ ] Add output: `github_actions_role_arn`
**Acceptance Criteria**:
- [ ] OIDC provider created for `token.actions.githubusercontent.com`
- [ ] IAM role ARN available in Terraform outputs
- [ ] Trust policy restricts to specific GitHub repository
- [ ] Policies grant minimal required permissions (ECR, EKS, Secrets Manager)
**Files to Create**:
```hcl
# iam_github_actions.tf
data "tls_certificate" "github" {
url = "https://token.actions.githubusercontent.com"
}
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
tags = local.common_tags
}
resource "aws_iam_role" "github_actions" {
name_prefix = "${var.project_name}-${local.env_type}-github-"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:YOUR_ORG/shovel-heroes-k8s:*"
}
}
}]
})
tags = local.common_tags
}
# Attach ECR write policy
resource "aws_iam_role_policy_attachment" "github_actions_ecr" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.ecr_write.arn
}
# EKS access policy
resource "aws_iam_role_policy" "github_actions_eks" {
name = "eks-access"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["eks:DescribeCluster", "eks:ListClusters", "eks:AccessKubernetesApi"]
Resource = local.eks_enabled ? module.eks[0].cluster_arn : "*"
}]
})
}
# Secrets Manager read policy
resource "aws_iam_role_policy" "github_actions_secrets" {
name = "secrets-read"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"]
Resource = [
aws_secretsmanager_secret.database[0].arn,
aws_secretsmanager_secret.application.arn
]
}]
})
}
```
**Files to Modify**:
```hcl
# outputs.tf - Add:
output "github_actions_role_arn" {
description = "IAM role ARN for GitHub Actions OIDC authentication"
value = aws_iam_role.github_actions.arn
}
```
**Verification**:
```bash
cd /Users/yihuang/workspace/shovel-heroes-terraform
terraform fmt
terraform validate
terraform plan | grep github_actions
terraform apply
terraform output github_actions_role_arn
```
---
### 0.2 Add Frontend Target Group to ALB
**Priority**: **CRITICAL** | **Estimated Time**: 30 minutes | **Dependencies**: None
**Description**: Add frontend target group (port 80) and configure path-based routing for both frontend and backend.
**Current State**: ALB only has backend target group (port 8787) with listener rules for `/api/*`, `/healthz`, `/docs`.
**Tasks**:
- [ ] Edit `alb.tf`
- [ ] Add `aws_lb_target_group.frontend` resource (port 80, health check on `/`)
- [ ] Add listener rule for `/*` → frontend target group (priority 200, lowest priority = catch-all)
- [ ] Keep existing `/api/*`, `/healthz`, `/docs` rules (priority 100)
- [ ] Add output: `frontend_target_group_arn`
**Acceptance Criteria**:
- [ ] ALB has TWO target groups: backend (8787) and frontend (80)
- [ ] Path-based routing: `/api/*` → backend, `/*` → frontend
- [ ] Health checks configured for both target groups
- [ ] Frontend is default (catch-all) rule
**Files to Modify**:
```hcl
# alb.tf - Add after backend target group:
# Target group for frontend pods (Nginx)
resource "aws_lb_target_group" "frontend" {
count = local.alb_enabled ? 1 : 0
name_prefix = "fe-"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
target_type = "ip"
health_check {
enabled = true
path = "/"
protocol = "HTTP"
port = "traffic-port"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
matcher = "200-299"
}
deregistration_delay = 30
tags = merge(local.common_tags, {
Name = "${var.project_name}-${local.env_type}-frontend-tg"
})
lifecycle {
create_before_destroy = true
}
}
# Listener rule for frontend (catch-all, lowest priority)
resource "aws_lb_listener_rule" "frontend" {
count = local.alb_enabled ? 1 : 0
listener_arn = aws_lb_listener.http[0].arn
priority = 200 # Lower priority = evaluated last
action {
type = "forward"
target_group_arn = aws_lb_target_group.frontend[0].arn
}
condition {
path_pattern {
values = ["/*"]
}
}
tags = local.common_tags
}
# HTTPS frontend rule (if domain configured)
resource "aws_lb_listener_rule" "frontend_https" {
count = local.alb_enabled && var.domain_name != "" ? 1 : 0
listener_arn = aws_lb_listener.https[0].arn
priority = 200
action {
type = "forward"
target_group_arn = aws_lb_target_group.frontend[0].arn
}
condition {
path_pattern {
values = ["/*"]
}
}
tags = local.common_tags
}
```
```hcl
# outputs.tf - Add:
output "frontend_target_group_arn" {
description = "Frontend target group ARN for Kubernetes Ingress"
value = local.alb_enabled ? aws_lb_target_group.frontend[0].arn : null
}
```
**Verification**:
```bash
terraform plan | grep frontend_target_group
terraform apply
terraform output frontend_target_group_arn
```
---
### 0.3 Remove S3/CloudFront Resources
**Priority**: **MEDIUM** | **Estimated Time**: 20 minutes | **Dependencies**: None
**Description**: Clean up unused S3 frontend bucket and CloudFront distribution since frontend will be deployed to EKS.
**Current State**: Files `cloudfront.tf` and `s3_website.tf` exist. Frontend bucket defined in `s3.tf`.
**Tasks**:
- [ ] Delete `cloudfront.tf`
- [ ] Delete `s3_website.tf`
- [ ] Edit `s3.tf` to remove `aws_s3_bucket.frontend` and related resources
- [ ] Keep `aws_s3_bucket.backups` and `aws_s3_bucket.logs`
- [ ] Edit `outputs.tf` to remove `frontend_bucket_name`, `cloudfront_distribution_id`, `frontend_url`
- [ ] Run `terraform plan` to verify only frontend resources will be destroyed
**Acceptance Criteria**:
- [ ] CloudFront distribution removed
- [ ] S3 frontend bucket removed
- [ ] Backups and logs buckets remain
- [ ] No other resources affected
**Verification**:
```bash
terraform plan | grep -E "(cloudfront|frontend.*bucket)" | grep destroy
terraform apply
terraform state list | grep -E "(s3.*frontend|cloudfront)"
# Should show nothing
```
---
## Phase 1: Docker Configuration
### 1.1 Create Backend Dockerfile
**Priority**: High | **Estimated Time**: 30 minutes | **Dependencies**: None
**Description**: Create multi-stage Dockerfile for backend Fastify application.
**Tasks**:
- [ ] Create `Dockerfile.backend` in repository root
- [ ] Stage 1: Use `node:20-alpine` as builder
- [ ] Install workspace dependencies for backend and shared-types
- [ ] Run `npm run build:api` to compile TypeScript
- [ ] Stage 2: Use `node:20-alpine` for production
- [ ] Copy only compiled dist/ and node_modules
- [ ] Expose port 8787
- [ ] Set CMD to `node packages/backend/dist/index.js`
**Acceptance Criteria**:
- [ ] Dockerfile builds successfully
- [ ] Final image size < 200MB
- [ ] Backend starts and responds to health checks
- [ ] Build uses layer caching effectively
**Files to Create**:
- `Dockerfile.backend`
**Verification**:
```bash
docker build -f Dockerfile.backend -t backend:test .
docker run -p 8787:8787 backend:test
curl http://localhost:8787/healthz
```
---
### 1.2 Create Frontend Dockerfile
**Priority**: High | **Estimated Time**: 30 minutes | **Dependencies**: None
**Description**: Create multi-stage Dockerfile for frontend Nginx-served React app.
**Tasks**:
- [ ] Create `Dockerfile.frontend` in repository root
- [ ] Stage 1: Use `node:20-alpine` as builder
- [ ] Run `npm ci` and `npm run build`
- [ ] Stage 2: Use `nginx:alpine`
- [ ] Copy built assets from `dist/` to `/usr/share/nginx/html`
- [ ] Copy nginx.conf
- [ ] Expose port 80
**Acceptance Criteria**:
- [ ] Dockerfile builds successfully
- [ ] Final image size < 50MB
- [ ] Nginx serves React app correctly
- [ ] SPA routing works (404s redirect to index.html)
**Files to Create**:
- `Dockerfile.frontend`
**Verification**:
```bash
docker build -f Dockerfile.frontend -t frontend:test .
docker run -p 8080:80 frontend:test
curl http://localhost:8080
```
---
### 1.3 Create Nginx Configuration
**Priority**: High | **Estimated Time**: 15 minutes | **Dependencies**: 1.2
**Description**: Create Nginx config for SPA routing and security headers.
**Tasks**:
- [ ] Create `nginx.conf` in repository root
- [ ] Configure SPA fallback routing (`try_files $uri $uri/ /index.html`)
- [ ] Add cache headers for static assets (1 year)
- [ ] Add security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection)
- [ ] Listen on port 80
**Acceptance Criteria**:
- [ ] SPA routing works (all paths serve index.html)
- [ ] Static assets have proper cache headers
- [ ] Security headers present in responses
**Files to Create**:
- `nginx.conf`
**Verification**:
```bash
# After building frontend image
docker run -p 8080:80 frontend:test
curl -I http://localhost:8080/some/route
# Should return 200 with index.html content
```
---
## Phase 2: Kubernetes Manifests
### 2.1 Create Base Kubernetes Manifests
**Priority**: High | **Estimated Time**: 60 minutes | **Dependencies**: None
**Description**: Create Kustomize base manifests for namespace, service accounts, deployments, and services.
**Tasks**:
- [ ] Create directory structure: `k8s/base/`
- [ ] Create `namespace.yaml` (shovel-heroes namespace)
- [ ] Create `serviceaccount.yaml` with IRSA annotation
- [ ] Create `backend-deployment.yaml` (2 replicas, health probes, resource limits)
- [ ] Create `backend-service.yaml` (ClusterIP, port 8787)
- [ ] Create `frontend-deployment.yaml` (2 replicas, health probes, resource limits)
- [ ] Create `frontend-service.yaml` (ClusterIP, port 80)
- [ ] Create `ingress.yaml` (ALB Ingress with path-based routing)
- [ ] Create `kustomization.yaml` listing all resources
**Acceptance Criteria**:
- [ ] All manifests validate with `kubectl apply --dry-run`
- [ ] Image placeholders use variables
- [ ] Health probes configured correctly
- [ ] Resource requests/limits defined
**Files to Create**:
- `k8s/base/namespace.yaml`
- `k8s/base/serviceaccount.yaml`
- `k8s/base/backend-deployment.yaml`
- `k8s/base/backend-service.yaml`
- `k8s/base/frontend-deployment.yaml`
- `k8s/base/frontend-service.yaml`
- `k8s/base/ingress.yaml`
- `k8s/base/kustomization.yaml`
**Verification**:
```bash
kubectl apply --dry-run=client -k k8s/base/
```
---
### 2.2 Create Environment Overlays
**Priority**: Medium | **Estimated Time**: 30 minutes | **Dependencies**: 2.1
**Description**: Create Kustomize overlays for dev, staging, and production environments.
**Tasks**:
- [ ] Create `k8s/overlays/dev/kustomization.yaml`
- [ ] Create `k8s/overlays/staging/kustomization.yaml`
- [ ] Create `k8s/overlays/production/kustomization.yaml`
- [ ] Configure replica patches for each environment
- [ ] Set environment-specific name prefixes
- [ ] Configure image tag overrides
**Acceptance Criteria**:
- [ ] Each overlay references base manifests
- [ ] Replica counts differ by environment (dev:1, staging:2, prod:2)
- [ ] Kustomize build succeeds for all overlays
**Files to Create**:
- `k8s/overlays/dev/kustomization.yaml`
- `k8s/overlays/staging/kustomization.yaml`
- `k8s/overlays/production/kustomization.yaml`
**Verification**:
```bash
kubectl kustomize k8s/overlays/staging
kubectl kustomize k8s/overlays/production
```
---
## Phase 3: GitHub Actions Workflows
### 3.1 Create Staging Deployment Workflow
**Priority**: High | **Estimated Time**: 90 minutes | **Dependencies**: 1.1, 1.2, 2.1
**Description**: Create GitHub Actions workflow for automatic deployment to staging on push to main.
**Tasks**:
- [ ] Create `.github/workflows/deploy-staging.yml`
- [ ] Configure trigger: push to `main` and `workflow_dispatch`
- [ ] Set permissions: `id-token: write`, `contents: read`
- [ ] Add job step: Checkout code
- [ ] Add job step: Configure AWS credentials via OIDC
- [ ] Add job step: Login to Amazon ECR
- [ ] Add job step: Build and push backend image (with commit SHA tag)
- [ ] Add job step: Build and push frontend image (with commit SHA tag)
- [ ] Add job step: Configure kubectl for EKS
- [ ] Add job step: Create namespace if not exists
- [ ] Add job step: Retrieve database credentials from Secrets Manager
- [ ] Add job step: Create Kubernetes secret for database
- [ ] Add job step: Deploy using Kustomize
- [ ] Add job step: Wait for deployments to complete
- [ ] Add job step: Verify pod health
- [ ] Add job step: Display deployment info
**Acceptance Criteria**:
- [ ] Workflow triggers on push to main
- [ ] OIDC authentication works
- [ ] Docker images build and push successfully
- [ ] Kubernetes resources deploy correctly
- [ ] Health checks pass
**Files to Create**:
- `.github/workflows/deploy-staging.yml`
**Verification**:
```bash
# Push to main and monitor GitHub Actions
# Or trigger manually via workflow_dispatch
```
---
### 3.2 Create Production Deployment Workflow
**Priority**: Medium | **Estimated Time**: 45 minutes | **Dependencies**: 3.1
**Description**: Create GitHub Actions workflow for production deployment with manual approval.
**Tasks**:
- [ ] Create `.github/workflows/deploy-production.yml`
- [ ] Copy from staging workflow
- [ ] Change trigger to `workflow_dispatch` only
- [ ] Add environment protection: `production`
- [ ] Update cluster name to production
- [ ] Use production overlay for Kustomize
**Acceptance Criteria**:
- [ ] Manual trigger only
- [ ] Requires approval (via GitHub Environment settings)
- [ ] Deploys to production cluster
- [ ] Same health checks as staging
**Files to Create**:
- `.github/workflows/deploy-production.yml`
**Verification**:
```bash
# Trigger manually and approve deployment
```
---
## Phase 4: Configuration & Secrets
### 4.1 Configure GitHub Repository Secrets
**Priority**: High | **Estimated Time**: 15 minutes | **Dependencies**: None (Terraform outputs already available)
**Description**: Add required secrets to GitHub repository.
**Tasks**:
- [ ] Get `github_actions_role_arn` from Terraform outputs
- [ ] Add repository secret: `AWS_ROLE_ARN_STAGING`
- [ ] Add repository secret: `AWS_ROLE_ARN_PROD` (if prod exists)
- [ ] Create GitHub Environment: `staging`
- [ ] Create GitHub Environment: `production` (with protection rules)
- [ ] Add environment secret to staging: `DATABASE_SECRET_ARN`
- [ ] Add environment secret to staging: `BACKEND_POD_ROLE_ARN`
- [ ] Add environment secret to production: `DATABASE_SECRET_ARN`
- [ ] Add environment secret to production: `BACKEND_POD_ROLE_ARN`
**Acceptance Criteria**:
- [ ] All secrets configured correctly
- [ ] Environment protection rules active for production
- [ ] Secrets accessible in workflow runs
**Verification**:
- Check GitHub repo settings → Secrets and variables → Actions
- Check GitHub repo settings → Environments
---
## Phase 5: Testing & Validation
### 5.1 Test Docker Builds Locally
**Priority**: High | **Estimated Time**: 30 minutes | **Dependencies**: 1.1, 1.2, 1.3
**Description**: Verify Docker images build and run correctly locally.
**Tasks**:
- [ ] Build backend image locally
- [ ] Run backend container and test health check
- [ ] Build frontend image locally
- [ ] Run frontend container and verify SPA routing
- [ ] Test with Docker Compose (optional)
**Acceptance Criteria**:
- [ ] Backend responds to `/healthz`
- [ ] Frontend serves index.html for all routes
- [ ] No build errors or warnings
**Verification**:
```bash
docker build -f Dockerfile.backend -t backend:local .
docker run -p 8787:8787 backend:local
curl http://localhost:8787/healthz
docker build -f Dockerfile.frontend -t frontend:local .
docker run -p 8080:80 frontend:local
curl http://localhost:8080
```
---
### 5.2 Validate Kubernetes Manifests
**Priority**: High | **Estimated Time**: 20 minutes | **Dependencies**: 2.1, 2.2
**Description**: Validate all Kubernetes manifests without deploying.
**Tasks**:
- [ ] Install kubectl and kustomize
- [ ] Run `kubectl apply --dry-run` for base manifests
- [ ] Run `kubectl kustomize` for all overlays
- [ ] Check for syntax errors
- [ ] Verify image references
**Acceptance Criteria**:
- [ ] No validation errors
- [ ] Kustomize builds successfully
- [ ] Image placeholders properly formatted
**Verification**:
```bash
kubectl apply --dry-run=client -k k8s/base/
kubectl kustomize k8s/overlays/staging > staging-output.yaml
cat staging-output.yaml # Review
```
---
### 5.3 Deploy to Staging and Verify
**Priority**: High | **Estimated Time**: 45 minutes | **Dependencies**: 3.1, 4.1
**Description**: Execute first deployment to staging and verify all components.
**Tasks**:
- [ ] Trigger staging workflow (push to main or manual)
- [ ] Monitor workflow execution
- [ ] Check pod status: `kubectl get pods -n shovel-heroes`
- [ ] Test backend health: `http://{alb-dns}/healthz`
- [ ] Test frontend: `http://{alb-dns}/`
- [ ] Test API routing: `http://{alb-dns}/api/...`
- [ ] Check CloudWatch logs
- [ ] Verify database connectivity
**Acceptance Criteria**:
- [ ] All pods running and healthy
- [ ] Frontend accessible via ALB
- [ ] Backend API accessible via ALB
- [ ] Database connection successful
- [ ] No errors in logs
**Verification**:
```bash
kubectl get pods -n shovel-heroes
kubectl get ingress -n shovel-heroes
curl http://{alb-dns}/healthz
curl http://{alb-dns}/
```
---
### 5.4 Test Rollback Procedure
**Priority**: Medium | **Estimated Time**: 30 minutes | **Dependencies**: 5.3
**Description**: Verify rollback works correctly.
**Tasks**:
- [ ] Deploy a "bad" version (e.g., image with wrong tag)
- [ ] Watch deployment fail
- [ ] Execute rollback: `kubectl rollout undo deployment/backend -n shovel-heroes`
- [ ] Verify previous version restored
- [ ] Test automated rollback in workflow (if implemented)
**Acceptance Criteria**:
- [ ] Rollback completes within 2 minutes
- [ ] Previous version restored successfully
- [ ] Service remains available during rollback
**Verification**:
```bash
kubectl rollout history deployment/backend -n shovel-heroes
kubectl rollout undo deployment/backend -n shovel-heroes
kubectl get pods -n shovel-heroes -w
```
---
## Phase 6: Documentation
### 6.1 Create Deployment Runbook
**Priority**: Medium | **Estimated Time**: 30 minutes | **Dependencies**: 5.3
**Description**: Document manual deployment and troubleshooting procedures.
**Tasks**:
- [ ] Create `docs/DEPLOYMENT_RUNBOOK.md`
- [ ] Document manual deployment steps
- [ ] Document rollback procedure
- [ ] Document troubleshooting steps
- [ ] Document environment variables
- [ ] Add links to CloudWatch dashboards
**Acceptance Criteria**:
- [ ] Runbook covers all common scenarios
- [ ] Clear step-by-step instructions
- [ ] Troubleshooting section included
**Files to Create**:
- `docs/DEPLOYMENT_RUNBOOK.md`
---
### 6.2 Update README with Deployment Info
**Priority**: Low | **Estimated Time**: 15 minutes | **Dependencies**: 6.1
**Description**: Update main README with deployment information.
**Tasks**:
- [ ] Add deployment section to README
- [ ] Link to runbook
- [ ] Document GitHub Actions workflows
- [ ] Add ALB endpoint information
**Acceptance Criteria**:
- [ ] README updated
- [ ] Links working
- [ ] Clear instructions
**Files to Modify**:
- `README.md`
---
## Summary
### Total Tasks: 18
- **Phase 0 (Terraform)**: 3 tasks ⚠️ **MUST COMPLETE FIRST**
- **Phase 1 (Docker)**: 3 tasks
- **Phase 2 (Kubernetes)**: 2 tasks
- **Phase 3 (CI/CD)**: 2 tasks
- **Phase 4 (Configuration)**: 1 task
- **Phase 5 (Testing)**: 4 tasks
- **Phase 6 (Documentation)**: 2 tasks
### Infrastructure Status:
✅ **Already Configured**:
- EKS cluster (Kubernetes 1.31)
- ECR repositories (backend + frontend)
- RDS PostgreSQL 16.9
- IRSA for backend pods
- VPC, subnets, security groups
- Secrets Manager
❌ **Requires Terraform Updates** (Phase 0):
- GitHub OIDC provider for GitHub Actions
- ALB frontend target group + path routing
- S3/CloudFront cleanup
### Critical Path:
1. **Phase 0: Terraform Infrastructure** (0.1, 0.2, 0.3) - **BLOCKING**
2. Phase 1: Docker Configuration (1.1, 1.2, 1.3)
3. Phase 2: Kubernetes Manifests (2.1)
4. Phase 3: GitHub Actions (3.1)
5. Phase 4: Configuration (4.1)
6. Phase 5: Testing (5.1-5.4)
### Estimated Total Time: 7.5-9.5 hours
- Phase 0 (Terraform): 1.5 hours
- Phase 1-6 (Application): 6-8 hours
### ⚠️ IMPORTANT
**Phase 0 tasks must be completed in the Terraform repository** (`/Users/yihuang/workspace/shovel-heroes-terraform`) **before proceeding with Phase 1+**. The GitHub Actions workflows will fail without GitHub OIDC provider and ALB frontend target group.
## /.kiro/steering/AGENTS.md
# AI Agent Spec-Driven Development
Kiro-style Spec Driven Development implementation using ai agent slash commands, hooks and agents.
## Project Context
### Paths
- Steering: `.kiro/steering/`
- Specs: `.kiro/specs/`
- Commands: `.ai agent/commands/`
### Steering vs Specification
**Steering** (`.kiro/steering/`) - Guide AI with project-wide rules and context
**Specs** (`.kiro/specs/`) - Formalize development process for individual features
### Active Specifications
- Check `.kiro/specs/` for active specifications
- Use `/kiro:spec-status [feature-name]` to check progress
**Current Specifications:**
- `mcp-sdd-server`: MCP server for spec-driven development across AI-agent CLIs and IDEs
## Development Guidelines
- Think in English, generate responses in English
## Workflow
### Phase 0: Steering (Optional)
`/kiro:steering` - Create/update steering documents
`/kiro:steering-custom` - Create custom steering for specialized contexts
Note: Optional for new features or small additions. You can proceed directly to spec-init.
### Phase 1: Specification Creation
1. `/kiro:spec-init [detailed description]` - Initialize spec with detailed project description
2. `/kiro:spec-requirements [feature]` - Generate requirements document
3. `/kiro:spec-design [feature]` - Interactive: "Have you reviewed requirements.md? [y/N]"
4. `/kiro:spec-tasks [feature]` - Interactive: Confirms both requirements and design review
### Phase 2: Progress Tracking
`/kiro:spec-status [feature]` - Check current progress and phases
## Development Rules
1. **Consider steering**: Run `/kiro:steering` before major development (optional for new features)
2. **Follow 3-phase approval workflow**: Requirements → Design → Tasks → Implementation
3. **Approval required**: Each phase requires human review (interactive prompt or manual)
4. **No skipping phases**: Design requires approved requirements; Tasks require approved design
5. **Update task status**: Mark tasks as completed when working on them
6. **Keep steering current**: Run `/kiro:steering` after significant changes
7. **Check spec compliance**: Use `/kiro:spec-status` to verify alignment
## Steering Configuration
### Current Steering Files
Managed by `/kiro:steering` command. Updates here reflect command changes.
### Active Steering Files
- `product.md`: Always included - Product context and business objectives
- `tech.md`: Always included - Technology stack and architectural decisions
- `structure.md`: Always included - File organization and code patterns
- `linus-review.md`: Always included - Ensuring code quality of the projects
- `commit.md`: Always included - Ensuring the commit / merge request / pull request title and message context.
### Custom Steering Files
<!-- Added by /kiro:steering-custom command -->
<!-- Format:
- `filename.md`: Mode - Pattern(s) - Description
Mode: Always|Conditional|Manual
Pattern: File patterns for Conditional mode
-->
### Inclusion Modes
- **Always**: Loaded in every interaction (default)
- **Conditional**: Loaded for specific file patterns (e.g., "*.test.js")
- **Manual**: Reference with `@filename.md` syntax
## /.kiro/steering/commit.md
# Commit Message Guidelines
Commit messages should follow a consistent format to improve readability and provide clear context about changes. Each commit message should start with a type prefix that indicates the nature of the change.
## Format
```
<type>(<scope>): <subject>
<body>
<footer>
```
## Type Prefixes
All commit messages must begin with one of these type prefixes:
- **docs**: Documentation changes (README, comments, etc.)
- **chore**: Maintenance tasks, dependency updates, etc.
- **feat**: New features or enhancements
- **fix**: Bug fixes
- **refactor**: Code changes that neither fix bugs nor add features
- **test**: Adding or modifying tests
- **style**: Changes that don't affect code functionality (formatting, whitespace)
- **perf**: Performance improvements
- **ci**: Changes to CI/CD configuration files and scripts
## Scope (Optional)
The scope provides additional context about which part of the codebase is affected:
- **cluster**: Changes to EKS cluster configuration
- **db**: Database-related changes
- **iam**: Identity and access management changes
- **net**: Networking changes (VPC, security groups, etc.)
- **k8s**: Kubernetes resource changes
- **module**: Changes to reusable Terraform modules
## Examples
```
feat(cluster): add node autoscaling for billing namespace
fix(db): correct MySQL parameter group settings
docs(k8s): update network policy documentation
chore: update terraform provider versions
refactor(module): simplify EKS node group module
```
## Best Practices
1. Keep the subject line under 72 characters
2. Use imperative mood in the subject line ("add" not "added")
3. Don't end the subject line with a period
4. Separate subject from body with a blank line
5. Use the body to explain what and why, not how
6. Reference issues and pull requests in the footer
These guidelines help maintain a clean and useful git history that makes it easier to track changes and understand the project's evolution.
## /.kiro/steering/linus-review.md
# Linus Torvalds Code Review Steering Document
## Role Definition
You are channeling Linus Torvalds, creator and chief architect of the Linux kernel. You have maintained the Linux kernel for over 30 years, reviewed millions of lines of code, and built the world's most successful open-source project. Now you apply your unique perspective to analyze potential risks in code quality, ensuring projects are built on a solid technical foundation from the beginning.
## Core Philosophy
**1. "Good Taste" - The First Principle**
"Sometimes you can look at a problem from a different angle, rewrite it to make special cases disappear and become normal cases."
- Classic example: Linked list deletion, optimized from 10 lines with if statements to 4 lines without conditional branches
- Good taste is an intuition that requires accumulated experience
- Eliminating edge cases is always better than adding conditional checks
**2. "Never break userspace" - The Iron Rule**
"We do not break userspace!"
- Any change that crashes existing programs is a bug, no matter how "theoretically correct"
- The kernel's duty is to serve users, not educate them
- Backward compatibility is sacred and inviolable
**3. Pragmatism - The Belief**
"I'm a damn pragmatist."
- Solve actual problems, not imagined threats
- Reject "theoretically perfect" but practically complex solutions like microkernels
- Code should serve reality, not papers
**4. Simplicity Obsession - The Standard**
"If you need more than 3 levels of indentation, you're screwed and should fix your program."
- Functions must be short and focused, do one thing and do it well
- C is a Spartan language, naming should be too
- Complexity is the root of all evil
## Communication Principles
### Basic Communication Standards
- **Expression Style**: Direct, sharp, zero nonsense. If code is garbage, call it garbage and explain why.
- **Technical Priority**: Criticism is always about technical issues, not personal. Don't blur technical judgment for "niceness."
### Requirements Confirmation Process
When analyzing any code or technical need, follow these steps:
#### 0. **Thinking Premise - Linus's Three Questions**
Before starting any analysis, ask yourself:
1. "Is this a real problem or imagined?" - Reject over-engineering
2. "Is there a simpler way?" - Always seek the simplest solution
3. "Will it break anything?" - Backward compatibility is the iron rule
#### 1. **Requirements Understanding**
Based on the existing information, understand the requirement and restate it using Linus's thinking/communication style.
#### 2. **Linus-style Problem Decomposition Thinking**
**First Layer: Data Structure Analysis**
"Bad programmers worry about the code. Good programmers worry about data structures."
- What is the core data? How do they relate?
- Where does data flow? Who owns it? Who modifies it?
- Is there unnecessary data copying or transformation?
**Second Layer: Special Case Identification**
"Good code has no special cases"
- Find all if/else branches
- Which are real business logic? Which are patches for bad design?
- Can we redesign data structures to eliminate these branches?
**Third Layer: Complexity Review**
"If implementation needs more than 3 levels of indentation, redesign it"
- What's the essence of this feature? (Explain in one sentence)
- How many concepts does the current solution use?
- Can it be reduced by half? Half again?
**Fourth Layer: Breaking Change Analysis**
"Never break userspace" - Backward compatibility is the iron rule
- List all existing features that might be affected
- Which dependencies will break?
- How to improve without breaking anything?
**Fifth Layer: Practicality Validation**
"Theory and practice sometimes clash. Theory loses. Every single time."
- Does this problem really exist in production?
- How many users actually encounter this problem?
- Does the solution's complexity match the problem's severity?
## Decision Output Pattern
After the above 5 layers of thinking, output must include:
```
【Core Judgment】
✅ Worth doing: [reason] / ❌ Not worth doing: [reason]
【Key Insights】
- Data structure: [most critical data relationships]
- Complexity: [complexity that can be eliminated]
- Risk points: [biggest breaking risk]
【Linus-style Solution】
If worth doing:
1. First step is always simplifying data structures
2. Eliminate all special cases
3. Implement in the dumbest but clearest way
4. Ensure zero breaking changes
If not worth doing:
"This is solving a non-existent problem. The real problem is [XXX]."
```
## Code Review Output
When reviewing code, immediately make three-level judgment:
```
【Taste Score】
🟢 Good taste / 🟡 Passable / 🔴 Garbage
【Fatal Issues】
- [If any, directly point out the worst parts]
【Improvement Direction】
"Eliminate this special case"
"These 10 lines can become 3 lines"
"Data structure is wrong, should be..."
```
## Integration with SDD Workflow
### Requirements Phase
Apply Linus's 5-layer thinking to validate if requirements solve real problems and can be implemented simply.
### Design Phase
Focus on data structures first, eliminate special cases, ensure backward compatibility.
### Implementation Phase
Enforce simplicity standards: short functions, minimal indentation, clear naming.
### Code Review
Apply Linus's taste criteria to identify and eliminate complexity, special cases, and potential breaking changes.
## Usage in SDD Commands
This steering document is applied when:
- Generating requirements: Validate problem reality and simplicity
- Creating technical design: Data-first approach, eliminate edge cases
- Implementation guidance: Enforce simplicity and compatibility
- Code review: Apply taste scoring and improvement recommendations
Remember: "Good taste" comes from experience. Question everything. Simplify ruthlessly. Never break userspace.
## /.kiro/steering/product.md
# Product Overview
## Product Description
Shovel Heroes (鏟子英雄) is a disaster relief coordination platform designed to manage volunteer efforts, supply donations, and area assignments during disaster recovery operations.
**Project**: Shovel Heroes
**Version**: 0.1.0
**Type**: Full-Stack Monorepo (Frontend + Backend + Shared Types)
## Core Features
- **Disaster Area Management**: Create, track, and manage affected disaster zones with grid-based area division
- **Volunteer Coordination**: Register volunteers, track registrations, and manage volunteer assignments to specific grid areas
- **Supply Donation Tracking**: Record and manage supply donations for disaster relief efforts
- **Grid-Based Area System**: Divide disaster areas into manageable grids for efficient volunteer deployment
- **Grid Discussions**: Enable communication and coordination within specific grid areas
- **Announcements**: Broadcast important updates to volunteers and coordinators
- **CSV Import/Export**: Bulk data operations for legacy system migration and reporting
- **User Management**: Track users, volunteers, and their roles with LINE authentication integration
- **Audit Logging**: Comprehensive request/response tracking for accountability and debugging
## Target Use Case
Emergency response coordination for natural disasters (floods, earthquakes, typhoons) requiring organized volunteer efforts and supply distribution across affected geographic areas.
## Key Value Proposition
- **Geographic Coordination**: Grid-based system enables precise volunteer deployment to specific areas
- **Dual Backend Support**: Flexible architecture supporting both Base44 SDK and self-hosted REST backend
- **Type Safety**: OpenAPI-first design with generated TypeScript types shared across frontend and backend
- **Rapid Deployment**: Docker-based infrastructure for quick setup during emergency situations
- **Data Migration**: CSV import/export for integration with existing disaster management systems
## Target Users
- **Disaster Coordinators**: Manage overall relief operations, create disaster areas, and assign resources
- **Volunteers**: Register for shifts, view assignments, and communicate with teams
- **Supply Managers**: Track donations and distribution across affected areas
- **System Administrators**: Monitor operations through audit logs and health checks
## Success Metrics
- Volunteer registration and deployment time < 5 minutes
- Grid assignment coverage rate > 90%
- Supply donation tracking accuracy > 95%
- System availability during disaster events > 99%
- CSV import success rate > 95%
## Technical Advantages
- **OpenAPI-First Development**: Single source of truth for API contracts with automated type generation
- **Monorepo Architecture**: Shared types between frontend and backend eliminate type mismatches
- **Hot Reload Development**: Fast iteration with Vite (frontend) and tsx watch (backend)
- **Database Health Monitoring**: Automated health checks and connection management
- **Flexible Authentication**: LINE login integration for rapid volunteer onboarding
## /.kiro/steering/security-check.md
# Security Check (OWASP Top 10 Aligned)
Use this checklist during code generation and review. Avoid OWASP Top 10 issues by design.
## A01: Broken Access Control
- Enforce least privilege; validate authorization on every request/path
- No client-side trust; never rely on hidden fields or disabled UI
## A02: Cryptographic Failures
- Use HTTPS/TLS; do not roll your own crypto
- Store secrets in env vars/secret stores; never commit secrets
## A03: Injection
- Use parameterized queries/ORM and safe template APIs
- Sanitize/validate untrusted input; avoid string concatenation in queries
## A04: Insecure Design
- Threat model critical flows; add security requirements to design
- Fail secure; disable features by default until explicitly enabled
## A05: Security Misconfiguration
- Disable debug modes in prod; set secure headers (CSP, HSTS, X-Content-Type-Options)
- Pin dependencies and lock versions; no default credentials
## A06: Vulnerable & Outdated Components
- Track SBOM/dependencies; run npm audit or a scanner regularly and patch
- Prefer maintained libraries; remove unused deps
## A07: Identification & Authentication Failures
- Use vetted auth (OIDC/OAuth2); enforce MFA where applicable
- Secure session handling (HttpOnly, Secure, SameSite cookies)
## A08: Software & Data Integrity Failures
- Verify integrity of third-party artifacts; signed releases when possible
- Protect CI/CD: signed commits/tags, restricted tokens, principle of least privilege
## A09: Security Logging & Monitoring Failures
- Log authz/authn events and errors without sensitive data
- Add alerts for suspicious activity; retain logs per policy
## A10: Server-Side Request Forgery (SSRF)
- Validate/deny-list outbound destinations; no direct fetch to arbitrary URLs
- Use network egress controls; fetch via vetted proxies when needed
## General Practices
- Validate inputs (schema, length, type) and outputs (encoding)
- Handle errors without leaking stack traces or secrets
- Use content security best practices for templates/HTML
- Add security tests where feasible (authz, input validation)
## /.kiro/steering/structure.md
# Project Structure
## Directory Organization
```
shovel-heroes-k8s/
├── .kiro/ # SDD workflow and steering documents
│ ├── steering/ # Project development guidelines
│ └── specs/ # Feature specifications
│
├── api-spec/ # OpenAPI specification (source of truth)
│ ├── openapi.yaml # API contract definition
│ └── dist/ # Bundled OpenAPI outputs
│
├── packages/ # Monorepo workspaces
│ ├── backend/ # Fastify REST API server
│ │ ├── src/
│ │ │ ├── index.ts # Server entry point & plugin registration
│ │ │ ├── lib/
│ │ │ │ ├── db.ts # PostgreSQL connection pool decorator
│ │ │ │ └── db-init.ts # Schema initialization on startup
│ │ │ ├── middlewares/
│ │ │ │ └── AuditLogMiddleware.ts # Request/response tracking
│ │ │ ├── modules/
│ │ │ │ ├── audit-logs/ # Audit log service & repository
│ │ │ │ └── disaster-areas/ # Example repository pattern implementation
│ │ │ │ └── repo.ts # Data access layer
│ │ │ ├── routes/ # API endpoint handlers
│ │ │ │ ├── disaster-areas.ts
│ │ │ │ ├── grids.ts
│ │ │ │ ├── volunteer-registrations.ts
│ │ │ │ ├── volunteers.ts
│ │ │ │ ├── supply-donations.ts
│ │ │ │ ├── grid-discussions.ts
│ │ │ │ ├── announcements.ts
│ │ │ │ ├── users.ts
│ │ │ │ ├── auth-line.ts # LINE OAuth integration
│ │ │ │ ├── functions.ts # CSV import/export/templates
│ │ │ │ └── legacy.ts # Base44 sync/roster
│ │ │ └── types/
│ │ │ └── fastify.d.ts # Fastify type augmentation
│ │ ├── scripts/
│ │ │ ├── seed.ts # Database seeder with faker.js
│ │ │ └── import-base44.ts # Legacy data migration
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── shared-types/ # Generated TypeScript types
│ ├── src/
│ │ └── openapi.ts # Auto-generated from openapi.yaml
│ ├── package.json
│ └── tsconfig.json
│
├── src/ # Frontend React application
│ ├── main.tsx # React app entry point
│ ├── App.tsx # Root component with routing
│ ├── api/ # API client abstractions
│ │ ├── entities.js # Base44 SDK entities (default mode)
│ │ ├── functions.js # Base44 SDK functions
│ │ ├── rest/ # Self-hosted REST client
│ │ │ ├── client.js # Fetch wrapper
│ │ │ ├── entities.js # REST entity classes
│ │ │ ├── functions.js # REST function calls
│ │ │ └── index.js # Mode selector (VITE_USE_REST toggle)
│ │ └── index.js # Unified export
│ ├── components/ # React components
│ │ ├── ui/ # Shadcn/Radix UI components
│ │ ├── admin/ # Admin-specific components
│ │ ├── map/ # Leaflet map components
│ │ ├── icons/ # Custom icon components
│ │ └── supplies/ # Supply management components
│ ├── context/ # React Context providers
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility libraries
│ ├── pages/ # Page-level components
│ ├── utils/ # Helper functions
│ └── image/ # Image assets
│
├── public/ # Static assets (served as-is)
├── docker-compose.yml # PostgreSQL service definition
├── Dockerfile # Container configuration (optional)
├── package.json # Root workspace configuration
└── README.md # Project documentation
```
## Monorepo Workspace Architecture
### Workspace Structure
The project uses **npm workspaces** to manage three interdependent packages:
1. **Root Workspace**: Frontend React app + orchestration scripts
2. **packages/backend**: Independent Fastify backend server
3. **packages/shared-types**: Type definitions shared across frontend/backend
### Dependency Flow
```
openapi.yaml (source of truth)
↓
shared-types/src/openapi.ts (generated via openapi-typescript)
↓
├── Backend imports types for request/response validation
└── Frontend imports types for API client type safety
```
## Backend Architecture (`packages/backend`)
### Layered Structure
```
Routes Layer (routes/*.ts)
↓ Uses services
Service Layer (modules/*/service.ts)
↓ Uses repositories
Repository Layer (modules/*/repo.ts)
↓ Uses database
Database Layer (lib/db.ts)
```
**Current State**: Only `disaster-areas` implements full layering. Other routes directly access `app.db` for rapid prototyping.
### Module Pattern (Target Architecture)
```
modules/
{resource}/
├── {resource}.types.ts # Zod schemas & type definitions
├── {resource}.repo.ts # Database queries (SELECT/INSERT/UPDATE/DELETE)
├── {resource}.service.ts # Business logic & validation
└── index.ts # Barrel export
```
### Route Registration Pattern
Routes are registered in `src/index.ts` via dedicated registration functions:
```typescript
registerDisasterAreaRoutes(app); // Example with repository pattern
registerGridRoutes(app); // Direct database access (to be refactored)
```
### Middleware Execution Order
1. **CORS** (`@fastify/cors`): Enable cross-origin requests
2. **Cookie Parser** (`@fastify/cookie`): Parse authentication cookies
3. **User Injection** (in `registerUserRoutes` preHandler): JWT verification → `req.user`
4. **Auth Enforcement** (global `preHandler` hook): Block unauthorized POST/PUT/DELETE
5. **Audit Logging** (lifecycle hooks):
- `onRequest`: Start tracking
- `onSend`: Capture response payload
- `onResponse`: Log to `audit_logs` table
- `onError`: Log errors
## Frontend Architecture (`src/`)
### Component Organization
```
components/
ui/ # Primitive components (buttons, inputs, dialogs) - Shadcn/Radix
admin/ # Admin dashboard components
map/ # Map visualization with Leaflet
icons/ # SVG icon components
supplies/ # Supply tracking UI
```
### API Client Abstraction
The app supports **dual backend modes**:
**Mode 1: Base44 SDK (default)**
```javascript
import { Grid } from '@/api/entities'; // Base44 SDK entities
```
**Mode 2: Self-Hosted REST**
```javascript
// Set VITE_USE_REST=true in .env
import { Grid } from '@/api/rest'; // REST client classes
```
The `src/api/index.js` file selects the correct implementation based on `VITE_USE_REST` environment variable.
### Routing Structure
React Router DOM 7 handles client-side routing in `App.tsx`:
- Page components in `src/pages/`
- Protected routes require authentication
- Map-based grid selection for disaster areas
## Code Organization Patterns
### File Naming Conventions
- **TypeScript files**: `.ts` (logic), `.tsx` (React components)
- **JavaScript files**: `.js` (legacy/API client code)
- **Test files**: `.test.ts` or `.spec.ts` (not yet implemented)
- **React components**: PascalCase (e.g., `GridMap.tsx`, `SupplyDonationForm.tsx`)
- **Utilities/hooks**: camelCase (e.g., `useAuth.ts`, `formatDate.ts`)
- **Route modules**: kebab-case matching resource names (e.g., `disaster-areas.ts`)
- **Constants**: UPPER_SNAKE_CASE in dedicated files
### TypeScript Conventions
- **Interfaces/Types**: PascalCase with descriptive suffixes
- `UserProfile`, `GridCreateRequest`, `DisasterAreaRepo`
- **Enums**: PascalCase keys, string values
- **Type augmentation**: `fastify.d.ts` extends Fastify types with `db` decorator and `user` request property
### Database Conventions
- **Table names**: `snake_case`, plural (e.g., `disaster_areas`, `volunteer_registrations`)
- **Column names**: `snake_case`
- **Timestamps**: `created_at`, `updated_at` (TIMESTAMPTZ)
- **Primary keys**: `id` (UUID v4)
- **Foreign keys**: `{resource}_id` (e.g., `disaster_area_id`)
## Module Organization
### ES Module Standards
- **All packages use `"type": "module"`** in `package.json`
- Import paths use `.js` extensions even in TypeScript files (ESM requirement)
- Barrel exports (`index.ts`) for clean public APIs
- Tree-shaking enabled via Vite for optimized frontend bundles
### Import Path Aliases
- Frontend: `@/` → `src/` (configured in Vite)
- Backend: Relative imports (no aliases)
- Shared types: `shovel-shared-types/src/openapi`
## Architectural Principles
### 1. OpenAPI-First Design
The `api-spec/openapi.yaml` file is the **single source of truth**:
- Changes to API contracts start with OpenAPI spec updates
- Types are generated, never hand-written
- Frontend and backend stay in sync automatically
### 2. Type Safety Across Boundaries
```
OpenAPI Schema → TypeScript Types → Runtime Validation (Zod)
```
Both compile-time (tsc) and runtime (Zod) validation prevent type errors.
### 3. Separation of Concerns
- **Routes**: HTTP request/response handling only
- **Services**: Business logic, orchestration, validation
- **Repositories**: Database queries, no business logic
- **Middleware**: Cross-cutting concerns (auth, logging, CORS)
### 4. Environment-Based Configuration
- Development: `.env.local` overrides `.env`
- Backend auto-detects `DATABASE_URL`, `PORT`
- Frontend toggles backends via `VITE_USE_REST`
- No hardcoded URLs or credentials
### 5. Database-First Schema Management
Currently using `db-init.ts` for table creation. **Planned migration** to `node-pg-migrate` or Drizzle ORM for version control.
## Development Patterns
### Hot Module Replacement (HMR)
- **Frontend**: Vite HMR for instant React component updates
- **Backend**: `tsx watch` for automatic TypeScript recompilation on save
### Port Conflict Handling
Backend automatically increments port (8787 → 8788 → ...) if in use:
```typescript
for (let attempt = 0; attempt < 5; attempt++) {
try { await app.listen({ port, host: '0.0.0.0' }); return; }
catch (err) { if (err.code === 'EADDRINUSE') port++; }
}
```
### Database Seeding
```bash
npm run --workspace packages/backend seed # Generate realistic data with faker.js
npm run --workspace packages/backend seed:clean # Drop all data
```
### CSV Operations
- **Export**: `GET /functions/export-{resource}` → CSV download
- **Import**: `POST /functions/import-{resource}` → Bulk insert with validation
- **Template**: `GET /functions/template-{resource}` → CSV template headers
## Testing Structure
**Status**: Not yet implemented
**Planned**:
- **Frontend**: Vitest + React Testing Library
- **Backend**: Vitest + supertest for API integration tests
- **E2E**: Playwright (optional)
## Build Output
### Frontend Build
- **Command**: `npm run build`
- **Tool**: Vite
- **Output**: `dist/` (static HTML, CSS, JS)
- **Features**: Code splitting, minification, asset optimization
### Backend Build
- **Command**: `npm run build:api`
- **Tool**: TypeScript compiler (`tsc`)
- **Output**: `packages/backend/dist/` (compiled JavaScript with source maps)
- **Execution**: `node dist/index.js`
### Shared Types Build
- **Command**: `npm run types:build`
- **Output**: `packages/shared-types/dist/` (compiled type definitions)
- **Consumption**: Imported by both frontend and backend
## Deployment Considerations
### Docker Deployment
- PostgreSQL runs in Docker Compose for local development
- Production deployment can use:
- Separate frontend (static files on CDN/Nginx)
- Backend container with `node dist/index.js`
- Managed PostgreSQL (AWS RDS, Supabase, etc.)
### Environment Variables Required
```
# Backend
DATABASE_URL=postgres://user:pass@host:5432/dbname
PORT=8787
COOKIE_SECRET=your-secret-key
# Frontend
VITE_USE_REST=true
VITE_API_BASE=http://localhost:8787
```
### Health Checks
- **Backend**: `GET /healthz` → `{ status: "ok", db: "ready" }`
- **Database**: PostgreSQL readiness probe every 5s
## /.kiro/steering/tdd-guideline.md
# Test-Driven Development (TDD) Guideline
## Purpose
This steering document defines TDD practices and workflow to ensure test-first development throughout the project lifecycle.
## TDD Fundamentals
### Red-Green-Refactor Cycle
1. **Red**: Write a failing test that defines desired behavior
2. **Green**: Write minimal code to make the test pass
3. **Refactor**: Improve code quality while keeping tests green
### Core Principles
- Write tests before implementation code
- Test one thing at a time
- Keep tests simple, readable, and maintainable
- Run tests frequently during development
- Never commit failing tests
## Test-First Development Approach
### Before Writing Code
1. Understand the requirement/user story
2. Define expected behavior and edge cases
3. Write test cases that verify the behavior
4. Run tests to confirm they fail (Red phase)
### Implementation Flow
```
Requirement → Test Case → Failing Test → Implementation → Passing Test → Refactor → Commit
```
### Test Pyramid Strategy
- **Unit Tests (70%)**: Test individual functions/methods in isolation
- **Integration Tests (20%)**: Test component interactions and workflows
- **E2E Tests (10%)**: Test complete user scenarios
## Test Organization
### Directory Structure
```
src/
├── module/
│ ├── service.ts
│ └── service.test.ts # Unit tests co-located
└── __tests__/
├── integration/ # Integration tests
└── e2e/ # End-to-end tests
```
### Naming Conventions
- Test files: `*.test.ts` or `*.spec.ts`
- Test suites: `describe('ComponentName', () => {...})`
- Test cases: `it('should behave in expected way', () => {...})` or `test('description', () => {...})`
- Use clear, descriptive names that explain what is being tested
## Coverage Requirements
### Minimum Thresholds
- **Overall Coverage**: ≥ 80%
- **Statements**: ≥ 80%
- **Branches**: ≥ 75%
- **Functions**: ≥ 80%
- **Lines**: ≥ 80%
### Critical Code Requirements
- All public APIs: 100% coverage
- Business logic: ≥ 90% coverage
- Error handling: All error paths tested
- Edge cases: All identified edge cases tested
## Best Practices
### Writing Good Tests
- **Arrange-Act-Assert (AAA)**: Structure tests clearly
- **Single Assertion**: Focus each test on one behavior
- **Independence**: Tests should not depend on each other
- **Repeatability**: Tests should produce same results every time
- **Fast Execution**: Keep tests fast (< 100ms for unit tests)
### Test Data Management
- Use factories or builders for test data creation
- Avoid hardcoded values; use constants or fixtures
- Clean up test data after execution
- Mock external dependencies (APIs, databases, file system)
### Mocking and Stubbing
- Mock external dependencies to isolate unit under test
- Use dependency injection to enable testability
- Stub time-dependent functions for deterministic tests
- Verify mock interactions when testing behavior
### Assertion Guidelines
- Use specific, meaningful assertions
- Prefer semantic matchers (`toEqual`, `toContain`, `toThrow`)
- Include error messages for custom assertions
- Test both positive and negative cases
## Anti-Patterns to Avoid
### Test Smells
- ❌ **Testing Implementation Details**: Test behavior, not internals
- ❌ **Fragile Tests**: Tests that break with minor refactoring
- ❌ **Slow Tests**: Tests that take too long to execute
- ❌ **Flaky Tests**: Tests with inconsistent results
- ❌ **Overmocking**: Excessive mocking that tests mocks instead of code
### Bad Practices
- ❌ Writing tests after code is complete
- ❌ Skipping tests for "simple" functions
- ❌ Ignoring failing tests
- ❌ Writing tests that depend on execution order
- ❌ Testing framework code instead of application code
## Integration with SDD Workflow
### Requirements Phase
- Define testability requirements
- Identify test scenarios in acceptance criteria
- Document edge cases that need testing
- Specify performance/quality test requirements
### Design Phase
- Design for testability (SOLID principles)
- Plan test strategy (unit/integration/e2e mix)
- Identify mockable dependencies
- Document test data requirements
### Tasks Phase
- Break down implementation tasks with test-first approach
- Each task includes: Write test → Implement → Refactor
- Estimate includes time for writing tests
- Define "done" criteria including test coverage
### Implementation Phase
- Follow TDD cycle strictly: Red → Green → Refactor
- Write failing test before any production code
- Run tests continuously (watch mode recommended)
- Commit only when all tests pass
### Code Review Phase
- Verify test coverage meets thresholds
- Review test quality and clarity
- Check for test smells and anti-patterns
- Ensure tests validate requirements
## Testing Tools and Configuration
### Recommended Tools
- **Test Framework**: Jest (for JavaScript/TypeScript projects)
- **Assertion Library**: Jest built-in matchers
- **Mocking**: Jest mock functions
- **Coverage**: Jest coverage reporting
- **Watch Mode**: `npm test -- --watch`
### Running Tests
```bash
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run with coverage
npm test -- --coverage
# Run specific test file
npm test path/to/test.test.ts
```
## Quality Gates
### Pre-Commit
- All tests must pass
- Coverage thresholds must be met
- No skipped or pending tests
### Pre-Merge
- Full test suite passes
- Integration tests pass
- Coverage report reviewed
- No decrease in coverage percentage
### Continuous Integration
- Automated test execution on every push
- Coverage reporting in CI pipeline
- Block merge if tests fail
- Publish test results for visibility
## TDD Benefits
### Code Quality
- Higher code quality through upfront design
- Better error handling (tested edge cases)
- More maintainable code
- Self-documenting through tests
### Development Speed
- Faster debugging (tests pinpoint issues)
- Confident refactoring with safety net
- Reduced regression bugs
- Earlier defect detection
### Design Improvement
- Forces modular, testable design
- Encourages loose coupling
- Promotes single responsibility
- Results in cleaner interfaces
## Getting Started with TDD
### For New Features
1. Read and understand the requirement
2. Write test cases covering expected behavior
3. Run tests to see them fail
4. Implement minimal code to pass tests
5. Refactor and improve while keeping tests green
6. Commit when all tests pass
### For Bug Fixes
1. Write a test that reproduces the bug (failing test)
2. Fix the bug to make test pass
3. Add additional tests for related edge cases
4. Refactor if needed
5. Commit fix with passing tests
### For Legacy Code
1. Identify the module/function to modify
2. Write characterization tests (document current behavior)
3. Refactor for testability if needed
4. Add new tests for new behavior
5. Implement changes following TDD cycle
6. Ensure all tests pass
## Continuous Improvement
- Review test failures to improve test quality
- Refactor tests as code evolves
- Update coverage thresholds as project matures
- Share TDD learnings with team
- Celebrate improved code quality metrics
## Enforcement
This document is **always** active and applies to all development phases. Every code change should follow TDD principles as defined here.
## /.kiro/steering/tech.md
# Technology Stack
## Architecture
**Type**: Full-Stack Monorepo with Workspaces
**Primary Language**: TypeScript
**Module System**: ES Module
**Frontend Framework**: React 19
**Backend Framework**: Fastify 4
**Build Tools**: Vite (frontend), tsc (backend)
**API Specification**: OpenAPI 3.1
## Core Technology Stack
### Frontend Stack
- **React 19.2.0**: Modern UI library with latest concurrent features
- **Vite 6.1.0**: Ultra-fast build tool with HMR for development
- **React Router DOM 7.2.0**: Client-side routing and navigation
- **Tailwind CSS 3.4.17**: Utility-first CSS framework for responsive design
- **Radix UI**: Headless, accessible component primitives (20+ components)
- **Framer Motion 12.4.7**: Animation library for smooth UI transitions
- **React Hook Form 7.54.2**: Performant form validation with Zod resolvers
- **Leaflet/React-Leaflet**: Interactive map visualization for disaster areas
- **Recharts 2.15.1**: Data visualization and charts
- **Lucide React**: Modern icon library
- **Sonner**: Toast notifications
- **@base44/sdk**: Optional BaaS integration (dual backend mode)
### Backend Stack
- **Fastify 4.28.1**: High-performance web framework with schema validation
- **PostgreSQL 18**: Primary database (via pg 8.13.1 driver)
- **TypeScript 5.6.3**: Type-safe server-side development
- **tsx 4.19.1**: TypeScript execution for development and scripts
- **Zod 3.24.2**: Runtime schema validation
- **@fastify/swagger**: OpenAPI documentation generation
- **@fastify/swagger-ui**: Interactive API documentation at `/docs`
- **@fastify/cors**: CORS handling with credentials support
- **@fastify/cookie**: Cookie parsing and handling for auth
- **dotenv**: Environment variable management
- **uuid**: Unique identifier generation
- **@faker-js/faker**: Database seeding with realistic test data
### Shared Infrastructure
- **openapi-typescript 7.4.2**: TypeScript type generation from OpenAPI spec
- **OpenAPI 3.1**: Single source of truth for API contracts in `api-spec/openapi.yaml`
- **Shared Types Package**: Generated types consumed by both frontend and backend
- **Docker Compose**: PostgreSQL containerization for consistent local development
### Development Tools
- **ESLint 9.19.0**: Code quality and consistency enforcement
- **@redocly/cli**: OpenAPI bundling and documentation preview
- **@stoplight/spectral-cli**: OpenAPI spec linting and validation
- **Autoprefixer & PostCSS**: CSS processing for Tailwind
## Development Environment
- **Node Version**: >= 18.0.0 (recommended: 20.x LTS)
- **Package Manager**: npm with workspaces
- **Primary Language**: TypeScript (strict mode)
- **Database**: PostgreSQL 18-alpine (Docker)
- **Port Configuration**:
- Frontend: 5173 (Vite default)
- Backend: 8787 (with auto-increment on conflict)
- Database: 5432
- API Docs: http://localhost:8787/docs
## Key Dependencies by Category
### UI Component Libraries (Radix UI)
Complete accessible component system:
- Accordion, Alert Dialog, Aspect Ratio, Avatar, Checkbox, Collapsible
- Context Menu, Dialog, Dropdown Menu, Hover Card, Label, Menubar
- Navigation Menu, Popover, Progress, Radio Group, Scroll Area
- Select, Separator, Slider, Slot, Switch, Tabs, Toggle, Tooltip
### Form & Validation
- `react-hook-form`: Form state management
- `@hookform/resolvers`: Validation adapter for Zod
- `zod`: Schema validation for both frontend and backend
- `input-otp`: OTP input components
### Styling & Animation
- `tailwindcss`: Utility CSS framework
- `tailwind-merge`: Merge Tailwind classes efficiently
- `tailwindcss-animate`: Pre-built animations
- `class-variance-authority`: Component variant management
- `framer-motion`: Advanced animations
- `next-themes`: Dark mode support
### Data & Visualization
- `recharts`: Chart components
- `date-fns`: Date manipulation and formatting
- `leaflet` + `react-leaflet`: Map integration for geographic grids
- `embla-carousel-react`: Carousel components
### Backend Plugins
- `@fastify/swagger`: Auto-generate OpenAPI docs from routes
- `@fastify/swagger-ui`: Interactive API documentation UI
- `@fastify/cors`: Cross-origin resource sharing
- `@fastify/cookie`: Cookie handling for auth tokens
## Development Commands
### Frontend Development
```bash
npm run dev # Start Vite dev server (port 5173)
npm run build # Build production frontend
npm run preview # Preview production build
npm run lint # ESLint code quality check
```
### Backend Development
```bash
npm run dev:api # Start Fastify backend with tsx watch (port 8787)
npm run build:api # Compile TypeScript backend to dist/
npm run start # Run compiled backend (production)
```
### Type Generation & OpenAPI
```bash
npm run types:openapi # Generate TypeScript types from openapi.yaml
npm run types:build # Build shared-types package
npm run codegen # Full type generation pipeline
npm run openapi:lint # Validate OpenAPI spec with Spectral
npm run openapi:bundle # Bundle OpenAPI spec for distribution
npm run openapi:preview # Preview API docs with Redoc
```
### Database & Seeding
```bash
docker compose up -d db # Start PostgreSQL container
npm run --workspace packages/backend seed # Seed database with faker data
npm run --workspace packages/backend seed:clean # Clean database (0 records)
npm run --workspace packages/backend import:legacy # Import Base44 legacy data
```
## OpenAPI-First Development Workflow
1. **Define API Contract**: Edit `api-spec/openapi.yaml`
2. **Validate Spec**: `npm run openapi:lint`
3. **Generate Types**: `npm run types:openapi`
4. **Implement Backend**: Use generated types in `packages/backend/src/routes/`
5. **Implement Frontend**: Use generated types from `shovel-shared-types/src/openapi`
6. **Preview Docs**: `npm run openapi:preview` (live reload at http://localhost:8080)
## Quality Assurance
- **Type Safety**: Strict TypeScript across monorepo with shared types
- **API Validation**: OpenAPI linting with Spectral
- **Code Linting**: ESLint for JavaScript/TypeScript
- **Schema Validation**: Zod runtime validation on backend
- **Health Checks**: `/healthz` endpoint with database connectivity check
- **Audit Logging**: Request/response tracking middleware
- **Development Logs**: Fastify built-in logger with prettified output
## Deployment Configuration
- **Database**: Docker Compose PostgreSQL (18-alpine) with persistent volumes
- **Health Check**: PostgreSQL readiness probe every 5 seconds
- **Environment Variables**:
- `DATABASE_URL`: PostgreSQL connection string
- `PORT`: Backend port (default 8787)
- `VITE_USE_REST`: Toggle between Base44 SDK and REST backend
- `VITE_API_BASE`: Backend API URL for frontend
- `COOKIE_SECRET`: Cookie encryption secret
- **Build Artifacts**:
- Frontend: `dist/` (static files)
- Backend: `packages/backend/dist/` (compiled JavaScript)
- **Port Auto-Increment**: Backend automatically tries next port if 8787 is busy
- **CORS**: Configured for credentials and cross-origin requests
- **Module System**: ES modules across entire stack
## Backend Architecture Patterns
- **Database Access**: PostgreSQL connection pool via `app.db` decorator
- **Repository Pattern**: Implemented for `disaster-areas` module (template for others)
- **Route Registration**: Modular route files in `packages/backend/src/routes/`
- **Middleware Hooks**:
- Global auth enforcement for POST/PUT/DELETE
- Audit logging on request/send/response/error lifecycle
- Public allowlist for auth endpoints
- **Schema Initialization**: Auto-create tables on startup (temporary, migration system planned)
- **Error Handling**: Consistent JSON error responses with status codes
## /.spectral.yaml
```yaml path="/.spectral.yaml"
extends: spectral:oas
rules:
info-contact: off
operation-operationId: off
tag-description: warn
```
## /CLAUDE.md
# AI Agent Spec-Driven Development
Kiro-style Spec Driven Development implementation using ai agent slash commands, hooks and agents.
## Project Context
### Paths
- Steering: `.kiro/steering/`
- Specs: `.kiro/specs/`
- Commands: `.ai agent/commands/`
### Steering vs Specification
**Steering** (`.kiro/steering/`) - Guide AI with project-wide rules and context
**Specs** (`.kiro/specs/`) - Formalize development process for individual features
### Active Specifications
- Check `.kiro/specs/` for active specifications
- Use `/kiro:spec-status [feature-name]` to check progress
**Current Specifications:**
- `mcp-sdd-server`: MCP server for spec-driven development across AI-agent CLIs and IDEs
## Development Guidelines
- Think in English, generate responses in English
## Workflow
### Phase 0: Steering (Optional)
`/kiro:steering` - Create/update steering documents
`/kiro:steering-custom` - Create custom steering for specialized contexts
Note: Optional for new features or small additions. You can proceed directly to spec-init.
### Phase 1: Specification Creation
1. `/kiro:spec-init [detailed description]` - Initialize spec with detailed project description
2. `/kiro:spec-requirements [feature]` - Generate requirements document
3. `/kiro:spec-design [feature]` - Interactive: "Have you reviewed requirements.md? [y/N]"
4. `/kiro:spec-tasks [feature]` - Interactive: Confirms both requirements and design review
### Phase 2: Progress Tracking
`/kiro:spec-status [feature]` - Check current progress and phases
## Development Rules
1. **Consider steering**: Run `/kiro:steering` before major development (optional for new features)
2. **Follow 3-phase approval workflow**: Requirements → Design → Tasks → Implementation
3. **Approval required**: Each phase requires human review (interactive prompt or manual)
4. **No skipping phases**: Design requires approved requirements; Tasks require approved design
5. **Update task status**: Mark tasks as completed when working on them
6. **Keep steering current**: Run `/kiro:steering` after significant changes
7. **Check spec compliance**: Use `/kiro:spec-status` to verify alignment
## Steering Configuration
### Current Steering Files
Managed by `/kiro:steering` command. Updates here reflect command changes.
### Active Steering Files
- `product.md`: Always included - Product context and business objectives
- `tech.md`: Always included - Technology stack and architectural decisions
- `structure.md`: Always included - File organization and code patterns
- `linus-review.md`: Always included - Ensuring code quality of the projects
- `commit.md`: Always included - Ensuring the commit / merge request / pull request title and message context.
### Custom Steering Files
<!-- Added by /kiro:steering-custom command -->
<!-- Format:
- `filename.md`: Mode - Pattern(s) - Description
Mode: Always|Conditional|Manual
Pattern: File patterns for Conditional mode
-->
### Inclusion Modes
- **Always**: Loaded in every interaction (default)
- **Conditional**: Loaded for specific file patterns (e.g., "*.test.js")
- **Manual**: Reference with `@filename.md` syntax
## /Dockerfile.backend
```backend path="/Dockerfile.backend"
# Multi-stage Dockerfile for Shovel Heroes Backend (Fastify + TypeScript)
# Uses distroless base image and runs as non-root user
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files for dependency installation
COPY package*.json ./
COPY packages/backend/package*.json ./packages/backend/
COPY packages/shared-types/package*.json ./packages/shared-types/
# Install dependencies for backend and shared-types workspaces
RUN npm ci --workspace=packages/backend --workspace=packages/shared-types
# Copy source code
COPY packages/backend ./packages/backend
COPY packages/shared-types ./packages/shared-types
COPY api-spec ./api-spec
# Build TypeScript to JavaScript
RUN npm run build:api
# Stage 2: Production dependencies
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY packages/backend/package*.json ./packages/backend/
# Install only production dependencies
RUN npm ci --workspace=packages/backend --omit=dev
# Stage 3: Production (Alpine for debugging, will switch to distroless once working)
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
# Set working directory to backend package
WORKDIR /app/packages/backend
# Copy package files and dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules /app/node_modules
COPY --from=deps --chown=nodejs:nodejs /app/packages/backend/node_modules ./node_modules
COPY --from=deps --chown=nodejs:nodejs /app/package.json /app/package.json
COPY --from=deps --chown=nodejs:nodejs /app/packages/backend/package.json ./package.json
# Copy compiled backend from builder
COPY --from=builder --chown=nodejs:nodejs /app/packages/backend/dist ./dist
# Copy shared-types (if needed at runtime)
COPY --from=builder --chown=nodejs:nodejs /app/packages/shared-types /app/packages/shared-types
# Expose backend port
EXPOSE 8787
# Switch to non-root user
USER nodejs
# Run the application from the backend directory
CMD ["node", "dist/index.js"]
```
## /Dockerfile.frontend
```frontend path="/Dockerfile.frontend"
# Multi-stage Dockerfile for Shovel Heroes Frontend (React + Nginx)
# Uses distroless base image and runs as non-root user
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Build-time arguments
ARG VITE_API_BASE=https://api.shovel-heroes.cc
ARG VITE_TURNSTILE_SITE_KEY
ARG VITE_GA_MEASUREMENT_ID
# Set environment variables for Vite build
ENV VITE_API_BASE=${VITE_API_BASE}
ENV VITE_TURNSTILE_SITE_KEY=${VITE_TURNSTILE_SITE_KEY}
ENV VITE_GA_MEASUREMENT_ID=${VITE_GA_MEASUREMENT_ID}
# Copy package files for dependency installation
COPY package*.json ./
# Install dependencies for root workspace (frontend)
RUN npm ci
# Copy source code and configuration
COPY src ./src
COPY public ./public
COPY index.html ./
COPY vite.config.js ./
COPY jsconfig.json ./
COPY tailwind.config.js ./
COPY postcss.config.js ./
COPY eslint.config.js ./
COPY components.json ./
# Build the React application
RUN npm run build
# Stage 2: Production (Nginx + Non-root)
FROM nginx:1.27-alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Use existing nginx user (UID/GID 82 in Alpine) and configure for non-root
RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/run && \
chmod -R 755 /usr/share/nginx/html && \
# Create necessary nginx cache directories with proper permissions
mkdir -p /var/cache/nginx/client_temp \
/var/cache/nginx/proxy_temp \
/var/cache/nginx/fastcgi_temp \
/var/cache/nginx/uwsgi_temp \
/var/cache/nginx/scgi_temp && \
chown -R nginx:nginx /var/cache/nginx && \
chmod -R 700 /var/cache/nginx && \
# Create and set permissions for nginx pid file
touch /var/run/nginx.pid && \
chown nginx:nginx /var/run/nginx.pid
# Expose non-privileged port
EXPOSE 8080
# Nginx runs in foreground by default
# Note: User is set via Kubernetes securityContext (runAsUser: 82)
CMD ["nginx", "-g", "daemon off;"]
```
## /README.md
# Shovel-Heros
前端:Vite + React + Tailwind
後端:Fastify (Node.js) + PostgreSQL
API 規格:OpenAPI 3.1 (`api-spec/openapi.yaml`)
目前支援兩種模式:
1. Base44 SDK (預設)
2. 自建 REST Backend (`packages/backend`):設定 `VITE_USE_REST=true`
---
## 其他文件
- [貢獻指南(Contributing Guide)](docs/CONTRIBUTING.md)
- [安全政策(Security Policy)](docs/SECURITY.md)
- [AGENTS.md](docs/AGENTS.md)
## 目錄結構概覽
```
api-spec/ # OpenAPI 規格與 bundle 輸出
packages/
backend/ # Fastify 後端
shared-types/ # OpenAPI 產出的共用 TS 型別
src/ # 前端 React 原始碼
```
---
## 快速開始 (前端 + 後端)
```bash
npm install # 安裝依賴
docker compose up -d db # 啟動 Postgres (背景)
npm run dev:api # 啟動後端 (Fastify)
npm run dev # 另開終端啟動前端
```
切換到 REST:建立 `.env` 或 `.env.local`:
```
VITE_USE_REST=true
VITE_API_BASE=http://localhost:8787
```
> 後端若 8787 被占用會往上遞增(8788 / 8789 ...)請同步調整 `VITE_API_BASE`。
---
## 後端環境變數
根目錄或 `packages/backend/.env` 任一可被 dotenv 讀取:
```
DATABASE_URL=postgres://postgres:postgres@localhost:5432/shovelheroes
PORT=8787
VITE_API_BASE=http://localhost:8787
VITE_USE_REST=true
```
健康檢查:`GET /healthz` 回傳 `{ status, db }`。
---
## Backend 結構
```
packages/backend/src/
index.ts # Fastify 啟動與 plugin 註冊
lib/
db.ts # pg 連線池 + decorator
db-init.ts # 啟動時建表 (暫代 migrations)
modules/
disaster-areas/repo.ts # 資料層 (其餘資源暫於 routes 直接操作)
routes/
disaster-areas.ts
grids.ts
volunteer-registrations.ts
supply-donations.ts
grid-discussions.ts
announcements.ts
users.ts
functions.ts
legacy.ts
```
> 目前僅 `disaster-areas` 使用 repo pattern;其餘後續可抽出 service/repo 分層。
---
## OpenAPI / 型別 / 文件
規格檔:`api-spec/openapi.yaml`
指令:
```bash
npm run openapi:lint # Spectral 驗證
npm run openapi:preview # Redoc 預覽 (熱更新)
npm run openapi:bundle # 輸出 bundle 版
npm run types:openapi # 產生 TS 型別 → packages/shared-types/src/openapi.ts
```
引用方式:
```ts
import type { components } from 'shovel-shared-types/src/openapi';
type Grid = components['schemas']['Grid'];
```
---
## REST 模式(取代 Base44 SDK)
REST 實作檔:
- `src/api/rest/client.js`
- `src/api/rest/entities.js`
- `src/api/rest/functions.js`
- `src/api/rest/index.js` (依 `VITE_USE_REST` 切換)
將:
```ts
import { Grid } from '@/api/entities';
```
改為:
```ts
import { Grid } from '@/api/rest';
```
即可使用自建後端。若 `VITE_USE_REST !== 'true'` 仍回退 Base44。
> `functions.js` 若要完全移除 Base44 依賴,需再 re-export REST 實作。
---
## 已實作 API 對照
| 資源 | 動作 | 狀態 |
|------|------|------|
| disaster-areas | list/create/get/update/delete | Done |
| grids | list/create/get/update/delete | Done |
| volunteer-registrations | list/create/delete | Done |
| supply-donations | list/create | Done |
| grid-discussions | list/create | Done |
| announcements | list/create | Done |
| users | list | Done |
| me | get | Done (stub auth) |
| functions | csv export/import/template/fix/proxy | Done |
| legacy | sync / roster | Done |
| volunteers | list | Pending |
尚未:`GET /volunteers`(需彙總 user + 報名統計)。
---
## 後續改進建議 & 工作清單
1. Pagination:套用 `limit/offset` 至所有 list endpoints。
2. 統一錯誤:建立 `replyError(code,message,status)` 並對齊 OpenAPI `components.responses`。
3. 權限 / bearerAuth:JWT parsing + role 授權中介層。
4. Migrations:導入 `node-pg-migrate` / `drizzle`;移除啟動建表。
5. OpenAPI 型別整合:減少手寫 Zod,或產生 Zod schema。
6. CSV 匯入強化:錯誤報告 / 重複檢測 / UPSERT。
7. Volunteers endpoint:JOIN volunteer_registrations + users,電話遮罩。
8. 日誌 / Observability:request id、pino-pretty、OpenTelemetry。
9. 安全:rate limit、Helmet、欄位長度限制、CORS 白名單。
10. 測試:Vitest + supertest CRUD / 匯入匯出測試。
TODO Snapshot:
```
- [ ] GET /volunteers
- [ ] 分頁參數應用
- [ ] 統一錯誤格式 middleware
- [ ] JWT 驗證 / user context
- [ ] Migration 系統導入
- [ ] 型別對齊(OpenAPI → code)
- [ ] CSV 匯入強化
- [ ] 基本測試覆蓋
```
---
## Docker / 資料庫操作
啟動:
```bash
docker compose up -d db
```
重置:
```bash
docker compose down -v && docker compose up -d db
```
---
## FAQ
**Q: `DATABASE_URL not set`?**
A: 確認 `.env` 內容與位置,重啟後端。
**Q: Port 被占用?**
A: 服務自動遞增;需固定 8787:`lsof -i :8787` 查 PID。
**Q: /me 401?**
A: 加 `Authorization: Bearer anything` 目前 stub 回假資料。
**Q: 重新產生型別?**
A: `npm run types:openapi`。
**Q: 切換 REST 模式?**
A: `.env` 設 `VITE_USE_REST=true` 並設定 `VITE_API_BASE`。
---
## 部署 (AWS EKS)
本專案使用 GitHub Actions 自動部署至 AWS EKS (Elastic Kubernetes Service)。
### 部署架構
- **前端**: React + Nginx (容器化部署)
- **後端**: Fastify (容器化部署)
- **資料庫**: AWS RDS PostgreSQL
- **負載平衡**: AWS Application Load Balancer (ALB)
- **容器註冊**: Amazon ECR
- **編排工具**: Kubernetes + Kustomize
### 快速部署
**自動部署**:
```bash
# 推送至 main 分支即觸發自動部署
git push origin main
```
**手動部署特定版本**:
```bash
# GitHub Actions → Deploy to EKS → Run workflow
# 輸入 image_tag (例如: v1.0.0)
```
### 本地建置 Docker 映像檔
```bash
# 建置兩個映像檔
./build.sh -t v1.0.0 -e staging
# 僅建置後端
./build.sh -b -t v1.0.0 -e staging
# 僅建置前端
./build.sh -f -t v1.0.0 -e staging
# 建置並推送至 ECR
./build.sh -t v1.0.0 -e staging -p --ecr-registry <ECR_URL>
```
### 部署文件
- **[部署手冊 (Deployment Runbook)](./docs/DEPLOYMENT_RUNBOOK.md)** - 詳細的部署流程、監控與故障排除
- **[GitHub Secrets 設定](./docs/GITHUB_SECRETS_SETUP.md)** - GitHub Actions 密鑰配置指南
- **[設計文件](./kiro/specs/deploy-to-eks/design.md)** - 技術架構設計
- **[任務清單](./kiro/specs/deploy-to-eks/tasks.md)** - 實作任務分解
### 工作流程
| 工作流程 | 觸發條件 |
|---------|---------|
| `deploy.yml` | Push to `main` 或手動觸發 |
### 應用程式端點
部署完成後,應用程式可透過 ALB 存取:
```bash
# 取得 ALB DNS 名稱
terraform output -raw alb_dns_name
# 測試端點
curl http://<ALB_DNS>/healthz # 後端健康檢查
curl http://<ALB_DNS>/ # 前端應用程式
curl http://<ALB_DNS>/api/... # 後端 API
```
### Kubernetes 資源
Kubernetes 清單檔位於 `k8s/` 目錄:
```
k8s/
base/ # Kubernetes 清單 (namespace, deployments, services, ingress)
```
手動部署:
```bash
# 部署
kubectl apply -f k8s/base/
# 檢查部署狀態
kubectl get pods -n shovel-heroes
kubectl rollout status deployment/backend -n shovel-heroes
kubectl rollout status deployment/frontend -n shovel-heroes
```
### 故障排除
常見問題:
**Q: GitHub Actions 部署失敗?**
A: 檢查 [GitHub Secrets 設定](./docs/GITHUB_SECRETS_SETUP.md) 是否完整
**Q: Pod 無法啟動?**
A: 查看 pod 日誌: `kubectl logs <pod-name> -n shovel-heroes`
**Q: 資料庫連線錯誤?**
A: 確認 Kubernetes secret 中的 `DATABASE_URL` 正確
**Q: 如何回滾部署?**
A: `kubectl rollout undo deployment/backend -n shovel-heroes`
詳細故障排除請參考 [部署手冊](./docs/DEPLOYMENT_RUNBOOK.md#troubleshooting)。
---
## Building the app (前端)
```bash
npm run build
```
輸出:`dist/`。後端可另建 Dockerfile 或 PM2 部署。
---
## 開發流程建議
1. 修改 `api-spec/openapi.yaml`
2. `npm run openapi:lint`
3. `npm run types:openapi`
4. 實作 / 更新 backend routes
5. 前端串接 / 驗證
6. PR & CI (未來加入測試)
---
For more information and support, please contact Base44 support at app@base44.com.
## /api-spec/openapi.yaml
```yaml path="/api-spec/openapi.yaml"
openapi: 3.1.0
info:
title: Shovel Heroes Public API
version: 0.1.0
description: |
REST endpoints replacing previous Base44 SDK usage.
This spec is inferred from the frontend code. Adjust field types and required properties to match your backend.
tags:
- name: DisasterAreas
description: 災區相關查詢與維護
- name: Grids
description: 救援/資源網格 CRUD 與狀態
- name: VolunteerRegistrations
description: 志工報名與取消
- name: SupplyDonations
description: 物資捐贈紀錄
- name: GridDiscussions
description: 網格討論留言
- name: Announcements
description: 系統公告 / 廣播訊息
- name: Volunteers
description: 志工名單或聚合資訊
- name: Users
description: 使用者與登入資訊
- name: Functions
description: 匯入匯出 / 後端批次或延伸功能
- name: Legacy
description: 與舊系統資料同步的 v2 相容端點
servers:
- url: https://your.api.server
description: Production
- url: http://localhost:8787
description: Local Dev
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
ID:
type: string
description: 唯一識別字串 (UUID、ULID 或資料庫主鍵)
example: "grid_01HZYQ9W123ABCDEF"
Timestamp:
type: string
format: date-time
description: ISO 8601 時間戳記 (UTC)
example: "2025-10-02T08:12:30Z"
DisasterArea:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
name:
type: string
description: 災區名稱
example: 光復鄉重災區
township:
type: string
description: 行政區 (鄉/鎮)
example: 光復鄉
county:
type: string
description: 縣市名稱
example: 花蓮縣
center_lat:
type: number
format: double
description: 災區中心點緯度
example: 23.8751
center_lng:
type: number
format: double
description: 災區中心點經度
example: 121.578
bounds:
type: object
description: 災區邊界 (近似方形) 用於地圖顯示
properties:
north: { type: number, example: 23.6705 }
south: { type: number, example: 23.6505 }
east: { type: number, example: 121.4325 }
west: { type: number, example: 121.4125 }
required:
- north
- south
- east
- west
grid_size:
type: integer
description: 建議網格尺寸 (公尺)
example: 200
status:
type: string
description: 災區狀態 (Base44 目前觀察為 active)
enum: [active]
example: active
description:
type: string
description: 災區描述與備註
example: 包含馬太鞍部落、佛祖街、自強路、武昌街等重災區域
created_by_id:
$ref: '#/components/schemas/ID'
created_by:
type: string
description: 建立者識別 (可能為 email)
example: kuo.tanya@gmail.com
is_sample:
type: boolean
description: 是否為示範資料
example: false
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
created_date:
$ref: '#/components/schemas/Timestamp'
updated_date:
$ref: '#/components/schemas/Timestamp'
required: [id, name, center_lat, center_lng]
SupplyNeed:
type: object
properties:
name:
type: string
description: 需求物資名稱
example: 鋤頭
quantity:
type: integer
description: 需求總數量
example: 20
unit:
type: string
description: 數量單位
example: 支
received:
type: integer
description: 已收到的數量
default: 0
example: 5
required: [name, quantity, unit]
Grid:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
code:
type: string
description: 網格代碼 (在災區中應唯一)
example: A-3
grid_type:
type: string
description: 網格類型 (決定使用情境)
enum: [mud_disposal, manpower, supply_storage, accommodation, food_area]
example: manpower
disaster_area_id:
$ref: '#/components/schemas/ID'
volunteer_needed:
type: integer
description: 需要志工數量 (僅 manpower 類型有意義)
default: 0
example: 10
volunteer_registered:
type: integer
description: 已報名志工數量
default: 0
example: 4
meeting_point:
type: string
nullable: true
description: 集合地點描述
example: 光復國小正門
risks_notes:
type: string
nullable: true
description: 風險或安全注意事項
example: 地面濕滑,注意積水與電線
contact_info:
type: string
nullable: true
description: 格主或聯絡人資訊 (手機/Line)
example: 0912-345-678
center_lat:
type: number
format: double
description: 中心點緯度
example: 23.87525
center_lng:
type: number
format: double
description: 中心點經度
example: 121.57812
bounds:
type: object
description: 粗略網格邊界 (地圖顯示用)
properties:
north:
type: number
example: 23.876
south:
type: number
example: 23.874
east:
type: number
example: 121.579
west:
type: number
example: 121.577
required: [north, south, east, west]
status:
type: string
description: 網格狀態
enum: [open, planning, closed, completed]
default: open
example: open
supplies_needed:
type: array
description: 所需物資列表
items:
$ref: '#/components/schemas/SupplyNeed'
example:
- name: 鋤頭
quantity: 20
unit: 支
received: 5
- name: 工作用手套
quantity: 50
unit: 副
received: 10
grid_manager_id:
$ref: '#/components/schemas/ID'
completion_photo:
type: string
nullable: true
description: 完工照片連結 (URL 或檔名)
example: https://example.com/completion/photo123.jpg
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
created_date:
$ref: '#/components/schemas/Timestamp'
updated_date:
$ref: '#/components/schemas/Timestamp'
created_by_id:
$ref: '#/components/schemas/ID'
created_by:
type: string
description: 建立者 (email / user ref)
example: kuo.tanya@gmail.com
is_sample:
type: boolean
description: 是否示範資料
example: false
required: [id, code, grid_type, disaster_area_id, center_lat, center_lng, bounds, status]
VolunteerRegistration:
type: object
description: 志工報名原始紀錄 (對應 Base44 VolunteerRegistration)
properties:
id:
$ref: '#/components/schemas/ID'
grid_id:
$ref: '#/components/schemas/ID'
volunteer_name:
type: string
description: 報名者名稱
example: 王銓詰
volunteer_phone:
type: string
nullable: true
description: 電話 (可能缺省或僅內部可見)
example: "0912345678"
volunteer_email:
type: string
nullable: true
description: 電子郵件
example: volunteer@example.org
available_time:
type: string
nullable: true
description: 可服務時間文字
example: 2025/10/04 上午12:00
skills:
type: array
description: 技能列表
items:
type: string
example:
- EMT-1
- 打掃
equipment:
type: array
description: 攜帶工具
items:
type: string
example:
- 雨鞋
- 鏟子
status:
$ref: '#/components/schemas/VolunteerStatus'
check_in_time:
type: string
format: date-time
nullable: true
description: 報到時間 (若已報到)
example: 2025-10-01T09:00:00Z
notes:
type: string
nullable: true
description: 備註
example: 需要手套
created_by_id:
$ref: '#/components/schemas/ID'
created_by:
type: string
description: 建立者 (email)
example: kuo.tanya@gmail.com
is_sample:
type: boolean
description: 是否示範資料
example: false
created_date:
$ref: '#/components/schemas/Timestamp'
updated_date:
$ref: '#/components/schemas/Timestamp'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
required: [id, grid_id, volunteer_name, status]
SupplyDonation:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
grid_id:
$ref: '#/components/schemas/ID'
name:
type: string
description: 捐贈物資名稱
example: 礦泉水
quantity:
type: integer
description: 捐贈數量
example: 100
unit:
type: string
description: 單位
example: 箱
donor_contact:
type: string
description: 捐贈者聯絡方式
example: water-donor@example.com
created_at:
$ref: '#/components/schemas/Timestamp'
required: [id, grid_id, name, quantity, unit]
GridDiscussion:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
grid_id:
$ref: '#/components/schemas/ID'
user_id:
$ref: '#/components/schemas/ID'
content:
type: string
description: 留言內容
example: 今天下午 2 點再集中一次清運。
created_at:
$ref: '#/components/schemas/Timestamp'
required: [id, grid_id, user_id, content]
Announcement:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
title:
type: string
description: 公告標題
example: 10/3 志工集合時間調整
body:
type: string
description: 公告內容 (舊命名, 將逐步改為 content)
deprecated: true
example: 明日集合時間改為 **08:30**,請提早 10 分鐘報到。
content:
type: string
description: 公告內容 (實際資料欄位)
example: 請先照顧好自己再幫助別人
category:
type: string
description: 公告分類
enum: [safety, equipment, center, external, contact]
example: safety
is_pinned:
type: boolean
description: 是否置頂
example: true
external_links:
type: array
description: 外部參考連結
items:
$ref: '#/components/schemas/ExternalLink'
example:
- name: 防災整合地圖
url: https://reurl.cc/9nNjqO
contact_phone:
type: string
nullable: true
description: 聯絡電話 (可含多組 / 分隔)
example: 03-870-1214
order:
type: integer
description: 排序用權重 (數字越小越前)
example: 1
created_by_id:
$ref: '#/components/schemas/ID'
created_by:
type: string
description: 建立者 (email)
example: kuo.tanya@gmail.com
is_sample:
type: boolean
description: 是否示範資料
example: false
created_at:
$ref: '#/components/schemas/Timestamp'
created_date:
$ref: '#/components/schemas/Timestamp'
updated_date:
$ref: '#/components/schemas/Timestamp'
required: [id, title, body]
ExternalLink:
type: object
properties:
name:
type: string
description: 連結名稱
example: 防災整合地圖
url:
type: string
format: uri
description: 連結 URL
example: https://reurl.cc/9nNjqO
required: [name, url]
User:
type: object
properties:
id:
$ref: '#/components/schemas/ID'
name:
type: string
description: 使用者顯示名稱
example: 林志工
email:
type: string
format: email
description: 使用者電子郵件
example: volunteer@example.org
required: [id]
VolunteerStatus:
type: string
description: 志工報名狀態
enum: [pending, confirmed, arrived, completed, cancelled]
example: pending
VolunteerListItem:
type: object
description: 志工報名清單中每一筆資料 (結合報名 + 呈現所需欄位)
properties:
id:
$ref: '#/components/schemas/ID'
grid_id:
$ref: '#/components/schemas/ID'
user_id:
$ref: '#/components/schemas/ID'
volunteer_name:
type: string
description: 志工顯示名稱 (可能從使用者或表單輸入取得)
example: 張小強
volunteer_phone:
type: string
nullable: true
description: 志工電話號碼;若 `can_view_phone=false` 時後端可直接省略或回傳遮蔽版本
example: 0912-345-678
status:
$ref: '#/components/schemas/VolunteerStatus'
available_time:
type: string
nullable: true
description: 可服務時間文字描述
example: 10/3 上午或 10/4 全天
skills:
type: array
description: 自述技能 (字串列表)
items:
type: string
example:
- 挖土機
- 醫療志工
equipment:
type: array
description: 可攜帶工具 (字串列表)
items:
type: string
example:
- 鐵鏟
- 手推車
notes:
type: string
nullable: true
description: 其他備註
example: 需要協助調度交通
created_date:
type: string
format: date-time
description: 報名建立時間 (命名沿用前端使用字段; 與其他資源的 created_at 不同)
example: 2025-10-02T08:12:30Z
required: [id, grid_id, user_id, volunteer_name, status, created_date]
VolunteersListResponse:
type: object
description: 志工清單回應外層封裝
properties:
data:
type: array
description: 志工報名資料陣列
items:
$ref: '#/components/schemas/VolunteerListItem'
can_view_phone:
type: boolean
description: 是否允許前端顯示 `volunteer_phone`;由後端依角色授權判斷
example: true
total:
type: integer
description: 總筆數 (分頁時可用)
example: 128
status_counts:
type: object
description: 各狀態統計 (若 `include_counts=true` 或後端預設回傳)
properties:
pending: { type: integer, example: 12 }
confirmed: { type: integer, example: 34 }
arrived: { type: integer, example: 8 }
completed: { type: integer, example: 55 }
cancelled: { type: integer, example: 19 }
page:
type: integer
description: 目前頁碼 (可選, 若採用 page 模式)
example: 1
limit:
type: integer
description: 單頁筆數 (對應查詢參數)
example: 50
required: [data, can_view_phone]
Error:
type: object
properties:
message:
type: string
description: 錯誤訊息
example: 未授權的請求
code:
type: string
description: 內部錯誤代碼 (可選)
example: AUTH_REQUIRED
required: [message]
parameters:
PageLimit:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
description: 單頁筆數 (預設 50, 上限 200)
PageOffset:
name: offset
in: query
required: false
schema:
type: integer
minimum: 0
default: 0
description: 起始位移 (用於分頁)
responses:
Unauthorized:
description: 未授權 (需要 Bearer Token)
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: 找不到資源
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ValidationError:
description: 請求參數或內容驗證失敗
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalError:
description: 伺服器內部錯誤
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
paths:
/disaster-areas:
get:
summary: List disaster areas
description: 取得所有災區清單 (含中心座標) 。
tags: [DisasterAreas]
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageOffset'
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/DisasterArea' }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Create disaster area
description: 新增一筆災區資料。
tags: [DisasterAreas]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DisasterArea'
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/disaster-areas/{id}:
get:
summary: Get disaster area
description: 依 ID 取得單一災區。
tags: [DisasterAreas]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/DisasterArea' } } } }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
put:
summary: Update disaster area
description: 更新災區內容。
tags: [DisasterAreas]
security: [ { bearerAuth: [] } ]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DisasterArea' }
responses:
'200': { description: Updated }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
delete:
summary: Delete disaster area
description: 刪除指定災區 (需權限)。
tags: [DisasterAreas]
security: [ { bearerAuth: [] } ]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/grids:
get:
summary: List grids
description: 取得全部網格 (可於未來加入篩選參數)。
tags: [Grids]
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageOffset'
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Grid' }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Create grid
description: 建立新的救援 / 資源網格。
tags: [Grids]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/Grid' }
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/grids/{id}:
get:
summary: Get grid
description: 依 ID 取得單一網格詳情。
tags: [Grids]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Grid' } } } }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
put:
summary: Update grid
description: 更新網格資料 (含需求/狀態)。
tags: [Grids]
security: [ { bearerAuth: [] } ]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/Grid' }
responses:
'200': { description: Updated }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
delete:
summary: Delete grid
description: 刪除網格 (通常僅管理員使用)。
tags: [Grids]
security: [ { bearerAuth: [] } ]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/volunteer-registrations:
get:
summary: List volunteer registrations
description: 取得所有志工報名紀錄 (未加分頁僅示範)。
tags: [VolunteerRegistrations]
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageOffset'
responses:
'200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VolunteerRegistration' } } } } }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Register volunteer
description: 志工報名參與指定網格。
tags: [VolunteerRegistrations]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/VolunteerRegistration' }
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/volunteer-registrations/{id}:
delete:
summary: Cancel volunteer registration
description: 取消指定志工報名。
tags: [VolunteerRegistrations]
security: [ { bearerAuth: [] } ]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/ID' }
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/supply-donations:
get:
summary: List supply donations
description: 取得物資捐贈紀錄清單。
tags: [SupplyDonations]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/SupplyDonation' }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Create supply donation
description: 建立一筆捐贈物資紀錄。
tags: [SupplyDonations]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SupplyDonation' }
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/grid-discussions:
get:
summary: List grid discussions
description: 取得全部網格的留言 (建議未來加入過濾)。
tags: [GridDiscussions]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/GridDiscussion' }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Post discussion message
description: 在指定網格張貼留言。
tags: [GridDiscussions]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/GridDiscussion' }
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/announcements:
get:
summary: List announcements
description: 取得公告列表 (可依時間排序)。
tags: [Announcements]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Announcement' }
'500': { $ref: '#/components/responses/InternalError' }
post:
summary: Create announcement
description: 建立一則公告。
tags: [Announcements]
security: [ { bearerAuth: [] } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/Announcement' }
responses:
'201': { description: Created }
'400': { $ref: '#/components/responses/ValidationError' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/volunteers:
get:
summary: List volunteers
description: |
取得志工報名清單(整合報名與使用者呈現所需欄位)。
回傳封裝含 `data` 與 `can_view_phone` 供前端判斷是否顯示電話。
欄位 `created_date` 目前沿用前端既有命名;若要統一建議改為 `created_at` 並同步前端。
tags: [Volunteers]
parameters:
- name: grid_id
in: query
required: false
schema: { $ref: '#/components/schemas/ID' }
description: 過濾指定網格的志工
- name: status
in: query
required: false
schema: { $ref: '#/components/schemas/VolunteerStatus' }
description: 過濾指定狀態
- name: include_counts
in: query
required: false
schema: { type: boolean, default: true }
description: 是否回傳狀態統計 `status_counts`
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageOffset'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/VolunteersListResponse'
'500': { $ref: '#/components/responses/InternalError' }
/users:
get:
summary: List users
description: 取得使用者列表 (需權限)。
tags: [Users]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/me:
get:
summary: Get current user
description: 回傳目前登入的使用者資訊。
tags: [Users]
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/functions/fix-grid-bounds:
post:
summary: Normalize / repair stored grid bounds
description: 批次修正網格邊界 (僅管理維運)。
tags: [Functions]
security: [ { bearerAuth: [] } ]
responses:
'200': { description: Done }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/functions/export-grids-csv:
get:
summary: Export grids as CSV
description: 匯出所有網格資料為 CSV 檔。
tags: [Functions]
responses:
'200':
description: CSV content
content:
text/csv:
schema: { type: string, description: CSV text }
'500': { $ref: '#/components/responses/InternalError' }
/functions/import-grids-csv:
post:
summary: Import grids from CSV content
description: 從 CSV 內容匯入 / 更新網格。
tags: [Functions]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
csv: { type: string }
required: [csv]
responses:
'200': { description: Import result }
'400': { $ref: '#/components/responses/ValidationError' }
'500': { $ref: '#/components/responses/InternalError' }
/functions/grid-template:
get:
summary: Download blank grid template CSV
description: 下載空白網格匯入範本。
tags: [Functions]
responses:
'200': { description: CSV }
'500': { $ref: '#/components/responses/InternalError' }
/functions/external-grid-api:
post:
summary: Proxy / external grid API call
description: 代理對外部網格系統的呼叫。
tags: [Functions]
requestBody:
required: true
content:
application/json:
schema: { type: object }
responses:
'200': { description: OK }
'400': { $ref: '#/components/responses/ValidationError' }
'500': { $ref: '#/components/responses/InternalError' }
/functions/external-volunteer-api:
post:
summary: Proxy / external volunteer API call
description: 代理對外部志工系統的呼叫。
tags: [Functions]
requestBody:
required: true
content:
application/json:
schema: { type: object }
responses:
'200': { description: OK }
'400': { $ref: '#/components/responses/ValidationError' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v2/sync:
post:
summary: Sync legacy data (v2)
description: 與舊版系統同步資料 (可能為非同步背景作業)。
tags: [Legacy]
responses:
'200': { description: Sync started }
'500': { $ref: '#/components/responses/InternalError' }
/api/v2/roster:
get:
summary: Get roster (v2)
description: 取得舊系統格式之 roster 資料。
tags: [Legacy]
responses:
'200': { description: OK }
'500': { $ref: '#/components/responses/InternalError' }
```
## /api/openapi.yaml
```yaml path="/api/openapi.yaml"
openapi: 3.1.0
info:
title: Shovel Heroes API
version: 0.2.0
description: |
Current implementation spec for Shovel Heroes backend.
This document reflects the actually implemented routes (LINE OAuth, grids, disaster areas,
volunteer registrations, volunteers aggregate view, supply donations, announcements, grid discussions, users).
Deprecated / planned endpoints from earlier drafts (tasks, checkins, resource-points) removed to avoid confusion.
contact:
name: Maintainers
email: shovel-heroes@noreply.github.com
servers:
- url: https://api.shovel-heroes.example.org
description: Production (placeholder)
- url: https://staging.api.shovel-heroes.example.org
description: Staging (placeholder)
tags:
- name: Auth
description: Authentication & LINE OAuth
- name: Users
description: User & identity endpoints
- name: DisasterAreas
description: Disaster area management
- name: Grids
description: Grid (需求) management
- name: VolunteerRegistrations
description: Volunteer registration records (per grid)
- name: Volunteers
description: Aggregated volunteer listing & stats
- name: SupplyDonations
description: Supply donation records
- name: Announcements
description: System / public announcements
- name: GridDiscussions
description: Comments / discussions under grids
paths:
/auth/line/login:
get:
tags: [Auth]
summary: LINE OAuth start (redirect)
description: Redirects user agent to LINE authorization page.
responses:
'302':
description: Redirect to LINE authorization URL
/auth/line/callback:
get:
tags: [Auth]
summary: LINE OAuth callback (redirect to frontend)
parameters:
- in: query
name: code
schema: { type: string }
- in: query
name: state
schema: { type: string }
- in: query
name: error
schema: { type: string }
responses:
'302':
description: Redirect to frontend /auth/line/return with code & state or error
/auth/line/exchange:
post:
tags: [Auth]
summary: Exchange LINE code/state for app JWT
description: Validates state, exchanges code with LINE, creates/updates user, issues short-lived JWT (2h).
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code, state]
properties:
code: { type: string }
state: { type: string }
responses:
'200':
description: JWT issued
content:
application/json:
schema: { $ref: '#/components/schemas/AuthTokenResponse' }
'400': { $ref: '#/components/responses/BadRequest' }
'502': { $ref: '#/components/responses/UpstreamError' }
'500': { $ref: '#/components/responses/ServerError' }
/auth/logout:
get:
tags: [Auth]
summary: Logical logout
description: Stateless JWT – client discards token.
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
message: { type: string, example: ok }
/users:
get:
tags: [Users]
summary: List users
responses:
'200':
description: User list
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/User' }
/me:
get:
tags: [Users]
summary: Current user profile
security: [{ bearerAuth: [] }]
responses:
'200':
description: Authenticated user
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
/disaster-areas:
get:
tags: [DisasterAreas]
summary: List disaster areas
responses:
'200':
description: List
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/DisasterArea' }
post:
tags: [DisasterAreas]
summary: Create disaster area
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DisasterAreaCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/DisasterArea' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
/disaster-areas/{id}:
get:
tags: [DisasterAreas]
summary: Get disaster area
parameters:
- $ref: '#/components/parameters/IdPath'
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/DisasterArea' } } } }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [DisasterAreas]
summary: Update disaster area
parameters:
- $ref: '#/components/parameters/IdPath'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DisasterAreaUpdate' }
responses:
'200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/DisasterArea' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [DisasterAreas]
summary: Delete disaster area
parameters:
- $ref: '#/components/parameters/IdPath'
responses:
'204': { description: Deleted }
'404': { $ref: '#/components/responses/NotFound' }
/grids:
get:
tags: [Grids]
summary: List grids
description: Returns grids ordered by created_at DESC.
responses:
'200':
description: List of grids
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Grid' }
post:
tags: [Grids]
summary: Create grid
description: Optional Turnstile token included when TURNSTILE_SECRET_KEY set.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/GridCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Grid' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/grids/{id}:
get:
tags: [Grids]
summary: Get grid
parameters: [ { $ref: '#/components/parameters/IdPath' } ]
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Grid' } } } }
'404': { $ref: '#/components/responses/NotFound' }
put:
tags: [Grids]
summary: Update grid
security: [{ bearerAuth: [] }]
parameters: [ { $ref: '#/components/parameters/IdPath' } ]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/GridUpdate' }
parameters:
- $ref: '#/components/parameters/IdPath'
- $ref: '#/components/parameters/ActingRoleHeader'
responses:
'200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/Grid' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
delete:
tags: [Grids]
summary: Delete grid
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/IdPath'
- $ref: '#/components/parameters/ActingRoleHeader'
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { description: Has related records, content: { application/json: { schema: { $ref: '#/components/schemas/GridDependencyConflict' } } } }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/volunteer-registrations:
get:
tags: [VolunteerRegistrations]
summary: List volunteer registrations
parameters:
- in: query
name: grid_id
schema: { type: string }
description: Filter by grid id
responses:
'200': { description: List, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VolunteerRegistration' } } } } }
post:
tags: [VolunteerRegistrations]
summary: Create volunteer registration
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/VolunteerRegistrationCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/VolunteerRegistration' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/volunteer-registrations/{id}:
put:
tags: [VolunteerRegistrations]
summary: Update registration status
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/IdPath'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/VolunteerRegistrationStatusUpdate' }
responses:
'200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/VolunteerRegistration' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
delete:
tags: [VolunteerRegistrations]
summary: Delete registration
parameters: [ { $ref: '#/components/parameters/IdPath' } ]
responses:
'204': { description: Deleted }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/volunteers:
get:
tags: [Volunteers]
summary: Aggregated volunteers list
parameters:
- in: query
name: grid_id
schema: { type: string }
- in: query
name: status
schema: { $ref: '#/components/schemas/VolunteerRegistrationStatus' }
- in: query
name: limit
schema: { type: integer, default: 200, minimum: 1, maximum: 500 }
- in: query
name: offset
schema: { type: integer, default: 0, minimum: 0 }
- in: query
name: include_counts
schema: { type: string, enum: ['true','false'], default: 'true' }
responses:
'200':
description: Aggregated list & counts
content:
application/json:
schema: { $ref: '#/components/schemas/VolunteersAggregateResponse' }
/supply-donations:
get:
tags: [SupplyDonations]
summary: List supply donations
parameters:
- in: query
name: grid_id
schema: { type: string }
responses:
'200': { description: List, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/SupplyDonation' } } } } }
post:
tags: [SupplyDonations]
summary: Create supply donation
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SupplyDonationCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/SupplyDonation' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/announcements:
get:
tags: [Announcements]
summary: List announcements
responses:
'200': { description: List, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Announcement' } } } } }
post:
tags: [Announcements]
summary: Create announcement
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AnnouncementCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Announcement' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
/grid-discussions:
get:
tags: [GridDiscussions]
summary: List grid discussions
parameters:
- in: query
name: grid_id
schema: { type: string }
description: Filter by grid id
responses:
'200': { description: List, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/GridDiscussion' } } } } }
post:
tags: [GridDiscussions]
summary: Create grid discussion message
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/GridDiscussionCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/GridDiscussion' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
components:
parameters:
IdPath:
name: id
in: path
required: true
schema: { type: string }
ActingRoleHeader:
name: X-Acting-Role
in: header
required: false
description: "Optional acting perspective. Only value currently used is 'user'; omitted means real role (e.g. admin)."
schema:
type: string
enum: ['user']
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >
Custom HS256 JWT issued by /auth/line/exchange. Short-lived (2h). No refresh yet. Include as `Authorization: Bearer <token>`.
responses:
BadRequest:
description: Invalid request parameters or body
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
Unauthorized:
description: Authentication required or invalid token
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
Forbidden:
description: Operation not permitted (role / ownership / actingRole restriction)
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
NotFound:
description: Resource not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
ServiceUnavailable:
description: Backend dependency (DB) not ready
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
UpstreamError:
description: Upstream provider failure (e.g., LINE token exchange)
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
ServerError:
description: Internal server error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
schemas:
User:
type: object
properties:
id: { type: string }
name: { type: string, nullable: true }
email: { type: string, nullable: true }
avatar_url: { type: string, nullable: true }
role: { type: string, nullable: true, description: "user | grid_manager | admin" }
DisasterArea:
type: object
properties:
id: { type: string }
name: { type: string }
township: { type: string, nullable: true }
county: { type: string, nullable: true }
center_lat: { type: number }
center_lng: { type: number }
bounds: { $ref: '#/components/schemas/Bounds', nullable: true }
grid_size: { type: integer, nullable: true }
status: { type: string, nullable: true }
description: { type: string, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
updated_at: { type: string, format: date-time, nullable: true }
DisasterAreaCreate:
allOf:
- $ref: '#/components/schemas/DisasterArea'
required: [name, center_lat, center_lng]
DisasterAreaUpdate:
type: object
description: Partial update
properties:
name: { type: string }
township: { type: string }
county: { type: string }
center_lat: { type: number }
center_lng: { type: number }
bounds: { $ref: '#/components/schemas/Bounds' }
grid_size: { type: integer }
status: { type: string }
description: { type: string }
Bounds:
type: object
properties:
north: { type: number }
south: { type: number }
east: { type: number }
west: { type: number }
required: [north, south, east, west]
SupplyNeed:
type: object
properties:
name: { type: string }
quantity: { type: number }
unit: { type: string }
received: { type: number, nullable: true }
Grid:
type: object
properties:
id: { type: string }
code: { type: string }
grid_type: { type: string }
disaster_area_id: { type: string }
volunteer_needed: { type: integer, nullable: true }
volunteer_registered: { type: integer, nullable: true }
meeting_point: { type: string, nullable: true }
risks_notes: { type: string, nullable: true }
contact_info: { type: string, nullable: true }
center_lat: { type: number }
center_lng: { type: number }
bounds: { $ref: '#/components/schemas/Bounds', nullable: true }
status: { type: string, description: "open | completed | preparing (others may appear)" }
supplies_needed: { type: array, items: { $ref: '#/components/schemas/SupplyNeed' }, nullable: true }
grid_manager_id: { type: string, nullable: true }
completion_photo: { type: string, nullable: true }
created_by_id: { type: string, nullable: true }
created_by: { type: string, nullable: true }
is_sample: { type: boolean, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
updated_at: { type: string, format: date-time, nullable: true }
GridCreate:
type: object
required: [code, grid_type, disaster_area_id, center_lat, center_lng]
properties:
code: { type: string }
grid_type: { type: string }
disaster_area_id: { type: string }
volunteer_needed: { type: integer, nullable: true }
meeting_point: { type: string, nullable: true }
risks_notes: { type: string, nullable: true }
contact_info: { type: string, nullable: true }
center_lat: { type: number }
center_lng: { type: number }
bounds: { $ref: '#/components/schemas/Bounds' }
status: { type: string, nullable: true }
supplies_needed: { type: array, items: { $ref: '#/components/schemas/SupplyNeed' }, nullable: true }
grid_manager_id: { type: string, nullable: true }
completion_photo: { type: string, nullable: true }
__turnstile_token: { type: string, nullable: true, description: "Cloudflare Turnstile token (optional / env dependent)" }
GridUpdate:
type: object
description: Partial update; any GridCreate field optional.
properties:
code: { type: string }
grid_type: { type: string }
volunteer_needed: { type: integer }
meeting_point: { type: string }
risks_notes: { type: string }
contact_info: { type: string }
bounds: { $ref: '#/components/schemas/Bounds' }
status: { type: string }
supplies_needed: { type: array, items: { $ref: '#/components/schemas/SupplyNeed' } }
grid_manager_id: { type: string }
completion_photo: { type: string }
GridDependencyConflict:
type: object
properties:
message: { type: string, example: Grid has related records }
details:
type: object
properties:
vr_count: { type: string, description: "Volunteer registrations count (string from SQL)" }
sd_count: { type: string }
gd_count: { type: string }
VolunteerRegistrationStatus:
type: string
enum: [pending, confirmed, arrived, completed, declined, cancelled]
description: "Lifecycle: pending -> confirmed -> arrived -> completed; branches: declined, cancelled (terminal)."
VolunteerRegistration:
type: object
properties:
id: { type: string }
grid_id: { type: string }
user_id: { type: string, nullable: true }
volunteer_name: { type: string, nullable: true }
volunteer_phone: { type: string, nullable: true }
volunteer_email: { type: string, nullable: true }
available_time: { type: string, nullable: true }
skills: { type: array, items: { type: string }, nullable: true }
equipment: { type: array, items: { type: string }, nullable: true }
status: { $ref: '#/components/schemas/VolunteerRegistrationStatus' }
notes: { type: string, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
updated_at: { type: string, format: date-time, nullable: true }
VolunteerRegistrationCreate:
type: object
required: [grid_id]
properties:
grid_id: { type: string }
user_id: { type: string }
volunteer_name: { type: string }
volunteer_phone: { type: string }
volunteer_email: { type: string }
available_time: { type: string }
skills: { type: array, items: { type: string } }
equipment: { type: array, items: { type: string } }
status: { $ref: '#/components/schemas/VolunteerRegistrationStatus' }
notes: { type: string }
description: Either user_id or volunteer_name must be provided (server validation rule).
VolunteerRegistrationStatusUpdate:
type: object
required: [status]
properties:
status: { $ref: '#/components/schemas/VolunteerRegistrationStatus' }
x-status-transitions:
description: Allowed transitions enforced server-side.
transitions:
pending: [confirmed, declined, cancelled]
confirmed: [arrived, cancelled]
arrived: [completed, cancelled]
completed: []
declined: []
cancelled: []
VolunteersAggregateItem:
type: object
properties:
id: { type: string }
grid_id: { type: string }
user_id: { type: string, nullable: true }
volunteer_name: { type: string }
volunteer_phone: { type: string, nullable: true }
status: { $ref: '#/components/schemas/VolunteerRegistrationStatus' }
available_time: { type: string, nullable: true }
skills: { type: array, items: { type: string } }
equipment: { type: array, items: { type: string } }
notes: { type: string, nullable: true }
created_date: { type: string, nullable: true }
VolunteersAggregateResponse:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/VolunteersAggregateItem' } }
can_view_phone: { type: boolean }
total: { type: integer }
status_counts:
type: object
additionalProperties: { type: integer }
nullable: true
limit: { type: integer }
page: { type: integer }
SupplyDonation:
type: object
properties:
id: { type: string }
grid_id: { type: string }
name: { type: string, nullable: true }
quantity: { type: number }
unit: { type: string, nullable: true }
donor_contact: { type: string, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
SupplyDonationCreate:
type: object
required: [grid_id, quantity]
properties:
grid_id: { type: string }
name: { type: string }
supply_name: { type: string }
quantity: { type: number }
unit: { type: string }
donor_name: { type: string }
donor_phone: { type: string }
donor_email: { type: string }
donor_contact: { type: string }
delivery_method: { type: string }
delivery_address: { type: string }
delivery_time: { type: string }
notes: { type: string }
description: Only subset persisted: name, quantity, unit, donor_contact.
Announcement:
type: object
properties:
id: { type: string }
title: { type: string }
body: { type: string }
content: { type: string, nullable: true }
category: { type: string, nullable: true }
is_pinned: { type: boolean, nullable: true }
external_links: { type: array, items: { type: object, properties: { name: { type: string }, url: { type: string } } }, nullable: true }
contact_phone: { type: string, nullable: true }
order: { type: integer, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
AnnouncementCreate:
type: object
required: [title, body]
properties:
title: { type: string }
body: { type: string }
content: { type: string }
category: { type: string }
is_pinned: { type: boolean }
external_links: { type: array, items: { type: object, properties: { name: { type: string }, url: { type: string } } } }
contact_phone: { type: string }
order: { type: integer }
GridDiscussion:
type: object
properties:
id: { type: string }
grid_id: { type: string }
user_id: { type: string, nullable: true }
content: { type: string }
author_name: { type: string, nullable: true }
author_role: { type: string, nullable: true }
created_at: { type: string, format: date-time, nullable: true }
GridDiscussionCreate:
type: object
required: [grid_id]
properties:
grid_id: { type: string }
content: { type: string }
message: { type: string }
description: Either content or message required.
AuthTokenResponse:
type: object
properties:
token: { type: string, description: "JWT (HS256)" }
user: { $ref: '#/components/schemas/User' }
Error:
type: object
properties:
message: { type: string }
code: { type: string, nullable: true }
details: { type: object, nullable: true }
required: [message]
x-guidelines:
actingRole:
description: "X-Acting-Role header narrows privileges (e.g. admin acting as 'user'). Currently applied to /grids PUT/DELETE."
security:
jwt: "Custom HS256 short-lived token. Consider future refresh rotation + JWKS for LINE id_token verification."
volunteerRegistrationLifecycle:
states: [pending, confirmed, arrived, completed, declined, cancelled]
transitions:
pending: [confirmed, declined, cancelled]
confirmed: [arrived, cancelled]
arrived: [completed, cancelled]
declined: []
completed: []
cancelled: []
gridDeletion:
conflict: "DELETE /grids/{id} returns 409 if related volunteer_registrations, supply_donations, or grid_discussions exist."
```
## /build.sh
```sh path="/build.sh"
#!/bin/bash
# Build script for Shovel Heroes Docker images
# Usage: ./build.sh [OPTIONS]
#
# Options:
# -t, --tag TAG Image tag (default: latest)
# -e, --env ENV Environment (dev|staging|prod, default: dev)
# -b, --backend-only Build backend only
# -f, --frontend-only Build frontend only
# -p, --push Push to ECR after build
# --ecr-registry URL ECR registry URL
# -h, --help Show this help message
set -e
# Default values
TAG="latest"
ENV="dev"
BUILD_BACKEND=true
BUILD_FRONTEND=true
PUSH=false
ECR_REGISTRY=""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-t|--tag)
TAG="$2"
shift 2
;;
-e|--env)
ENV="$2"
shift 2
;;
-b|--backend-only)
BUILD_FRONTEND=false
shift
;;
-f|--frontend-only)
BUILD_BACKEND=false
shift
;;
-p|--push)
PUSH=true
shift
;;
--ecr-registry)
ECR_REGISTRY="$2"
shift 2
;;
-h|--help)
echo "Build script for Shovel Heroes Docker images"
echo ""
echo "Usage: ./build.sh [OPTIONS]"
echo ""
echo "Options:"
echo " -t, --tag TAG Image tag (default: latest)"
echo " -e, --env ENV Environment (dev|staging|prod, default: dev)"
echo " -b, --backend-only Build backend only"
echo " -f, --frontend-only Build frontend only"
echo " -p, --push Push to ECR after build"
echo " --ecr-registry URL ECR registry URL"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " ./build.sh # Build both images with tag 'latest'"
echo " ./build.sh -t v1.0.0 -e staging # Build with tag 'staging-v1.0.0'"
echo " ./build.sh -b -t dev-test # Build backend only with tag 'dev-test'"
echo " ./build.sh -p --ecr-registry 123.ecr.aws... # Build and push to ECR"
exit 0
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}"
exit 1
;;
esac
done
# Determine image names
if [ -n "$ECR_REGISTRY" ]; then
BACKEND_IMAGE="${ECR_REGISTRY}/shovel-heroes-backend"
FRONTEND_IMAGE="${ECR_REGISTRY}/shovel-heroes-frontend"
else
BACKEND_IMAGE="shovel-heroes-backend"
FRONTEND_IMAGE="shovel-heroes-frontend"
fi
# Add environment prefix to tag
FULL_TAG="${ENV}-${TAG}"
echo -e "${GREEN}🚀 Building Shovel Heroes Docker images${NC}"
echo -e "Environment: ${YELLOW}${ENV}${NC}"
echo -e "Tag: ${YELLOW}${FULL_TAG}${NC}"
echo ""
# Build backend
if [ "$BUILD_BACKEND" = true ]; then
echo -e "${GREEN}📦 Building backend image...${NC}"
docker build \
-f Dockerfile.backend \
-t ${BACKEND_IMAGE}:${FULL_TAG} \
-t ${BACKEND_IMAGE}:${ENV}-latest \
.
echo -e "${GREEN}✓ Backend image built successfully${NC}"
echo -e " - ${BACKEND_IMAGE}:${FULL_TAG}"
echo -e " - ${BACKEND_IMAGE}:${ENV}-latest"
echo ""
fi
# Build frontend
if [ "$BUILD_FRONTEND" = true ]; then
echo -e "${GREEN}📦 Building frontend image...${NC}"
# Set API base URL based on environment
case $ENV in
prod|production)
VITE_API_BASE="https://api.shovel-heroes.cc"
;;
staging)
VITE_API_BASE="https://api.shovel-heroes.cc"
;;
dev|development)
VITE_API_BASE="http://localhost:8787"
;;
*)
VITE_API_BASE="https://api.shovel-heroes.cc"
;;
esac
docker build \
-f Dockerfile.frontend \
--build-arg VITE_API_BASE=${VITE_API_BASE} \
-t ${FRONTEND_IMAGE}:${FULL_TAG} \
-t ${FRONTEND_IMAGE}:${ENV}-latest \
.
echo -e "${GREEN}✓ Frontend image built successfully${NC}"
echo -e " - ${FRONTEND_IMAGE}:${FULL_TAG}"
echo -e " - ${FRONTEND_IMAGE}:${ENV}-latest}"
echo -e " - API Base: ${YELLOW}${VITE_API_BASE}${NC}"
echo ""
fi
# Push to ECR if requested
if [ "$PUSH" = true ]; then
if [ -z "$ECR_REGISTRY" ]; then
echo -e "${RED}Error: --ecr-registry is required when using --push${NC}"
exit 1
fi
echo -e "${GREEN}📤 Pushing images to ECR...${NC}"
if [ "$BUILD_BACKEND" = true ]; then
docker push ${BACKEND_IMAGE}:${FULL_TAG}
docker push ${BACKEND_IMAGE}:${ENV}-latest
echo -e "${GREEN}✓ Backend images pushed${NC}"
fi
if [ "$BUILD_FRONTEND" = true ]; then
docker push ${FRONTEND_IMAGE}:${FULL_TAG}
docker push ${FRONTEND_IMAGE}:${ENV}-latest
echo -e "${GREEN}✓ Frontend images pushed${NC}"
fi
echo ""
fi
echo -e "${GREEN}✅ Build completed successfully!${NC}"
echo ""
echo "To run locally:"
if [ "$BUILD_BACKEND" = true ]; then
echo -e " ${YELLOW}docker run -p 8787:8787 ${BACKEND_IMAGE}:${FULL_TAG}${NC}"
fi
if [ "$BUILD_FRONTEND" = true ]; then
echo -e " ${YELLOW}docker run -p 80:80 ${FRONTEND_IMAGE}:${FULL_TAG}${NC}"
fi
```
## /components.json
```json path="/components.json"
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```
## /docker-compose.yml
```yml path="/docker-compose.yml"
version: '3.9'
services:
db:
image: postgres:18-alpine
container_name: shovelheroes-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: shovelheroes
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata: {}
```
## /docs/AGENTS.md
# AGENTS.md
本文件定義 AI Agent 在本專案中的角色設定與開發準則。
## 專案概覽
**Shovel-Heroes(鏟子英雄)** 是一個災害應變協作平台,協助志工、物資、任務與通報的管理與協調。
### 技術架構
- **前端**: Vite + React 18 + Tailwind CSS
- **後端**: Fastify (Node.js) + PostgreSQL
- **型別系統**: OpenAPI 3.1 → TypeScript (透過 `openapi-typescript`)
- **開發模式**:
- Base44 SDK (預設)
- 自建 REST Backend (`packages/backend`):設定 `VITE_USE_REST=true`
### 目錄結構
```text
api-spec/ # OpenAPI 規格定義
packages/
backend/ # Fastify 後端服務
src/
index.ts # 主程式與插件註冊
lib/ # 資料庫連線、初始化
modules/ # 業務邏輯模組
routes/ # API 路由定義
middlewares/ # 中介軟體
shared-types/ # OpenAPI 產生的共用 TS 型別
src/ # React 前端原始碼
components/ # UI 元件
pages/ # 頁面元件
api/ # API 客戶端
context/ # React Context
hooks/ # Custom Hooks
lib/ # 工具函式
```
---
## Agent 角色定義
### 核心角色
你是一位具備以下專業能力的全端開發專家:
1. **後端開發專家 (Node.js)**
- 精通 Fastify 框架及其生態系統
- 熟悉 PostgreSQL 資料庫設計與最佳化
- 理解 RESTful API 設計原則
- 掌握 OpenAPI 規格撰寫與維護
2. **前端開發專家 (React)**
- 熟練 React 18 及其 Hooks API
- 精通 Vite 建置工具配置
- 掌握 Tailwind CSS 與 Radix UI 元件系統
- 理解 React Context 與狀態管理
3. **TypeScript 型別專家**
- 能有效運用 OpenAPI 產生的型別
- 維護前後端型別一致性
- 撰寫型別安全的程式碼
---
## 開發準則
### 1. 最小變更原則
**核心理念**: 每次程式碼變更應以最小影響範圍為目標。
- ✅ **應該做**:
- 只修改與需求直接相關的程式碼
- 優先使用既有的函式、元件與模式
- 保持現有的程式碼結構與架構
- 在修改前充分理解現有實作
- ❌ **不應該做**:
- 進行不必要的重構
- 修改需求範圍外的檔案
- 引入新的依賴套件(除非必要)
- 改變既有的命名慣例或資料夾結構
**範例**:
```javascript
// ❌ 不好:順便重構無關的程式碼
function updateGrid(gridId, data) {
// 修改目標:加入新欄位
const updatedData = { ...data, newField: 'value' };
// 不應該順便改寫這段無關邏輯
const results = complexLogic.map(item => {
return transformItem(item); // 原本就運作正常的程式碼
});
}
// ✅ 好:只修改必要部分
function updateGrid(gridId, data) {
// 只加入新欄位,其他邏輯保持原樣
const updatedData = { ...data, newField: 'value' };
// 保持原有邏輯不變
const results = complexLogic.map(x => x.transform());
}
```
### 2. 遵循專案 ESLint 規範
**規範檔案**: `eslint.config.js`
專案已配置以下規則:
- 基於 `@eslint/js` 推薦規則
- React 18.3 相關規則
- React Hooks 最佳實踐
- React Refresh 相容性
**已停用的規則**(請勿違反專案決策):
- `react/prop-types`: 'off' - 使用 TypeScript 型別,不使用 PropTypes
- `no-unused-vars`: 'off' - 允許未使用變數(環境變數等)
- `no-undef`: 'off' - 允許全域變數(process, require 等)
- `react/no-unknown-property`: 'off' - 允許自訂 JSX 屬性(如 cmdk-*, toast-*)
**開發流程**:
```bash
# 修改程式碼前先執行 lint 檢查
npm run lint
# 修改完成後再次確認
npm run lint
```
### 3. 程式碼風格準則
#### 後端 (Fastify)
**路由註冊模式**:
```typescript
// routes/example.ts
import type { FastifyInstance } from 'fastify';
export function registerExampleRoutes(app: FastifyInstance) {
// GET 端點
app.get('/api/examples', async (req, reply) => {
if (!app.hasDecorator('db')) return [];
const { rows } = await app.db.query('SELECT * FROM examples');
return rows;
});
// POST 端點(需要身份驗證)
app.post('/api/examples', async (req, reply) => {
// 驗證與處理邏輯
});
}
```
**資料庫操作**:
```typescript
// 使用 app.db decorator
if (!app.hasDecorator('db')) {
return reply.status(503).send({ error: 'Database not available' });
}
const { rows } = await app.db.query(
'SELECT * FROM table WHERE id = $1',
[id]
);
```
#### 前端 (React)
**元件結構**:
```jsx
// pages/Example.jsx
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export default function ExamplePage() {
const [data, setData] = useState([]);
useEffect(() => {
// 資料載入邏輯
}, []);
return (
<div className="container mx-auto p-4">
{/* 使用 Tailwind CSS */}
</div>
);
}
```
**API 呼叫**:
```javascript
// 使用現有的 API 客戶端
import { Entity } from "@/api/entities";
import { functionName } from "@/api/functions";
// 實體操作
const items = await Entity.list();
const item = await Entity.get(id);
await Entity.create(data);
// 函式呼叫
const result = await functionName(params);
```
### 4. 型別安全
**OpenAPI 工作流程**:
```bash
# 1. 修改 API 規格
# api-spec/openapi.yaml
# 2. 驗證規格
npm run openapi:lint
# 3. 產生 TypeScript 型別
npm run types:openapi
# 4. 建置共用型別套件
npm run types:build
```
**使用產生的型別**:
```typescript
import type { components } from 'shovel-shared-types/src/openapi';
// 使用 OpenAPI 定義的型別
type Grid = components['schemas']['Grid'];
type VolunteerRegistration = components['schemas']['VolunteerRegistration'];
```
### 5. 安全性要求
遵循 `SECURITY.md` 的規範:
- ✅ 輸入驗證:所有 API 端點必須驗證輸入資料
- ✅ 錯誤處理:不洩漏內部錯誤細節給前端
- ✅ 身份驗證:保護需要授權的端點
- ✅ 率限制:實作 API 呼叫頻率限制
- ❌ 不得提交:金鑰、密碼、個人資料
### 6. 測試與驗證
**手動驗證清單**:
```bash
# 前端開發
npm run dev # 啟動開發伺服器
npm run build # 確認建置成功
npm run lint # 檢查程式碼品質
# 後端開發
npm run dev:api # 啟動後端服務
# 訪問 http://localhost:8787/docs 查看 Swagger UI
# 訪問 http://localhost:8787/healthz 確認服務健康
# 資料庫
docker compose up -d db # 啟動資料庫
npm run seed # 填入測試資料
npm run seed:clean # 清空測試資料
```
---
## 開發工作流程
### 新增功能
1. **理解需求**: 確認需求範圍與影響範圍
2. **檢視現有實作**: 搜尋相似功能,參考既有模式
3. **最小變更**: 只修改必要的檔案
4. **遵循規範**: 依照 ESLint 與專案風格撰寫
5. **驗證功能**: 本機測試與 lint 檢查
6. **更新文件**: 同步更新 README、OpenAPI 規格
### 修復問題
1. **重現問題**: 理解問題的根本原因
2. **定位範圍**: 找出最小修改範圍
3. **保守修正**: 避免引入新的變更
4. **回歸測試**: 確認未影響其他功能
### 重構建議
> ⚠️ 除非明確要求,否則避免主動重構
如需重構:
- 必須有明確的理由(效能、可維護性)
- 獨立 PR,不與功能開發混合
- 充分測試,確保行為不變
---
## 參考文件
- **專案說明**: `README.md`
- **貢獻指南**: `CONTRIBUTING.md`
- **安全政策**: `SECURITY.md`
- **API 規格**: `api-spec/openapi.yaml`
- **型別定義**: `packages/shared-types/src/openapi.ts`
---
## 常見問題處理
### 前端
**Q: 需要新增 UI 元件?**
- 優先使用 `src/components/ui/` 現有元件
- 參考 Radix UI + Tailwind CSS 模式
- 保持與現有設計系統一致
**Q: 需要呼叫新的 API?**
- 檢查 `src/api/` 是否已有類似功能
- 遵循現有的 Base44 SDK 或 REST 客戶端模式
- 確保錯誤處理完整
### 後端
**Q: 需要新增 API 端點?**
- 先更新 `api-spec/openapi.yaml`
- 在對應的 routes 檔案新增路由
- 遵循既有的回應格式與錯誤處理
**Q: 需要修改資料庫 Schema?**
- 更新 `packages/backend/src/lib/db-init.ts`
- 使用 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
- 確保向下相容
**Q: 需要新增中介軟體?**
- 參考 `packages/backend/src/middlewares/`
- 在 `index.ts` 適當位置註冊
- 注意執行順序
---
## 總結
作為 AI Agent,你的目標是:
1. 🎯 **精準修改**: 只改需要改的,不多不少
2. 📏 **遵循規範**: 尊重專案的 ESLint 與風格設定
3. 🔒 **保持穩定**: 避免破壞現有功能
4. 📚 **學習優先**: 先理解現有程式碼,再動手修改
5. 🤝 **協作思維**: 撰寫易於他人理解與維護的程式碼
記住:**最好的程式碼是不需要寫的程式碼,最好的重構是不需要做的重構。**
## /docs/CONTRIBUTING.md
# 貢獻指南(Contributing Guide)
感謝你願意協助「鏟子英雄」!以下流程能確保大家在救災期間快速、穩定且安全地協作。
## 快速開始
- 需要環境:Node.js **18+**(LTS)
- 安裝:`npm install`
- 開發:`npm run dev`
- 建置:`npm run build`
> 以上命令與 Vite + React 腳手架一致,與倉庫 README 相符。
## 分支與提交流程
1. 從 `main` 切出功能分支:`feat/xxx`、`fix/yyy`、`docs/zzz`
2. **提交訊息:採用 Conventional Commits**
- `feat: 新增任務清單檢視`
- `fix: 修正地圖 marker 點擊崩潰`
- `chore(docs): 新增 SECURITY.md`
3. 每個 PR 盡量聚焦單一目的;如僅更新 `package.json`,請與功能變更分開 PR(回應 PR #1 的維護者建議)。
## 程式碼風格與品質
- Lint:本專案已含 `eslint.config.js`,請在提交前執行 `npm run lint`。
- 型別/錯誤處理:建立 `Result` 風格回傳或一致的錯誤邏輯,避免在 UI 泄漏內部錯誤細節。
- UI:Tailwind CSS 已配置,請沿用既有樣式系統與設計 Token。
## 安全與隱私要求(與 SECURITY.md 對齊)
- 新增表單或 API 呼叫:務必加入**輸入驗證**、**錯誤訊息最小化**、**前端人機驗證(視風險)**。
- 涉及建立/修改動作的 API:請加入**伺服端率限制**,並支援 `Idempotency-Key`。
- 不得在程式碼提交任何金鑰或個資;提供 `.env.example` 指引環境變數。
## PR 檢核清單(Review Checklist)
- [ ] 有對應 Issue 或清楚描述
- [ ] 有測試或可手動驗收步驟
- [ ] 通過 Lint/Build
- [ ] 不引入敏感資訊、金鑰或多餘權限
- [ ] 對文件(README/CLAUDE.md/SECURITY.md/OpenAPI)同步更新
## Issue 回報格式建議
- 現象/重現步驟/預期與實際行為/螢幕截圖或日誌節錄(若含個資,請遮罩處理)/可能影響範圍
## 行為準則
我們採用 Contributor Covenant 原則:尊重、安全、包容、聚焦任務。
## 社群與討論空間
- [Line社群](https://line.me/ti/g2/X4H7PCoUKV_kx5LN5VtfVj9qA93vNquolxCdmQ)
## /docs/DEPLOYMENT_RUNBOOK.md
# Deployment Runbook
This runbook provides step-by-step procedures for deploying, monitoring, and troubleshooting the Shovel Heroes application on AWS EKS.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Automated Deployments](#automated-deployments)
3. [Manual Deployments](#manual-deployments)
4. [Rollback Procedures](#rollback-procedures)
5. [Monitoring & Verification](#monitoring--verification)
6. [Troubleshooting](#troubleshooting)
7. [Environment Variables](#environment-variables)
---
## Prerequisites
### Required Tools
```bash
# AWS CLI
aws --version # v2.x or higher
# kubectl
kubectl version --client # v1.31.x
# kustomize
kustomize version # v5.x or higher
# Docker (for local testing)
docker --version
```
### Required Access
- ✅ AWS IAM credentials or SSO access
- ✅ GitHub repository write access (for workflows)
- ✅ EKS cluster access (kubectl configured)
- ✅ ECR push permissions
### Infrastructure Status
Verify Terraform infrastructure is deployed:
```bash
cd /Users/yihuang/workspace/shovel-heroes-terraform
terraform output
```
Expected outputs:
- `eks_cluster_name`
- `ecr_repository_urls`
- `alb_dns_name`
- `github_actions_role_arn`
- `backend_target_group_arn`
- `frontend_target_group_arn`
---
## Automated Deployments
### Staging Deployment (Automatic)
**Trigger**: Push to `main` branch
**Workflow**: `.github/workflows/deploy-staging.yml`
**Process**:
1. Code is pushed to `main` branch
2. GitHub Actions workflow triggers automatically
3. Docker images are built and tagged with commit SHA
4. Images are pushed to ECR
5. Kubernetes manifests are applied via Kustomize
6. Deployment waits for pods to become healthy
7. Health checks verify backend and frontend
**Monitoring**:
- GitHub Actions: https://github.com/YOUR_ORG/shovel-heroes-k8s/actions
- Watch workflow execution in real-time
- Check job logs for errors
**Verification**:
```bash
# Get ALB DNS name
ALB_DNS=$(terraform output -raw alb_dns_name)
# Test backend
curl http://$ALB_DNS/healthz
# Test frontend
curl http://$ALB_DNS/
# Check pods
kubectl get pods -n shovel-heroes-staging
```
### Production Deployment (Manual Approval)
**Trigger**:
- GitHub Release (automatic build + deploy)
- Manual workflow dispatch (promote existing image)
**Workflow**: `.github/workflows/deploy-production.yml`
#### Option 1: Deploy from Release
```bash
# Create a new release
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# Go to GitHub → Releases → Create release from tag
# This triggers production workflow with approval gate
```
#### Option 2: Promote Staging Image
```bash
# Go to GitHub Actions → Deploy to Production (EKS) → Run workflow
# Select branch: main
# Input image_tag: staging-abc123 (from staging deployment)
# Click "Run workflow"
# Approve deployment when prompted
```
**Approval Process**:
1. Workflow pauses at "production" environment gate
2. Designated approvers receive notification
3. Review staging deployment and test results
4. Approve or reject deployment
5. Workflow continues if approved
**Verification**:
```bash
# Get production ALB DNS
ALB_DNS=$(terraform output -raw alb_dns_name)
# Test endpoints
curl http://$ALB_DNS/healthz
curl http://$ALB_DNS/
# Check pods
kubectl get pods -n shovel-heroes-production
# Check rollout status
kubectl rollout status deployment/backend-prod -n shovel-heroes-production
kubectl rollout status deployment/frontend-prod -n shovel-heroes-production
```
---
## Manual Deployments
Use manual deployment when automated workflows fail or for local development.
### Step 1: Build Docker Images
```bash
# Build both images
./build.sh -t v1.0.0 -e staging
# Or build individually
./build.sh -b -t v1.0.0 -e staging # Backend only
./build.sh -f -t v1.0.0 -e staging # Frontend only
```
### Step 2: Push to ECR
```bash
# Get ECR URLs from Terraform
BACKEND_ECR=$(terraform output -json ecr_repository_urls | jq -r '.backend')
FRONTEND_ECR=$(terraform output -json ecr_repository_urls | jq -r '.frontend')
# Login to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $BACKEND_ECR
# Tag and push
docker tag shovel-heroes-backend:staging-v1.0.0 $BACKEND_ECR:staging-v1.0.0
docker tag shovel-heroes-frontend:staging-v1.0.0 $FRONTEND_ECR:staging-v1.0.0
docker push $BACKEND_ECR:staging-v1.0.0
docker push $FRONTEND_ECR:staging-v1.0.0
```
### Step 3: Configure kubectl
```bash
# Get cluster name from Terraform
CLUSTER_NAME=$(terraform output -raw eks_cluster_name)
# Update kubeconfig
aws eks update-kubeconfig --region us-east-1 --name $CLUSTER_NAME
# Verify access
kubectl get nodes
```
### Step 4: Create Kubernetes Secrets (First Deployment Only)
```bash
# Get secrets from AWS Secrets Manager
DB_SECRET_ARN=$(terraform output -raw database_secret_arn)
APP_SECRET_ARN=$(terraform output -raw application_secret_arn)
DB_SECRET=$(aws secretsmanager get-secret-value --secret-id $DB_SECRET_ARN --query SecretString --output text)
APP_SECRET=$(aws secretsmanager get-secret-value --secret-id $APP_SECRET_ARN --query SecretString --output text)
# Extract values
DATABASE_URL=$(echo $DB_SECRET | jq -r '.url')
JWT_SECRET=$(echo $APP_SECRET | jq -r '.jwt_secret')
API_KEY=$(echo $APP_SECRET | jq -r '.api_key')
# Create secret
kubectl create secret generic shovel-heroes-secrets \
--from-literal=database-url="$DATABASE_URL" \
--from-literal=jwt-secret="$JWT_SECRET" \
--from-literal=api-key="$API_KEY" \
-n shovel-heroes-staging \
--dry-run=client -o yaml | kubectl apply -f -
```
### Step 5: Update Manifests with Values
```bash
cd k8s/overlays/staging
# Update image tags
kustomize edit set image \
BACKEND_ECR_URL=$BACKEND_ECR:staging-v1.0.0 \
FRONTEND_ECR_URL=$FRONTEND_ECR:staging-v1.0.0
# Update IRSA role ARN in base manifest
BACKEND_ROLE_ARN=$(terraform output -raw backend_pod_role_arn)
sed -i '' "s|BACKEND_POD_ROLE_ARN|$BACKEND_ROLE_ARN|g" ../../base/serviceaccount.yaml
# Update ALB target groups in ingress
BACKEND_TG_ARN=$(terraform output -raw backend_target_group_arn)
FRONTEND_TG_ARN=$(terraform output -raw frontend_target_group_arn)
ALB_NAME=$(terraform output -raw alb_dns_name | cut -d'-' -f1-3)
sed -i '' "s|BACKEND_TARGET_GROUP_ARN|$BACKEND_TG_ARN|g" ../../base/ingress.yaml
sed -i '' "s|FRONTEND_TARGET_GROUP_ARN|$FRONTEND_TG_ARN|g" ../../base/ingress.yaml
sed -i '' "s|ALB_NAME|$ALB_NAME|g" ../../base/ingress.yaml
```
### Step 6: Deploy
```bash
# Preview changes
kubectl kustomize k8s/overlays/staging
# Apply manifests
kubectl apply -k k8s/overlays/staging
# Watch deployment
kubectl rollout status deployment/backend-staging -n shovel-heroes-staging
kubectl rollout status deployment/frontend-staging -n shovel-heroes-staging
# Check pods
kubectl get pods -n shovel-heroes-staging -w
```
---
## Rollback Procedures
### Quick Rollback (Last Working Version)
```bash
# Rollback backend
kubectl rollout undo deployment/backend-staging -n shovel-heroes-staging
# Rollback frontend
kubectl rollout undo deployment/frontend-staging -n shovel-heroes-staging
# Verify rollback
kubectl rollout status deployment/backend-staging -n shovel-heroes-staging
kubectl get pods -n shovel-heroes-staging
```
### Rollback to Specific Revision
```bash
# View deployment history
kubectl rollout history deployment/backend-staging -n shovel-heroes-staging
# Example output:
# REVISION CHANGE-CAUSE
# 1 Initial deployment
# 2 Update to v1.1.0
# 3 Update to v1.2.0 (current)
# Rollback to specific revision
kubectl rollout undo deployment/backend-staging -n shovel-heroes-staging --to-revision=2
# Verify
kubectl rollout status deployment/backend-staging -n shovel-heroes-staging
```
### Redeploy Specific Image Tag
```bash
# Update image tag in kustomization
cd k8s/overlays/staging
kustomize edit set image BACKEND_ECR_URL=$BACKEND_ECR:staging-v1.1.0
# Apply
kubectl apply -k .
# Wait for rollout
kubectl rollout status deployment/backend-staging -n shovel-heroes-staging
```
### Emergency Rollback (Scale Down)
If rollback fails, scale down the deployment:
```bash
# Scale to 0 replicas (takes app offline)
kubectl scale deployment/backend-staging --replicas=0 -n shovel-heroes-staging
# Fix the issue, then scale back up
kubectl scale deployment/backend-staging --replicas=2 -n shovel-heroes-staging
```
---
## Monitoring & Verification
### Health Checks
```bash
# Backend health check
curl http://$ALB_DNS/healthz
# Expected response: {"status":"ok"}
# Frontend health check
curl http://$ALB_DNS/health
# Expected response: healthy
```
### Pod Status
```bash
# List all pods
kubectl get pods -n shovel-heroes-staging
# Expected output:
# NAME READY STATUS RESTARTS AGE
# backend-staging-xxx-yyy 1/1 Running 0 5m
# backend-staging-xxx-zzz 1/1 Running 0 5m
# frontend-staging-xxx-yyy 1/1 Running 0 5m
# frontend-staging-xxx-zzz 1/1 Running 0 5m
# Describe pod for details
kubectl describe pod backend-staging-xxx-yyy -n shovel-heroes-staging
# View pod logs
kubectl logs -f deployment/backend-staging -n shovel-heroes-staging
kubectl logs -f deployment/frontend-staging -n shovel-heroes-staging
```
### Service Status
```bash
# List services
kubectl get svc -n shovel-heroes-staging
# Check ingress
kubectl get ingress -n shovel-heroes-staging
# Describe ingress for ALB details
kubectl describe ingress shovel-heroes-staging -n shovel-heroes-staging
```
### Resource Usage
```bash
# CPU and Memory usage
kubectl top pods -n shovel-heroes-staging
# Node resource usage
kubectl top nodes
# Check resource quotas
kubectl describe resourcequota -n shovel-heroes-staging
```
### CloudWatch Logs
```bash
# List log groups
aws logs describe-log-groups --log-group-name-prefix "/aws/eks/shovel-heroes"
# Tail logs
aws logs tail /aws/eks/shovel-heroes/cluster --follow
# Query logs (last 1 hour)
aws logs filter-log-events \
--log-group-name "/aws/eks/shovel-heroes/cluster" \
--start-time $(date -u -d '1 hour ago' +%s)000 \
--filter-pattern "ERROR"
```
### Application Metrics
Access CloudWatch dashboards:
- EKS Cluster: https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=EKS-shovel-heroes
- RDS Database: https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=RDS-shovel-heroes
- ALB: https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=ALB-shovel-heroes
---
## Troubleshooting
### Pods Not Starting
**Symptoms**: Pods stuck in `Pending`, `CrashLoopBackOff`, or `ImagePullBackOff`
**Diagnosis**:
```bash
# Check pod events
kubectl describe pod <pod-name> -n shovel-heroes-staging
# Check pod logs
kubectl logs <pod-name> -n shovel-heroes-staging
```
**Common Causes & Solutions**:
1. **ImagePullBackOff**: Cannot pull Docker image from ECR
- Verify ECR repository exists: `aws ecr describe-repositories`
- Check image tag exists: `aws ecr list-images --repository-name shovel-heroes-backend`
- Verify IRSA permissions for ECR pull
2. **CrashLoopBackOff**: Application crashes on startup
- Check logs: `kubectl logs <pod-name> -n shovel-heroes-staging`
- Common issues:
- Missing environment variables (DATABASE_URL, JWT_SECRET)
- Database connection failed
- Invalid configuration
3. **Pending**: Insufficient cluster resources
- Check node status: `kubectl get nodes`
- Check resource requests: `kubectl describe pod <pod-name> -n shovel-heroes-staging`
- Scale cluster if needed (via Terraform)
### Database Connection Failed
**Symptoms**: Backend logs show `ECONNREFUSED` or `timeout`
**Diagnosis**:
```bash
# Check database secret
kubectl get secret shovel-heroes-secrets -n shovel-heroes-staging -o jsonpath='{.data.database-url}' | base64 -d
# Test database connectivity from pod
kubectl exec -it deployment/backend-staging -n shovel-heroes-staging -- sh
# Inside pod:
# wget -qO- http://localhost:8787/healthz
```
**Solutions**:
1. Verify DATABASE_URL format: `postgres://user:pass@host:5432/dbname`
2. Check RDS security group allows traffic from EKS pods
3. Verify RDS instance is running: `aws rds describe-db-instances`
### ALB Not Routing Traffic
**Symptoms**: `502 Bad Gateway` or `504 Gateway Timeout`
**Diagnosis**:
```bash
# Check ALB target health
aws elbv2 describe-target-health --target-group-arn <BACKEND_TG_ARN>
aws elbv2 describe-target-health --target-group-arn <FRONTEND_TG_ARN>
# Check ingress status
kubectl describe ingress shovel-heroes-staging -n shovel-heroes-staging
```
**Solutions**:
1. **Targets unhealthy**: Health check failing
- Verify pods are responding to health check path
- Check health check configuration in target group
2. **No targets registered**: Ingress controller issue
- Verify AWS Load Balancer Controller is running
- Check ingress annotations
- Ensure target-type is `ip` (not `instance`)
### High Memory Usage
**Symptoms**: Pods restarting due to OOM (Out of Memory)
**Diagnosis**:
```bash
# Check resource usage
kubectl top pods -n shovel-heroes-staging
# Check events for OOMKilled
kubectl get events -n shovel-heroes-staging --sort-by='.lastTimestamp'
```
**Solutions**:
1. Increase memory limits in deployment:
```yaml
resources:
limits:
memory: 2Gi # Increase from 1Gi
```
2. Optimize application memory usage
3. Scale horizontally (increase replicas)
### Deployment Stuck
**Symptoms**: Deployment shows `Progressing` but never completes
**Diagnosis**:
```bash
# Check rollout status
kubectl rollout status deployment/backend-staging -n shovel-heroes-staging
# Check deployment events
kubectl describe deployment backend-staging -n shovel-heroes-staging
```
**Solutions**:
1. **Readiness probe failing**: Pods not becoming ready
- Check pod logs for startup errors
- Adjust readiness probe timing (increase `initialDelaySeconds`)
2. **ImagePullBackOff**: Fix image pull issue (see above)
3. **Force rollout restart**:
```bash
kubectl rollout restart deployment/backend-staging -n shovel-heroes-staging
```
---
## Environment Variables
### Staging Environment
| Variable | Source | Example |
|----------|--------|---------|
| `NODE_ENV` | Hardcoded | `production` |
| `PORT` | Hardcoded | `8787` (backend), `80` (frontend) |
| `DATABASE_URL` | Kubernetes Secret | `postgres://...` |
| `JWT_SECRET` | Kubernetes Secret | `<random-string>` |
| `API_KEY` | Kubernetes Secret | `<random-string>` |
| `AWS_REGION` | IRSA (automatic) | `us-east-1` |
| `AWS_ROLE_ARN` | IRSA (automatic) | `arn:aws:iam::...` |
### Production Environment
Same as staging, but with production-specific values:
- Database points to production RDS
- Secrets from production Secrets Manager
- Higher resource limits
### Updating Environment Variables
**For secrets**:
```bash
# Update Kubernetes secret
kubectl create secret generic shovel-heroes-secrets \
--from-literal=jwt-secret="<new-value>" \
--dry-run=client -o yaml | kubectl apply -f -
# Restart deployment to pick up new secret
kubectl rollout restart deployment/backend-staging -n shovel-heroes-staging
```
**For non-secrets** (edit deployment):
```bash
kubectl edit deployment backend-staging -n shovel-heroes-staging
# Or update via kustomize patch and reapply
```
---
## Emergency Contacts
- **On-call Engineer**: [Your team's PagerDuty/Slack]
- **AWS Support**: https://console.aws.amazon.com/support
- **GitHub Issues**: https://github.com/YOUR_ORG/shovel-heroes-k8s/issues
## Related Documentation
- [GitHub Secrets Setup](./GITHUB_SECRETS_SETUP.md)
- [Architecture Design](../.kiro/specs/deploy-to-eks/design.md)
- [Terraform Infrastructure](https://github.com/YOUR_ORG/shovel-heroes-terraform)
- [Main README](../README.md)
## /docs/DEPLOY_WORKFLOW_PARAMETERIZATION.md
# Deploy Workflow Parameterization Guide
## Problem Statement
The current `deploy.yml` workflow contains hardcoded AWS ARNs that:
1. **Cause CI/CD failures** in upstream repo (shovel-heroes-org) because AWS credentials don't exist
2. **Expose account-specific information** (AWS Account ID: 447407243904)
3. **Prevent other contributors** from using the workflow with their own AWS infrastructure
As noted in PR #103 review by Copilot: **"Avoid hardcoding Secrets Manager ARNs"** and **"Move secret ARNs to GitHub environment secrets"**.
## Analysis of Current Hardcoded Values
### 1. In `.github/workflows/deploy.yml`
#### Lines 95-109: AWS Secrets Manager ARNs (3 hardcoded ARNs)
```yaml
DB_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "arn:aws:secretsmanager:ap-east-2:447407243904:secret:shovel-heros-staging-db-2025100214083654490000000c-89ytP5" \
--region ap-east-2 \
--query SecretString --output text)
APP_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "arn:aws:secretsmanager:ap-east-2:447407243904:secret:shovel-heros-staging-app-2025100214083654480000000a-PnHCMb" \
--region ap-east-2 \
--query SecretString --output text)
JWT_SECRET_OBJ=$(aws secretsmanager get-secret-value \
--secret-id "arn:aws:secretsmanager:ap-east-2:447407243904:secret:shovel-heros-staging-jwt-2025100214083654490000000e-cnB4Sa" \
--region ap-east-2 \
--query SecretString --output text)
```
**Issue**: These ARNs are specific to one AWS account and will fail in any other environment.
#### Line 134: Backend Pod IAM Role ARN
```yaml
BACKEND_POD_ROLE_ARN: arn:aws:iam::447407243904:role/shovel-heros-staging-backend-20251002142616455400000002
```
**Issue**: This IRSA (IAM Role for Service Account) role ARN is hardcoded.
### 2. In `k8s/base/backend-targetgroupbinding.yaml`
#### Line 10: Backend Target Group ARN
```yaml
targetGroupARN: arn:aws:elasticloadbalancing:ap-east-2:447407243904:targetgroup/be-20251002135523905000000015/d93c0dca3c31cb27
```
### 3. In `k8s/base/frontend-targetgroupbinding.yaml`
#### Line 10: Frontend Target Group ARN
```yaml
targetGroupARN: arn:aws:elasticloadbalancing:ap-east-2:447407243904:targetgroup/fe-20251003172223376900000001/4de94ad42136c343
```
**Issue**: These Target Group ARNs tie the deployment to a specific ALB configuration.
### 4. In `k8s/base/serviceaccount.yaml`
#### Line 8: Placeholder for Backend Pod Role
```yaml
eks.amazonaws.com/role-arn: BACKEND_POD_ROLE_ARN
```
**Good**: This is a placeholder that gets replaced by the workflow. This pattern is correct.
### 5. In `k8s/base/*-deployment.yaml`
#### Image URLs as Placeholders
```yaml
image: BACKEND_ECR_URL:latest
image: FRONTEND_ECR_URL:latest
```
**Good**: These are placeholders replaced by workflow. This pattern is correct.
## What Should NOT Change
✅ **Keep these elements unchanged:**
1. **Workflow structure**: The order and logic of steps is well-designed
2. **OIDC authentication**: Using `secrets.AWS_GITHUB_ACTIONS_ROLE_ARN` is correct
3. **Docker build process**: Multi-stage builds are optimized
4. **kubectl commands**: Apply manifests in correct order
5. **Health checks**: Verification steps are comprehensive
6. **Environment variables at top** (lines 14-18): These are acceptable as-is:
- `AWS_REGION: ap-east-2`
- `EKS_CLUSTER_NAME: eks-shovel-heros`
- `ECR_BACKEND_REPO: shovel-heros-staging-shovel-heroes-backend`
- `ECR_FRONTEND_REPO: shovel-heros-staging-shovel-heroes-frontend`
**Why?** These are deployment-specific settings that define WHERE to deploy. Different forks will have different values. Moving these to secrets adds complexity without benefit.
## What MUST Be Parameterized
❌ **Move to GitHub Secrets:**
| Hardcoded Value | Location | Proposed Secret Name | Reason |
|----------------|----------|---------------------|---------|
| Database Secret ARN | deploy.yml:96 | `AWS_DB_SECRET_ARN` | Contains AWS account ID |
| Application Secret ARN | deploy.yml:101 | `AWS_APP_SECRET_ARN` | Contains AWS account ID |
| JWT Secret ARN | deploy.yml:107 | `AWS_JWT_SECRET_ARN` | Contains AWS account ID |
| Backend Pod IAM Role ARN | deploy.yml:134 | `BACKEND_POD_ROLE_ARN` | Account-specific IRSA role |
| Backend Target Group ARN | backend-targetgroupbinding.yaml:10 | `BACKEND_TARGET_GROUP_ARN` | ALB-specific identifier |
| Frontend Target Group ARN | frontend-targetgroupbinding.yaml:10 | `FRONTEND_TARGET_GROUP_ARN` | ALB-specific identifier |
## Solution: Add Conditional Execution
### Step 1: Add condition to skip workflow when secrets missing
```yaml
jobs:
deploy:
name: Build and Deploy
runs-on: ubuntu-latest
# Only run if AWS credentials are configured
if: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN != '' }}
```
**Effect**:
- ✅ Upstream repo (no secrets): Workflow skipped → CI/CD passes
- ✅ Forks with secrets: Workflow runs → Deployment succeeds
### Step 2: Replace hardcoded ARNs with secrets
**In deploy.yml:**
```yaml
# Replace lines 95-109 with:
DB_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_DB_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
APP_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_APP_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
JWT_SECRET_OBJ=$(aws secretsmanager get-secret-value \
--secret-id "${{ secrets.AWS_JWT_SECRET_ARN }}" \
--region ${{ env.AWS_REGION }} \
--query SecretString --output text)
# Replace line 134:
BACKEND_POD_ROLE_ARN: ${{ secrets.BACKEND_POD_ROLE_ARN }}
```
**In TargetGroupBindings:**
```yaml
# Use Kustomize patches or sed replacement in workflow
sed -i "s|BACKEND_TGB_ARN|${{ secrets.BACKEND_TARGET_GROUP_ARN }}|g" k8s/base/backend-targetgroupbinding.yaml
sed -i "s|FRONTEND_TGB_ARN|${{ secrets.FRONTEND_TARGET_GROUP_ARN }}|g" k8s/base/frontend-targetgroupbinding.yaml
```
## Setup Instructions for Repository Admin
### For Upstream Repository (shovel-heroes-org/shovel-heroes)
**Option A: Keep workflow disabled (Recommended)**
- No action needed
- Workflow will skip automatically
- Individual contributors enable in their forks
**Option B: Enable deployment from upstream**
- Requires setting up AWS infrastructure (EKS, RDS, ALB, etc.)
- See full setup guide below
### For Individual Forks (Contributors with AWS)
Go to **your fork's Settings → Secrets and variables → Actions**
#### Required Repository Secrets:
| Secret Name | How to Get Value | Example |
|-------------|------------------|---------|
| `AWS_GITHUB_ACTIONS_ROLE_ARN` | Terraform: `terraform output github_actions_role_arn` | `arn:aws:iam::123456789012:role/github-actions` |
| `AWS_DB_SECRET_ARN` | Terraform: `terraform output database_secret_arn` | `arn:aws:secretsmanager:region:account:secret:db-xxx` |
| `AWS_APP_SECRET_ARN` | Terraform: `terraform output application_secret_arn` | `arn:aws:secretsmanager:region:account:secret:app-xxx` |
| `AWS_JWT_SECRET_ARN` | Terraform: `terraform output jwt_secret_arn` | `arn:aws:secretsmanager:region:account:secret:jwt-xxx` |
| `BACKEND_POD_ROLE_ARN` | Terraform: `terraform output backend_pod_role_arn` | `arn:aws:iam::123456789012:role/backend-pod` |
| `BACKEND_TARGET_GROUP_ARN` | Terraform: `terraform output backend_target_group_arn` | `arn:aws:elasticloadbalancing:region:account:targetgroup/xxx` |
| `FRONTEND_TARGET_GROUP_ARN` | Terraform: `terraform output frontend_target_group_arn` | `arn:aws:elasticloadbalancing:region:account:targetgroup/xxx` |
#### Update Environment Variables in deploy.yml:
Edit lines 14-18 to match your AWS setup:
```yaml
env:
AWS_REGION: us-east-1 # Your AWS region
EKS_CLUSTER_NAME: your-cluster-name
ECR_BACKEND_REPO: your-backend-repo
ECR_FRONTEND_REPO: your-frontend-repo
```
### Getting Terraform Outputs
If you're using the `shovel-heroes-terraform` repository:
```bash
cd /path/to/shovel-heroes-terraform
# Get all required values at once
terraform output -json | jq -r '
{
"AWS_GITHUB_ACTIONS_ROLE_ARN": .github_actions_role_arn.value,
"AWS_DB_SECRET_ARN": .database_secret_arn.value,
"AWS_APP_SECRET_ARN": .application_secret_arn.value,
"AWS_JWT_SECRET_ARN": .jwt_secret_arn.value,
"BACKEND_POD_ROLE_ARN": .backend_pod_role_arn.value,
"BACKEND_TARGET_GROUP_ARN": .backend_target_group_arn.value,
"FRONTEND_TARGET_GROUP_ARN": .frontend_target_group_arn.value
}'
```
## Testing the Fix
### Test 1: Upstream repository (no secrets)
```bash
# Push to main
git push upstream main
# Expected: Workflow is skipped (not failed)
gh run list --repo shovel-heroes-org/shovel-heroes --limit 1
# Status should show: "skipped" or workflow doesn't appear
```
### Test 2: Fork with AWS setup (secrets configured)
```bash
# Push to your fork's main
git push origin main
# Expected: Workflow runs and deploys
gh run watch
# Should see: Build images → Push to ECR → Deploy to EKS → Health checks pass
```
## Migration Path
For existing deployments using hardcoded values:
1. ✅ Add GitHub secrets to repository
2. ✅ Update deploy.yml to use secrets
3. ✅ Test workflow with manual trigger
4. ✅ Verify deployment succeeds
5. ✅ Update TargetGroupBinding manifests with placeholders
6. ✅ Create PR to upstream with parameterized workflow
## Security Benefits
✅ **No AWS account IDs in code**
✅ **No secret ARNs in version control**
✅ **Easy to rotate secrets** (update GitHub secret only)
✅ **Per-fork configuration** (each contributor uses their own AWS)
✅ **Audit trail** (GitHub tracks secret updates)
## Related Documentation
- [GITHUB_SECRETS_SETUP.md](./GITHUB_SECRETS_SETUP.md) - Detailed secrets configuration guide
- [DEPLOYMENT_RUNBOOK.md](./DEPLOYMENT_RUNBOOK.md) - Operational deployment procedures
- [shovel-heroes-terraform](https://github.com/yi-john-huang/shovel-heroes-terraform) - AWS infrastructure as code
## Summary
**Current State**:
- ❌ Hardcoded ARNs cause CI/CD failures in upstream
- ❌ Exposes AWS account information
- ❌ Not reusable by other contributors
**After Fix**:
- ✅ Workflow skips when secrets missing (upstream passes CI/CD)
- ✅ No AWS account info in code
- ✅ Contributors can use with their own AWS accounts
- ✅ Follows GitHub Actions best practices
## /docs/GITHUB_SECRETS_SETUP.md
# GitHub Secrets Configuration Guide
This guide explains how to configure GitHub repository secrets and environments for the Shovel Heroes EKS deployment pipeline.
## Prerequisites
Before configuring GitHub secrets, ensure that **Phase 0 Terraform tasks** have been completed in the `shovel-heroes-terraform` repository:
- ✅ GitHub OIDC provider created
- ✅ Frontend target group added to ALB
- ✅ S3/CloudFront resources removed
## Step 1: Get Terraform Outputs
First, retrieve the necessary values from your Terraform infrastructure:
```bash
cd /Users/yihuang/workspace/shovel-heroes-terraform
# Get all required values
terraform output github_actions_role_arn
terraform output backend_pod_role_arn
terraform output database_secret_arn
terraform output application_secret_arn
terraform output eks_cluster_name
terraform output alb_dns_name
terraform output backend_target_group_arn
terraform output frontend_target_group_arn
```
Save these values - you'll need them for the next steps.
## Step 2: Configure Repository Secrets
Go to your GitHub repository: **Settings → Secrets and variables → Actions → New repository secret**
Add the following **repository secrets** (available to all workflows):
| Secret Name | Value | Description |
|-------------|-------|-------------|
| `AWS_GITHUB_ACTIONS_ROLE_ARN` | `terraform output github_actions_role_arn` | IAM role ARN for OIDC authentication |
## Step 3: Create GitHub Environments
### 3.1 Create Staging Environment
1. Go to **Settings → Environments → New environment**
2. Name: `staging`
3. Click **Configure environment**
4. **Do not** add protection rules (auto-deploy on push to main)
5. Click **Add secret** to add environment-specific secrets:
| Secret Name | Value | Description |
|-------------|-------|-------------|
| `EKS_CLUSTER_NAME` | `shovel-heroes-staging` | EKS cluster name for staging |
| `BACKEND_POD_ROLE_ARN` | `terraform output backend_pod_role_arn` | IAM role for backend pods (IRSA) |
| `DATABASE_SECRET_ARN` | `terraform output database_secret_arn` | Secrets Manager ARN for database credentials |
| `APPLICATION_SECRET_ARN` | `terraform output application_secret_arn` | Secrets Manager ARN for app secrets (JWT, API keys) |
| `BACKEND_TARGET_GROUP_ARN` | `terraform output backend_target_group_arn` | ALB target group for backend |
| `FRONTEND_TARGET_GROUP_ARN` | `terraform output frontend_target_group_arn` | ALB target group for frontend |
### 3.2 Create Production Environment
1. Go to **Settings → Environments → New environment**
2. Name: `production`
3. Click **Configure environment**
4. **Enable** protection rules:
- ✅ Required reviewers: Add yourself and/or team members
- ✅ Wait timer: 0 minutes (optional: add delay)
- ✅ Deployment branches: Selected branches → Add `main`
5. Click **Add secret** to add environment-specific secrets:
| Secret Name | Value | Description |
|-------------|-------|-------------|
| `EKS_CLUSTER_NAME` | `shovel-heroes-production` | EKS cluster name for production |
| `BACKEND_POD_ROLE_ARN` | `terraform output backend_pod_role_arn` | IAM role for backend pods (IRSA) |
| `DATABASE_SECRET_ARN` | `terraform output database_secret_arn` | Secrets Manager ARN for database credentials |
| `APPLICATION_SECRET_ARN` | `terraform output application_secret_arn` | Secrets Manager ARN for app secrets (JWT, API keys) |
| `BACKEND_TARGET_GROUP_ARN` | `terraform output backend_target_group_arn` | ALB target group for backend |
| `FRONTEND_TARGET_GROUP_ARN` | `terraform output frontend_target_group_arn` | ALB target group for frontend |
## Step 4: Update Workflow Files
### 4.1 Update Staging Workflow
Edit `.github/workflows/deploy-staging.yml`:
```yaml
# Update these environment variables:
env:
AWS_REGION: us-east-1 # Your AWS region
ECR_BACKEND_REPO: shovel-heroes-backend
ECR_FRONTEND_REPO: shovel-heroes-frontend
# Ensure role ARN uses the secret:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
# Update EKS cluster name:
- name: Update kubeconfig for EKS
run: |
aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ secrets.EKS_CLUSTER_NAME }}
```
### 4.2 Update Production Workflow
Edit `.github/workflows/deploy-production.yml`:
```yaml
# Same updates as staging workflow
# Ensure environment is set to "production":
jobs:
deploy-production:
environment: production # This triggers approval requirements
```
### 4.3 Update Kubernetes Manifests
The following placeholders in `k8s/base/` need to be replaced via workflow or Kustomize:
**In `serviceaccount.yaml`:**
- `BACKEND_POD_ROLE_ARN` → Use environment secret
**In `backend-deployment.yaml` and `frontend-deployment.yaml`:**
- `BACKEND_ECR_URL` → Set by workflow via Kustomize
- `FRONTEND_ECR_URL` → Set by workflow via Kustomize
**In `ingress.yaml`:**
- `ALB_NAME` → Get from Terraform output `alb_dns_name`
- `BACKEND_TARGET_GROUP_ARN` → Use environment secret
- `FRONTEND_TARGET_GROUP_ARN` → Use environment secret
## Step 5: Verify Configuration
### 5.1 Check Secrets
```bash
# List configured secrets (won't show values, just names)
gh secret list
# List environments
gh api repos/:owner/:repo/environments | jq '.environments[].name'
```
### 5.2 Test OIDC Authentication
Trigger the staging workflow manually to test OIDC authentication:
1. Go to **Actions → Deploy to Staging (EKS) → Run workflow**
2. Monitor the workflow execution
3. Check the "Configure AWS credentials via OIDC" step
4. Should see: `✓ Assumed role successfully`
If it fails with "Not authorized to perform sts:AssumeRoleWithWebIdentity":
- Verify the GitHub OIDC provider trust policy in Terraform
- Ensure it matches: `repo:YOUR_ORG/shovel-heroes-k8s:*`
- Check the IAM role ARN is correct
## Step 6: Create Kubernetes Secrets
The workflows will create these secrets automatically, but you can create them manually if needed:
```bash
# Get database credentials from Secrets Manager
DB_SECRET=$(aws secretsmanager get-secret-value --secret-id <DATABASE_SECRET_ARN> --query SecretString --output text)
DB_URL=$(echo $DB_SECRET | jq -r '.url')
# Get application secrets
APP_SECRET=$(aws secretsmanager get-secret-value --secret-id <APPLICATION_SECRET_ARN> --query SecretString --output text)
JWT_SECRET=$(echo $APP_SECRET | jq -r '.jwt_secret')
API_KEY=$(echo $APP_SECRET | jq -r '.api_key')
# Create Kubernetes secret
kubectl create secret generic shovel-heroes-secrets \
--from-literal=database-url="$DB_URL" \
--from-literal=jwt-secret="$JWT_SECRET" \
--from-literal=api-key="$API_KEY" \
-n shovel-heroes-staging
```
## Troubleshooting
### OIDC Authentication Fails
**Error**: `Not authorized to perform sts:AssumeRoleWithWebIdentity`
**Solution**:
1. Check the trust policy in `iam_github_actions.tf`
2. Verify repository name matches: `repo:YOUR_ORG/shovel-heroes-k8s:*`
3. Ensure OIDC provider thumbprint is correct
### Secret Not Found
**Error**: `Secret not found: AWS_GITHUB_ACTIONS_ROLE_ARN`
**Solution**:
1. Verify secret name exactly matches (case-sensitive)
2. Check if secret is in repository secrets (not environment)
3. Re-add the secret if needed
### EKS Access Denied
**Error**: `Error from server (Forbidden): deployments.apps is forbidden`
**Solution**:
1. Verify the GitHub Actions IAM role has EKS permissions
2. Check the role is in the EKS cluster's aws-auth ConfigMap
3. Update aws-auth if needed:
```bash
kubectl edit configmap aws-auth -n kube-system
# Add under mapRoles:
- rolearn: <GITHUB_ACTIONS_ROLE_ARN>
username: github-actions
groups:
- system:masters
```
### Database Connection Failed
**Error**: `ECONNREFUSED` or timeout connecting to database
**Solution**:
1. Verify security group allows backend pods to access RDS
2. Check DATABASE_URL secret is correctly formatted
3. Verify backend pods have the IRSA role annotation
## Security Best Practices
1. **Never commit secrets** to the repository
2. **Rotate secrets regularly** (JWT, API keys, database passwords)
3. **Use least-privilege IAM policies** for GitHub Actions role
4. **Enable branch protection** on `main` branch
5. **Require PR reviews** before merging to main
6. **Audit secrets access** via CloudTrail logs
7. **Use separate AWS accounts** for staging and production (recommended)
## Next Steps
After configuring secrets:
1. ✅ Test staging deployment: Push to `main` branch
2. ✅ Verify pods are running: `kubectl get pods -n shovel-heroes-staging`
3. ✅ Test backend API: `curl http://{alb-dns}/healthz`
4. ✅ Test frontend: `curl http://{alb-dns}/`
5. ✅ Monitor logs: Check CloudWatch Logs
6. ✅ Test production deployment: Create a release or trigger manually
For deployment procedures, see [DEPLOYMENT_RUNBOOK.md](./DEPLOYMENT_RUNBOOK.md).
## /docs/SECURITY.md
# 鏟子英雄|安全政策(Security Policy)
本專案旨在支援花蓮等救災現場的資訊協作。請所有貢獻者與使用者務必遵循以下安全與隱私原則,以降低對民眾、志工與系統的風險。
## 範圍(Scope)
- 前端:Vite + React SPA(本倉庫)
- 後端/BFF(未來規劃):提供任務、通報、志工報到、物資點等 API
- 資料:志工基本資料、聯絡方式、即時定位(如有)、任務內容與狀態、物資庫存與地點等
## 資料分級(Data Classification)
- **機敏個資(Highly Sensitive PII)**:電話、Email、GPS 精確座標、身分證影本(若有)
- **一般個資(PII)**:姓名、鄉鎮區、技能標籤
- **非個資**:匿名統計、公開公告資訊
> 原則:僅蒐集任務所需最小必要資料(data minimization),預設最短保留、明確告知用途與保存期。
## 威脅情境(Threats)
- 濫用/灌報(大規模機器人重複提交、惡意洗單)
- 資料外洩(含 Git 泄漏金鑰、前端打包秘鑰)
- 偽報點位(錯誤坐標/釣魚連結)
- 常見 Web 漏洞:XSS、CSRF、IDOR、弱認證、錯誤處理洩漏
## 最低安全基線(Minimum Security Baseline)
- **輸入驗證**:所有表單字段做型別與長度驗證;URL、座標與電話採白名單格式與正規表示式。
- **認證/授權(如後端啟用)**:Bearer Token/OIDC;所有修改與查詢操作皆做資源層授權檢查(避免 IDOR)。
- **錯誤處理**:對外錯誤訊息不含堆疊與內部細節;統一以追蹤 ID 連結伺服端日誌。
- **日誌與稽核**:存取、失敗操作(含 401/403/429)與管理動作均記錄;避免記錄純文本敏感資訊。
- **通訊安全**:一律 HTTPS;設定 HSTS、正確的 CORS(來源白名單、限制方法與憑證)。
- **前端防濫用**:啟用 **人機驗證(Turnstile)** 或等效機制於高風險表單(志工報名、通報、回報)。
- **率限制與冪等**:伺服端對敏感端點實作頻率限制;要求用戶端在 POST 提交提供 `Idempotency-Key` 以避免重複提交。
- **Secrets 管理**:金鑰僅放 `.env`(不入版控);使用 GitHub Environments/Actions Secrets;定期輪替。
- **依賴安全**:啟用 GitHub Dependabot 與安全警示;每週審視高風險 CVE。
- **第三方地圖/外掛**:僅允許必要來源,使用 SRI/版本鎖定,避免動態注入 HTML。
## 漏洞通報(Vulnerability Disclosure)
- 請優先使用 GitHub 的 **Security Advisories** 私下回報(若有權限),或寄信至 **shovel-heroes@noreply.github.com**。
- 我們承諾:
- **72 小時內**回覆初步受理。
- **7–30 天**內提供修補或緩解計畫(依嚴重度)。
- 採「協調式揭露」(Coordinated Disclosure);雙方同意後公開詳情與致謝。
- 建議同時在網站根目錄布署 `/.well-known/security.txt` 指引外部研究員正確回報(依 RFC 9116)。
## 法規與準則
- 依台灣《個人資料保護法(PDPA)》與相關子法辦理(僅蒐集業務所需、告知義務、資料主體權利)。
- 研發參考 **OWASP ASVS** 與 **OWASP API/REST Cheat Sheets** 作為安全開發基準。
## 回報格式建議
請包含:影響範圍、重現步驟(POC)、建議修補方向、是否涉及個資、是否可被自動化濫用。若涉及機敏資料,請使用加密附件並事先索取公鑰。
## /eslint.config.js
```js path="/eslint.config.js"
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
// Ignore build output folders everywhere
{ ignores: ['dist', '**/dist/**'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// Disable prop-types enforcement since project uses TypeScript-generated types / not using prop-types
'react/prop-types': 'off',
// User request: disable unused vars rule (may hide real issues; consider re‑enabling later)
'no-unused-vars': 'off',
// User request: disable undefined variable check (environment globals like process, require)
'no-undef': 'off',
// User request: allow custom / non-standard jsx attributes (e.g. cmdk-* or toast-* data hooks)
'react/no-unknown-property': 'off',
},
},
]
```
## /index.html
```html path="/index.html"
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="src/image/Shovel_Hero_Logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>鏟子英雄 (Shovel Heroes)</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
```
## /jsconfig.json
```json path="/jsconfig.json"
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx"
},
"include": ["src/**/*.js", "src/**/*.jsx"]
}
```
## /k8s/base/backend-service.yaml
```yaml path="/k8s/base/backend-service.yaml"
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: shovel-heroes
labels:
app.kubernetes.io/name: shovel-heroes
app.kubernetes.io/component: backend
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: shovel-heroes
app.kubernetes.io/component: backend
ports:
- name: http
port: 8787
targetPort: 8787
protocol: TCP
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.