Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
7d84d3f
fix: add GitHub token to API requests to avoid rate limit
Narvaal Jun 22, 2026
90b63e0
Merge branch 'dev' into production
Narvaal Jun 22, 2026
536b229
fix: handle empty head_commit.message in workflow_dispatch SSM step
Narvaal Jun 22, 2026
76c3d95
Merge branch 'dev' into production
Narvaal Jun 22, 2026
bb2e616
fix: last commit message in two lines with line-clamp-2
Narvaal Jun 22, 2026
f8c5545
fix: resolve merge conflict — SHA inline, message wraps with line-cla…
Narvaal Jun 22, 2026
7b403ed
Merge branch 'dev' into production
Narvaal Jun 22, 2026
995cf2e
Merge branch 'dev' into production
Narvaal Jun 22, 2026
6c22cd0
Merge branch 'dev' into production
Narvaal Jun 22, 2026
724a278
Merge branch 'dev' into production
Narvaal Jun 22, 2026
52db462
Merge branch 'dev' into production
Narvaal Jun 22, 2026
ca55433
Merge branch 'dev' into production
Narvaal Jun 22, 2026
780f73c
Merge branch 'dev' into production
Narvaal Jun 22, 2026
33b635c
Merge branch 'dev' into production
Narvaal Jun 22, 2026
1337048
Merge branch 'dev' into production
Narvaal Jun 22, 2026
2daea10
Merge branch 'dev' into production
Narvaal Jun 22, 2026
849a6b2
Merge branch 'dev' into production
Narvaal Jun 22, 2026
d3df77f
Merge branch 'dev' into production
Narvaal Jun 22, 2026
1b112e2
Merge branch 'dev' into production
Narvaal Jun 22, 2026
dbdd55e
Merge branch 'dev' into production
Narvaal Jun 22, 2026
7bc17df
Merge branch 'dev' into production
Narvaal Jun 22, 2026
d5b6a7b
merge: dev into production
Narvaal Jun 23, 2026
1151444
merge: dev into production
Narvaal Jun 23, 2026
69a3b7e
merge: dev into production
Narvaal Jun 23, 2026
ce0f79e
merge: dev into production
Narvaal Jun 23, 2026
863518c
merge: dev into production
Narvaal Jun 23, 2026
7a7ba16
merge: dev into production
Narvaal Jun 23, 2026
a1daecc
merge: dev into production
Narvaal Jun 23, 2026
ef324c4
merge: dev into production
Narvaal Jun 23, 2026
f34d820
merge: dev into production
Narvaal Jun 23, 2026
71a40a1
merge: dev into production
Narvaal Jun 23, 2026
f0fb607
merge: dev into production
Narvaal Jun 23, 2026
f504610
merge: dev into production
Narvaal Jun 23, 2026
bb69694
merge: dev into production
Narvaal Jun 23, 2026
a94c093
merge: dev into production
Narvaal Jun 23, 2026
ef8ee40
merge: dev into production
Narvaal Jun 23, 2026
76a4020
merge: dev into production
Narvaal Jun 23, 2026
5ddc929
merge: dev into production
Narvaal Jun 23, 2026
a9ca524
merge: dev into production
Narvaal Jun 23, 2026
5ee29d2
merge: dev into production
Narvaal Jun 23, 2026
9f2d3ba
merge: dev into production
Narvaal Jun 23, 2026
630f3fb
merge: dev into production
Narvaal Jun 23, 2026
ce6b208
Merge branch 'dev' into production
Narvaal Jun 23, 2026
f227cbc
Merge branch 'dev' into production
Narvaal Jun 23, 2026
af2a2dc
Merge branch 'dev' into production
Narvaal Jun 23, 2026
902503b
Merge branch 'dev' into production
Narvaal Jun 23, 2026
528fa76
Merge branch 'dev' into production
Narvaal Jun 23, 2026
1c45697
Merge branch 'dev' into production
Narvaal Jun 23, 2026
fb2f083
Merge branch 'dev' into production
Narvaal Jun 23, 2026
7d9e580
Merge branch 'dev' into production
Narvaal Jun 23, 2026
6f687ee
Merge branch 'dev' into production
Narvaal Jun 23, 2026
0050f2a
Merge branch 'dev' into production
Narvaal Jun 23, 2026
5432eda
Merge branch 'dev' into production
Narvaal Jun 23, 2026
58693eb
Merge branch 'dev' into production
Narvaal Jun 23, 2026
77e3915
Merge branch 'dev' into production
Narvaal Jun 23, 2026
838b4fd
Merge branch 'dev' into production
Narvaal Jun 23, 2026
8b58301
Merge branch 'dev' into production
Narvaal Jun 23, 2026
f6a9592
Merge branch 'dev' into production
Narvaal Jun 23, 2026
db98e08
Merge branch 'dev' into production
Narvaal Jun 23, 2026
805decb
Merge branch 'dev' into production
Narvaal Jun 23, 2026
d7f2154
Merge branch 'dev' into production
Narvaal Jun 24, 2026
dceadd0
Merge branch 'dev' into production
Narvaal Jun 24, 2026
43dff32
Merge branch 'dev' into production
Narvaal Jun 24, 2026
079f6d1
Merge branch 'dev' into production
Narvaal Jun 24, 2026
e399502
Merge branch 'dev' into production
Narvaal Jun 24, 2026
0b395e5
Merge branch 'dev' into production
Narvaal Jun 24, 2026
90de28e
Merge branch 'dev' into production
Narvaal Jun 24, 2026
106adb6
Merge branch 'dev' into production
Narvaal Jun 24, 2026
f8b8b1c
Merge branch 'dev' into production
Narvaal Jun 24, 2026
3945af8
Merge branch 'dev' into production
Narvaal Jun 24, 2026
2684010
Merge branch 'dev' into production
Narvaal Jun 24, 2026
5a41a73
Merge branch 'dev' into production
Narvaal Jun 24, 2026
6b93bc3
Merge branch 'dev' into production
Narvaal Jun 24, 2026
d83e4b6
Merge branch 'dev' into production
Narvaal Jun 24, 2026
bf776be
Merge branch 'dev' into production
Narvaal Jun 24, 2026
ca650d0
Merge branch 'dev' into production
Narvaal Jun 24, 2026
db6d4e4
Merge branch 'dev' into production
Narvaal Jun 24, 2026
24e57de
Merge branch 'dev' into production
Narvaal Jun 24, 2026
99a9902
Merge branch 'dev' into production
Narvaal Jun 24, 2026
cfef2b5
Merge branch 'dev' into production
Narvaal Jun 24, 2026
2145f6e
Merge branch 'dev' into production
Narvaal Jun 24, 2026
c712298
Merge branch 'dev' into production
Narvaal Jun 24, 2026
6b7b20a
Merge branch 'dev' into production
Narvaal Jun 24, 2026
7789285
Merge branch 'dev' into production
Narvaal Jun 24, 2026
579f52b
Merge branch 'dev' into production
Narvaal Jun 24, 2026
291677a
fix: align PT/EN text and theme icon vertically in navbar
Narvaal Jun 24, 2026
4c53e91
fix: remove icon spin animation from theme toggle button to prevent f…
Narvaal Jun 24, 2026
7be6cb6
chore: upgrade Lambda runtime from nodejs20.x to nodejs22.x
Narvaal Jun 25, 2026
88faca9
Merge branch 'dev' into production
Narvaal Jun 25, 2026
6f20ee4
chore: pin GitHub Actions to Node.js 24-compatible versions, bump bui…
Narvaal Jun 25, 2026
a83afe8
Merge branch 'dev' into production
Narvaal Jun 25, 2026
39ed69f
chore: remove unused CloudFront Function (was UNASSOCIATED, never att…
Narvaal Jun 25, 2026
149ae23
Merge branch 'dev' into production
Narvaal Jun 25, 2026
ef3a04e
docs: update CLAUDE.md — Node 22, pinned Actions versions, remove sta…
Narvaal Jun 25, 2026
2a3065e
Merge branch 'dev' into production
Narvaal Jun 25, 2026
c9db147
chore: bump GitHub Actions to Node.js 24-native versions (checkout v7…
Narvaal Jun 25, 2026
aaad24d
Merge branch 'dev' into production
Narvaal Jun 25, 2026
4d6a440
chore: trigger workflow to validate Node.js 24 Actions upgrade
Narvaal Jun 25, 2026
3cde298
Merge branch 'dev' into production
Narvaal Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/deploy-frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6.4.0
with:
node-version: '20'
node-version: '22'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

Expand All @@ -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@v6.2.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Expand Down
40 changes: 30 additions & 10 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

<!-- last updated: 2026-06-25 -->

## Git Workflow

**Always commit to the `dev` branch.** Never commit directly to `production` or `main` without explicit human approval.
Expand Down Expand Up @@ -42,7 +44,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** |
Expand All @@ -60,12 +62,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`.

Expand All @@ -80,7 +86,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
Expand All @@ -94,6 +100,16 @@ Lambda hotfix without full CI/CD:
terraform apply -target=aws_lambda_function.<name>
```

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)
Expand All @@ -114,7 +130,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 <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

Expand All @@ -126,7 +146,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 `/<id>.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 `/<id>.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`.

Expand Down
16 changes: 3 additions & 13 deletions frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function LangSlot({ lang }: { lang: string }) {
return (
<span
className="relative inline-block overflow-hidden"
style={{ height: '1.1em', width: '2.2ch' }}
style={{ height: '16px', width: '2.2ch' }}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
Expand Down Expand Up @@ -93,19 +93,9 @@ export function Navbar() {
toggle({ x: r.left + r.width / 2, y: r.top + r.height / 2 })
}}
aria-label="Toggle theme"
className="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"
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"
>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={theme}
initial={{ rotate: -45, opacity: 0, scale: 0.7 }}
animate={{ rotate: 0, opacity: 1, scale: 1 }}
exit={{ rotate: 45, opacity: 0, scale: 0.7 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
>
{theme === 'dark' ? <Sun className="size-4" /> : <Moon className="size-4" />}
</motion.div>
</AnimatePresence>
{theme === 'dark' ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>

{/* Mobile menu button */}
Expand Down
15 changes: 0 additions & 15 deletions infra/cloudfront_api.tf
Original file line number Diff line number Diff line change
@@ -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" {
Expand Down
20 changes: 10 additions & 10 deletions infra/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down