From 7d84d3f86fc53f354bbfd10a2a75c250eac218d8 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Mon, 22 Jun 2026 13:21:39 -0300 Subject: [PATCH 01/11] fix: add GitHub token to API requests to avoid rate limit VITE_GITHUB_TOKEN injected at build time raises limit from 60 to 5000 req/hour. Falls back to unauthenticated if token is not set. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/deploy-frontend.yml | 1 + frontend/src/hooks/useGithubActivity.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index b647ece..f0aaef5 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -32,6 +32,7 @@ jobs: working-directory: frontend env: VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} + VITE_GITHUB_TOKEN: ${{ secrets.VITE_GITHUB_TOKEN }} run: npm run build - name: Configure AWS credentials diff --git a/frontend/src/hooks/useGithubActivity.ts b/frontend/src/hooks/useGithubActivity.ts index 5df873a..31b8e7d 100644 --- a/frontend/src/hooks/useGithubActivity.ts +++ b/frontend/src/hooks/useGithubActivity.ts @@ -45,6 +45,14 @@ function writeCache(data: GithubActivity) { } } +const GITHUB_TOKEN = import.meta.env.VITE_GITHUB_TOKEN as string | undefined + +function githubHeaders(): HeadersInit { + return GITHUB_TOKEN + ? { Authorization: `Bearer ${GITHUB_TOKEN}`, Accept: 'application/vnd.github+json' } + : { Accept: 'application/vnd.github+json' } +} + export function useGithubActivity(username: string) { const [data, setData] = useState(() => readCache()) const [loading, setLoading] = useState(() => readCache() === null) @@ -56,6 +64,7 @@ export function useGithubActivity(username: string) { try { const reposRes = await fetch( `https://api.github.com/users/${username}/repos?sort=updated&per_page=8&type=public`, + { headers: githubHeaders() }, ) if (!reposRes.ok) return // rate limited or error — keep showing cached/empty const rawRepos = await reposRes.json() @@ -75,6 +84,7 @@ export function useGithubActivity(username: string) { repos.map(async (repo) => { const res = await fetch( `https://api.github.com/repos/${username}/${repo.name}/commits?per_page=2`, + { headers: githubHeaders() }, ) if (!res.ok) return [] const raw = await res.json() From 536b229689f49972b0d824af7e8ea55bdd4025af Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Mon, 22 Jun 2026 13:43:06 -0300 Subject: [PATCH 02/11] fix: handle empty head_commit.message in workflow_dispatch SSM step Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/deploy-frontend.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index a5df779..4c6bbf7 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -59,9 +59,11 @@ jobs: --paths "/*" - name: Write deploy info to SSM + env: + COMMIT_MSG: ${{ github.event.head_commit.message || 'manual deploy' }} run: | aws ssm put-parameter --name /portfolio/version \ - --value "${{ github.sha }}" --type String --overwrite + --value "$GITHUB_SHA" --type String --overwrite aws ssm put-parameter --name /portfolio/last-deploy \ --value "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --type String --overwrite @@ -70,4 +72,4 @@ jobs: --value "${GITHUB_SHA::7}" --type String --overwrite aws ssm put-parameter --name /portfolio/last-commit-message \ - --value "${{ github.event.head_commit.message }}" --type String --overwrite + --value "${COMMIT_MSG}" --type String --overwrite From bb2e616c6e98baabb00f69c1dfa1faec613f2541 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Mon, 22 Jun 2026 13:48:43 -0300 Subject: [PATCH 03/11] fix: last commit message in two lines with line-clamp-2 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/InfraStatusPanel.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/InfraStatusPanel.tsx b/frontend/src/components/InfraStatusPanel.tsx index b029f45..1f289f0 100644 --- a/frontend/src/components/InfraStatusPanel.tsx +++ b/frontend/src/components/InfraStatusPanel.tsx @@ -101,14 +101,12 @@ export function InfraStatusPanel() {

{i.lastCommitLabel}

-
- - {status.lastCommit.sha.slice(0, 7)} - - - {status.lastCommit.message} - -
+ + {status.lastCommit.sha.slice(0, 7)} + +

+ {status.lastCommit.message} +

)} From 291677abe4b9ae9df4f341a89497fc6eb49b5d56 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Wed, 24 Jun 2026 14:46:44 -0300 Subject: [PATCH 04/11] fix: align PT/EN text and theme icon vertically in navbar Height of LangSlot matches icon size-4 (16px); both buttons use flex items-center for consistent centering. --- frontend/src/components/Navbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 675213c..3f17a8e 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -13,7 +13,7 @@ function LangSlot({ lang }: { lang: string }) { return ( Date: Wed, 24 Jun 2026 14:53:56 -0300 Subject: [PATCH 05/11] fix: remove icon spin animation from theme toggle button to prevent flash --- frontend/src/components/Navbar.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 3f17a8e..6a1de45 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -95,17 +95,7 @@ export function Navbar() { aria-label="Toggle theme" className="flex items-center justify-center rounded-lg p-2 text-zinc-600 transition-colors hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50" > - - - {theme === 'dark' ? : } - - + {theme === 'dark' ? : } {/* Mobile menu button */} From 7be6cb6bae7055616ac06e1f9ce617b17535f7da Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:00:21 -0300 Subject: [PATCH 06/11] chore: upgrade Lambda runtime from nodejs20.x to nodejs22.x --- infra/lambda.tf | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/infra/lambda.tf b/infra/lambda.tf index ac90b80..0ca98f5 100644 --- a/infra/lambda.tf +++ b/infra/lambda.tf @@ -85,7 +85,7 @@ resource "aws_lambda_function" "status" { filename = data.archive_file.status_lambda.output_path source_code_hash = data.archive_file.status_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -105,7 +105,7 @@ resource "aws_lambda_function" "contact" { filename = data.archive_file.contact_lambda.output_path source_code_hash = data.archive_file.contact_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 15 tags = local.tags @@ -178,7 +178,7 @@ resource "aws_lambda_function" "settings" { filename = data.archive_file.settings_lambda.output_path source_code_hash = data.archive_file.settings_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -230,7 +230,7 @@ resource "aws_lambda_function" "resume" { filename = data.archive_file.resume_lambda.output_path source_code_hash = data.archive_file.resume_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 15 tags = local.tags @@ -249,7 +249,7 @@ resource "aws_lambda_function" "contacts_patch" { filename = data.archive_file.contacts_patch_lambda.output_path source_code_hash = data.archive_file.contacts_patch_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -267,7 +267,7 @@ resource "aws_lambda_function" "contacts_get" { filename = data.archive_file.contacts_lambda.output_path source_code_hash = data.archive_file.contacts_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -320,7 +320,7 @@ resource "aws_lambda_function" "video" { filename = data.archive_file.video_lambda.output_path source_code_hash = data.archive_file.video_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 15 tags = local.tags @@ -339,7 +339,7 @@ resource "aws_lambda_function" "visitors" { filename = data.archive_file.visitors_lambda.output_path source_code_hash = data.archive_file.visitors_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -383,7 +383,7 @@ resource "aws_lambda_function" "content" { filename = data.archive_file.content_lambda.output_path source_code_hash = data.archive_file.content_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags @@ -424,7 +424,7 @@ resource "aws_lambda_function" "admin_auth" { filename = data.archive_file.admin_auth_lambda.output_path source_code_hash = data.archive_file.admin_auth_lambda.output_base64sha256 handler = "index.handler" - runtime = "nodejs20.x" + runtime = "nodejs22.x" role = aws_iam_role.lambda_exec.arn timeout = 10 tags = local.tags From 6f20ee4e7e19360c759c50bc9454a75c58b075b3 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:03:44 -0300 Subject: [PATCH 07/11] chore: pin GitHub Actions to Node.js 24-compatible versions, bump build node to 22 --- .github/workflows/deploy-frontend.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index 2df9f18..77b87d0 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -15,14 +15,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v4.4.0 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: frontend/package-lock.json @@ -39,7 +39,7 @@ jobs: run: npm run build - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v4.1.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 39ed69fcf379808b029a5b20cb404ff7da135df0 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:15:01 -0300 Subject: [PATCH 08/11] chore: remove unused CloudFront Function (was UNASSOCIATED, never attached to any behavior) --- infra/cloudfront_api.tf | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/infra/cloudfront_api.tf b/infra/cloudfront_api.tf index a21022f..a06560d 100644 --- a/infra/cloudfront_api.tf +++ b/infra/cloudfront_api.tf @@ -1,18 +1,3 @@ -# ── CloudFront Function: strip /api prefix before forwarding to API Gateway ─── - -resource "aws_cloudfront_function" "api_rewrite" { - name = "${var.project_name}-api-rewrite-${var.environment}" - runtime = "cloudfront-js-2.0" - publish = true - code = <<-EOT - function handler(event) { - var request = event.request; - request.uri = request.uri.replace(/^\/api/, '') || '/'; - return request; - } - EOT -} - # ── Origin request policy: forward CloudFront-Viewer-Country to Lambda ──────── resource "aws_cloudfront_origin_request_policy" "api" { From ef3a04e1e761f31c9cacb527458d7831dc5a55cf Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:18:12 -0300 Subject: [PATCH 09/11] =?UTF-8?q?docs:=20update=20CLAUDE.md=20=E2=80=94=20?= =?UTF-8?q?Node=2022,=20pinned=20Actions=20versions,=20remove=20stale=20CF?= =?UTF-8?q?=20Function=20refs,=20add=20resource=20IDs=20and=20terraform=20?= =?UTF-8?q?targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/CLAUDE.md | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 84eaac0..1e28b6e 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -42,7 +42,7 @@ Cloud-native portfolio platform on AWS. React SPA frontend, fully serverless bac |---|---|---| | Hosting | S3 + CloudFront (OAC) | **live** | | CI/CD | GitHub Actions | **live** | -| API | API Gateway HTTP API + Lambda (Node.js 20) | **live** | +| API | API Gateway HTTP API + Lambda (Node.js 22) | **live** | | IaC | Terraform (`infra/`) | **live** | | Secrets / config | SSM Parameter Store | **live** | | Database | DynamoDB | **live** | @@ -60,12 +60,16 @@ Frontend connects via `VITE_API_BASE_URL`. When unset locally, all API calls ret ### CI/CD — `.github/workflows/deploy-frontend.yml` -Triggers on push to `production` (or `workflow_dispatch`). Steps: -1. `actions/checkout@v4` with `fetch-depth: 0` — full history for `git log --no-merges` -2. `npm ci` + `npm run build` (injects `VITE_*` secrets) -3. `aws s3 sync dist/ s3://$S3_BUCKET/` — assets with long cache, `index.html` no-cache -4. `aws cloudfront create-invalidation` — purges CDN cache -5. `aws ssm put-parameter` — writes last real commit (non-merge) SHA/message/date to `/portfolio/*` +Triggers on push to `production` **only for `frontend/**` path changes** (or `workflow_dispatch`). Steps: +1. `actions/checkout@v4.2.2` with `fetch-depth: 0` — full history for `git log --no-merges` +2. `actions/setup-node@v4.4.0` with `node-version: '22'` +3. `npm ci` + `npm run build` (injects `VITE_*` secrets) +4. `aws-actions/configure-aws-credentials@v4.1.0` +5. `aws s3 sync dist/ s3://$S3_BUCKET/` — assets with long cache, `index.html` no-cache +6. `aws cloudfront create-invalidation` — purges CDN cache +7. `aws ssm put-parameter` — writes last real commit (non-merge) SHA/message/date to `/portfolio/*` + +**Important:** CI/CD only deploys the frontend. Terraform/infra changes require manual `terraform apply` in `infra/`. GitHub secrets required: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `S3_BUCKET`, `CLOUDFRONT_DISTRIBUTION_ID`, `VITE_API_BASE_URL`, `VITE_GITHUB_TOKEN`, `VITE_ADMIN_PASSWORD`. @@ -80,7 +84,7 @@ infra/ lambda.tf # Lambda exec role + all Lambda functions + IAM policies api_gateway.tf # HTTP API, $default stage, "api" named stage (both throttled), # all routes + Lambda permissions, POST /admin/auth route - cloudfront_api.tf # CloudFront Function (not attached), origin request policy + cloudfront_api.tf # origin request policy # (whitelist: CloudFront-Viewer-Country + Authorization) dynamodb.tf # all DynamoDB tables (including rate_limit and admin_sessions) ses.tf # SES domain identity + Lambda SES send IAM policy @@ -94,6 +98,16 @@ Lambda hotfix without full CI/CD: terraform apply -target=aws_lambda_function. ``` +Lambda resource names in `lambda.tf`: `status`, `contact`, `contacts_get`, `contacts_patch`, `settings`, `resume`, `video`, `content`, `admin_auth`, `visitors`. + +Retrieve live resource IDs at any time: +```bash +terraform output +# cloudfront_distribution_id = "E3GN9C58SUEB3Q" +# s3_bucket_name = "cloud-portfolio-frontend-356892335394" +# api_gateway_url = "https://58l9thztmj.execute-api.us-east-1.amazonaws.com/" +``` + SSM parameters: - `/portfolio/version` — full git SHA of merge commit (written by CI) - `/portfolio/last-deploy` — ISO 8601 timestamp (written by CI) @@ -114,7 +128,11 @@ Setup: - ACM wildcard cert (`*.alessandro-bezerra.me` + SAN `alessandro-bezerra.me`) in `us-east-1`, DNS validated via Route 53 - CloudFront aliases currently `["portfolio.${domain_name}"]` only - Route 53 A alias records: `portfolio.` → CloudFront; `www.` and `@` records exist in Route 53 but are NOT in CloudFront aliases yet (blocked by Squarespace's old CloudFront distribution owning those CNAMEs) -- **When Squarespace releases the aliases** (verify with `aws cloudfront list-conflicting-aliases --alias www.alessandro-bezerra.me --distribution-id `): change `aliases` in `main.tf` to `["portfolio.${var.domain_name}", "www.${var.domain_name}", var.domain_name]` and run `terraform apply` +- **When Squarespace releases the aliases** — verify first: + ```bash + aws cloudfront list-conflicting-aliases --alias www.alessandro-bezerra.me --distribution-id E3GN9C58SUEB3Q + ``` + When `Quantity` returns `0`, change `aliases` in `main.tf` to `["portfolio.${var.domain_name}", "www.${var.domain_name}", var.domain_name]` and run `terraform apply` ### CloudFront dual-origin architecture @@ -126,7 +144,7 @@ CloudFront distribution has two origins: **Authorization forwarding is critical** — without it, Bearer tokens sent by the admin frontend never reach Lambda. The whitelist policy (`cloudfront_api.tf`) explicitly includes `Authorization`. -**API GW `api` named stage**: all Lambda routes are accessible at `/.execute-api.us-east-1.amazonaws.com/api/*`. This means a request to `portfolio.../api/visitors` hits CloudFront → APIGW origin → `api` stage → `/visitors` route — no URI rewriting needed. The CloudFront Function in `cloudfront_api.tf` exists in state but is NOT attached to any behavior. +**API GW `api` named stage**: all Lambda routes are accessible at `/.execute-api.us-east-1.amazonaws.com/api/*`. This means a request to `portfolio.../api/visitors` hits CloudFront → APIGW origin → `api` stage → `/visitors` route — no URI rewriting needed. The `$default` stage also exists (referenced in `outputs.tf` for the invoke URL) but receives no CloudFront traffic. **API Gateway throttling**: both `$default` and `api` stages have `throttling_burst_limit = 50`, `throttling_rate_limit = 20`. From c9db147a33fe938c94c63d4c94d492aee4587b05 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:20:40 -0300 Subject: [PATCH 10/11] chore: bump GitHub Actions to Node.js 24-native versions (checkout v7, setup-node v6, configure-aws-credentials v6) --- .github/workflows/deploy-frontend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index 77b87d0..17bb3b5 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -15,12 +15,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v7.0.0 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.4.0 with: node-version: '22' cache: 'npm' @@ -39,7 +39,7 @@ jobs: run: npm run build - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4.1.0 + uses: aws-actions/configure-aws-credentials@v6.2.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 4d6a4406a8658c48f3f109b98bfe4c3c97cd7a38 Mon Sep 17 00:00:00 2001 From: alessandrobezerra Date: Thu, 25 Jun 2026 18:21:43 -0300 Subject: [PATCH 11/11] chore: trigger workflow to validate Node.js 24 Actions upgrade --- frontend/CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 1e28b6e..c670a61 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -2,6 +2,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + + ## Git Workflow **Always commit to the `dev` branch.** Never commit directly to `production` or `main` without explicit human approval.