Skip to content

Commit 3d66d4e

Browse files
committed
Enhance Redis integration and update documentation
- Updated `.env.example` to clarify Redis usage for rate limiting and presence management, ensuring developers understand the fallback mechanisms. - Enhanced `AGENTS.md` with updated instructions for local API URL configurations and Redis integration details. - Added Redis service configuration to `docker-compose.gcp-ops.yml` for local development and updated CI workflows to include Redis health checks. - Improved health check routes in `health.rs` to utilize Redis for connection management, ensuring accurate service status reporting. - Updated metrics handling in `metrics.rs` to track rate limit rejections, enhancing observability. These changes improve the application's Redis integration, enhance documentation clarity, and provide better monitoring capabilities.
1 parent 0dbfdf0 commit 3d66d4e

20 files changed

Lines changed: 901 additions & 222 deletions

File tree

.cursor/hooks/state/continual-learning-index.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"mtimeMs": 1780043491255.2393
88
},
99
"/home/yourblooo/.cursor/projects/home-yourblooo-development-portfolio-backend/agent-transcripts/b9c426f8-a2a1-4df3-bc36-8e849d755662/b9c426f8-a2a1-4df3-bc36-8e849d755662.jsonl": {
10-
"mtimeMs": 1780236743482.536
10+
"mtimeMs": 1780246125446.7759
1111
}
1212
}
1313
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"version": 1,
3-
"lastRunAtMs": 1780236735739,
4-
"turnsSinceLastRun": 11,
5-
"lastTranscriptMtimeMs": 1780236735655.7478,
6-
"lastProcessedGenerationId": "0369d852-36d7-4633-a5c0-10ff6be84fa7",
3+
"lastRunAtMs": 1780246116164,
4+
"turnsSinceLastRun": 5,
5+
"lastTranscriptMtimeMs": 1780246115874.715,
6+
"lastProcessedGenerationId": "fc8e9382-f917-4ab6-8ee6-433e6d5407bf",
77
"trialStartedAtMs": null
88
}

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ DB_POOL_MIN=2
2121
DB_POOL_MAX=10
2222
DB_CONNECT_RETRY_SECS=60
2323

24-
# Optional Redis URL (health probe only; app code does not connect yet — presence/gate not persisted)
24+
# Optional Redis URL (rate limiting + presence; falls back to in-memory when unset)
2525
REDIS_URL=redis://localhost:6379
2626

2727
# ----------------------------------------------------------------------------
@@ -119,7 +119,7 @@ GEMINI_API_KEY=
119119

120120
# ----------------------------------------------------------------------------
121121
# Phase 4B — Visitor presence (WebSocket)
122-
# GET /ws/presence — in-memory room counts; REDIS_URL reserved for scaling.
122+
# GET /ws/presence — Redis-backed room counts when REDIS_URL is set; in-memory fallback otherwise.
123123
# ----------------------------------------------------------------------------
124124

125125
# ----------------------------------------------------------------------------

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,18 @@ jobs:
6969
--health-interval 5s
7070
--health-timeout 5s
7171
--health-retries 12
72+
redis:
73+
image: redis:7-alpine
74+
ports:
75+
- 6379:6379
76+
options: >-
77+
--health-cmd "redis-cli ping"
78+
--health-interval 5s
79+
--health-timeout 5s
80+
--health-retries 12
7281
env:
7382
TEST_DATABASE_URL: postgres://portfolio:portfolio@localhost:5432/portfolio_test
83+
TEST_REDIS_URL: redis://localhost:6379
7484
steps:
7585
- name: Checkout code
7686
uses: actions/checkout@v4

AGENTS.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- Do not discard the terminal experience; prefer dual entry (standard landing + gated terminal) over a full UI rewrite.
77
- Create git commits and pull requests only when explicitly asked.
88
- Gate stays enabled in development; bypass only via `GATE_BYPASS_SECRET`, not an automatic dev-off flag.
9-
- User fills API keys and other random secrets in `.env.development` locally (copy from `.env.example`); production via `.env` on GCP or Vercel platform env.
9+
- User fills API keys and other random secrets in `.env.development` locally (copy from `.env.example`); production via `.env` on GCP or Vercel platform env. Local `.env.development` API URLs should target `localhost:8080`, not production Cloud Run.
1010
- Gate puzzles should be NATAS-style web challenges (login, hidden paths, Referer), not SSH/Bandit/Behemoth simulations.
1111
- First time deploying to GCP; prefers clear, beginner-friendly infrastructure documentation and bootstrap guides.
1212
- Portfolio-frontend chats must proactively use Cursor SDK + Team Kit skills/MCP (`.cursor/rules/cursor-sdk-team-kit.mdc`, alwaysApply).
@@ -18,12 +18,12 @@
1818
- Multi-repo portfolio: `portfolio-backend` (Rust/Axum, SQLx/PostgreSQL) and `portfolio-frontend` (Next.js 16, React 19, terminal-interactive portfolio).
1919
- Planned public routing: standard landing at `/`, terminal at `/terminal`, three-level gate at `/gate` (and `/gate/[level]`).
2020
- Blog, social share, RSS, and other content routes stay shared/public; do not duplicate them across standard and terminal UIs.
21-
- Gate puzzles (backend-validated, no answers in frontend bundle): L1 static login `yourblooo0`/`yourblooo0`; L2 requires L1 completion before `/s3cr3t/users.txt` reveals login `yourblooo1` + env password; L3 backend validates `Referer: {SITE_URL}/terminal` in `complete_level_3`; gate JWT pinned to HS256 with `iss`/`aud`. Terminal route `noindex`; terminal SSR verifies JWT via `/api/gate/status`. Gate/terminal is UX puzzle layer, not API auth perimeter (admin JWT separate). `GATE_BYPASS_SECRET` via Next.js `proxy.ts` (`X-Gate-Bypass`), not Rust handlers. Legacy env `GATE_L2_STUB_MD5`, `GATE_L3_SHELLCODE_HASH`, `GATE_L3_OFFSET` unused (archived shellcode design); only `GATE_L1_ANSWER`, `GATE_L2_ANSWER`, `GATE_TOKEN_SECRET` required.
22-
- Backend API default port 8080; frontend align `NEXT_PUBLIC_API_URL` / `BACKEND_URL` with 8080 locally. Production (Vercel): both must be the HTTPS Cloud Run service base URL (no trailing slash); `NEXT_PUBLIC_*` changes require a Vercel redeploy; a wrong `*.run.app` URL yields a Google-edge 404 with zero Cloud Run requests. Most prod API traffic is Vercel SSR server-side, with silent empty fallbacks if the backend is unreachable. Local dev: backend `load_env_file()` reads `.env.development` when `ENVIRONMENT != production`; frontend `bun run dev` uses `--env-file=.env.development`; Docker Compose via `scripts/compose-dev.sh` or `--env-file .env.development`. CORS auto-merges localhost origins (ports 3000–3002) when `ENVIRONMENT != production`; gate vars documented in `.env.example`.
21+
- Gate puzzles (backend-validated, no answers in frontend bundle): L1 static login `yourblooo0`/`yourblooo0`; L2 requires L1 completion before `/s3cr3t/users.txt` reveals login `yourblooo1` + env password; L3 backend validates `Referer: {SITE_URL}/terminal` in `complete_level_3`; gate JWT pinned to HS256 with `iss`/`aud`. Terminal route `noindex`; terminal SSR verifies JWT via `/api/gate/status`. Gate/terminal is UX puzzle layer, not API auth perimeter (admin JWT separate). `GATE_BYPASS_SECRET` via Next.js `proxy.ts` (`X-Gate-Bypass`), not Rust handlers. Legacy env `GATE_L2_STUB_MD5`, `GATE_L3_SHELLCODE_HASH`, `GATE_L3_OFFSET` unused (archived shellcode design); only `GATE_L1_ANSWER`, `GATE_L2_ANSWER`, `GATE_TOKEN_SECRET` required. Rebuild/restart local Docker backend after gate answer or gate code changes — stale image keeps old L1 credentials.
22+
- Backend API default port 8080; frontend align `NEXT_PUBLIC_API_URL` / `BACKEND_URL` with 8080 locally. Production (Vercel): both must be the HTTPS Cloud Run service base URL (no trailing slash); `NEXT_PUBLIC_*` changes require a Vercel redeploy; a wrong `*.run.app` URL yields a Google-edge 404 with zero Cloud Run requests. Most prod API traffic is Vercel SSR server-side, with silent empty fallbacks if the backend is unreachable. `ROADMAP_EMAIL`/`ROADMAP_PASSWORD` are Cloud Run/GCP secrets only — not Vercel env. Local dev: backend `load_env_file()` reads `.env.development` when `ENVIRONMENT != production`; frontend `bun run dev` uses `--env-file=.env.development`; Docker Compose via `scripts/compose-dev.sh` or `--env-file .env.development`. CORS auto-merges localhost origins (ports 3000–3002) when `ENVIRONMENT != production`; gate vars documented in `.env.example`.
2323
- Feature status SSOT: `portfolio-frontend/FEATURE_PLANNING.md` (+ `docs/dual-ui-gate.md` for gate ops); `ROADMAP.md` removed May 2026 after performance backlog moved to Feature #33.
2424
- PWA is site-wide (`public/manifest.json`, `public/sw.js`, scope `/`, offline page); install prompt only after terminal onboarding tour completes.
25-
- Observability stack (Loki, Grafana, Prometheus) runs on a GCE ops VM in production via `docker-compose.gcp-ops.yml`; same stack in local `docker-compose.yml` for dev. Redis health uses real PING via `REDIS_URL`; not yet used for app cache or gate sessions. Optional `METRICS_BEARER_TOKEN` protects `/metrics`; Prometheus prod scrape uses bearer auth.
26-
- GCP production: backend on Cloud Run (asia-southeast2/Jakarta) with `max_instances=1` for in-memory gate sessions (Redis not used for gate yet), public `allUsers` `run.invoker` (Vercel frontend + public API; admin auth app-level), frontend on Vercel; `DATABASE_URL` must be the ops VM internal IP over Serverless VPC Access (not a subnet-CIDR host, not `localhost`); ops VM may use default VPC addressing or Terraform ops ranges—firewall must allow TCP 5432 from the VPC connector CIDR to the VM; unreachable Postgres at startup panics before the revision listens on 8080. Serverless VPC Access connector (`10.10.1.0/28`, not Direct VPC egress) with private-ranges-only egress to ops subnet `10.10.0.0/24` unless Cloud NAT for all-traffic; container command/args empty (image CMD `/app/portfolio-backend`, port 8080); Terraform (network, iam, artifact_registry, secrets, compute_ops, cloud_run); ops VM (e2-medium, no public IP, IAP SSH) hosts Postgres 16 + observability at `/mnt/data`; `REDIS_URL` optional on Cloud Run (health probe only, not in prod compose stack). CI via `deploy-gcp.yml` Workload Identity Federation.
25+
- Observability stack (Loki, Grafana, Prometheus) runs on a GCE ops VM in production via `docker-compose.gcp-ops.yml`; same stack in local `docker-compose.yml` for dev. Redis on ops VM (`6379`) backs distributed rate limiting and WebSocket presence when `REDIS_URL` is set; falls back to in-memory `tower_governor` + local presence counts when unset or unreachable. Optional `METRICS_BEARER_TOKEN` protects `/metrics`; Prometheus prod scrape uses bearer auth.
26+
- GCP production: backend on Cloud Run (asia-southeast2/Jakarta) with `max_instances=1` for in-memory gate sessions (Redis not used for gate yet), public `allUsers` `run.invoker` (Vercel frontend + public API; admin auth app-level), frontend on Vercel; `DATABASE_URL` must be the ops VM internal IP over Serverless VPC Access (not a subnet-CIDR host, not `localhost`); `REDIS_URL=redis://<ops_vm_internal_ip>:6379` from Terraform; ops VM may use default VPC addressing or Terraform ops ranges—firewall must allow TCP 5432 and 6379 from the VPC connector CIDR to the VM; unreachable Postgres at startup panics before the revision listens on 8080. Serverless VPC Access connector (`10.10.1.0/28`, not Direct VPC egress); Terraform egress `PRIVATE_RANGES_ONLY` — `ALL_TRAFFIC` through VPC without Cloud NAT breaks outbound APIs (roadmap.sh, GitHub, Resend, Gemini); container command/args empty (image CMD `/app/portfolio-backend`, port 8080); Terraform (network, iam, artifact_registry, secrets, compute_ops, cloud_run); ops VM (e2-medium, no public IP, IAP SSH) hosts Postgres 16 + observability at `/mnt/data`; `REDIS_URL` optional on Cloud Run (health probe only, not in prod compose stack). CI via `deploy-gcp.yml` Workload Identity Federation.
2727
- Next.js 16 edge handler is `src/proxy.ts` (`export function proxy()`), not legacy `middleware.ts`.
28-
- Frontend performance: `cacheComponents: true` (PPR) with Feature #33 docs at `docs/features/FEATURE_33_PERFORMANCE.md`; dual RUM (pino/Loki + Vercel Speed Insights). Pre-completion verification: backend `cargo fmt/check/clippy/test`; frontend `bun run lint` + `type-check`. Known Vitest hang: `background-manager.test.tsx` (~16 min).
28+
- Frontend performance: `cacheComponents: true` (PPR) with Feature #33 docs at `docs/features/FEATURE_33_PERFORMANCE.md`; `/roadmap` uses request-time fetch via `await headers()` under PPR; dual RUM (pino/Loki + Vercel Speed Insights). Production CSP must not use `strict-dynamic`/nonce without wiring nonce into Next.js scripts (`'unsafe-inline'` used); Vercel builds disable file logging (`LOG_TO_FILE`, `NEXT_PHASE=phase-production-build` — no `logs/server` mkdir); client logs POST plain JSON to `/api/logs` (in-memory crypto session fails across Vercel serverless instances). Pre-completion verification: backend `cargo fmt/check/clippy/test`; frontend `bun run lint` + `type-check`. Known Vitest hang: `background-manager.test.tsx` (~16 min).
2929
- Backend performance & integrations: all API routes P95 <50ms SLO (`docs/performance/API_SLA.md`, `config/slo-rules.yml`, Grafana 50ms alerts, `scripts/latency-smoke.sh` in CI); roadmap/github in-memory cache with stale-while-revalidate; roadmap auth via `ROADMAP_EMAIL`/`ROADMAP_PASSWORD``POST …/v1-login` (Terraform secrets `roadmap-email`/`roadmap-password`; `ROADMAP_AUTH_TOKEN` removed); Swagger UI disabled in production.

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ totp-rs = { version = "5", features = ["gen_secret", "otpauth"] }
9595
metrics = "0.24"
9696
metrics-exporter-prometheus = "0.16"
9797

98-
# Redis (health probe; presence scaling reserved)
99-
redis = { version = "0.27", features = ["tokio-comp"] }
98+
# Redis (health, rate limiting, presence)
99+
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
100100

101101
# OpenAPI / Swagger documentation. `axum_extras` lets utoipa parse path /
102102
# query params automatically; `uuid` and `chrono` map our DB types to

docker-compose.gcp-ops.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ services:
2626
retries: 5
2727
start_period: 30s
2828

29+
redis:
30+
image: redis:7-alpine
31+
container_name: portfolio-redis
32+
restart: unless-stopped
33+
command: >
34+
redis-server
35+
--appendonly yes
36+
--appendfsync everysec
37+
ports:
38+
- "6379:6379"
39+
volumes:
40+
- /mnt/data/redis:/data
41+
networks:
42+
- ops
43+
healthcheck:
44+
test: ["CMD", "redis-cli", "ping"]
45+
interval: 10s
46+
timeout: 5s
47+
retries: 5
48+
start_period: 10s
49+
2950
loki:
3051
image: grafana/loki:3.3.2
3152
container_name: portfolio-loki

docs/performance/API_SLA.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Target: **P95 < 50ms** for all HTTP routes.
6363
Routes in this tier cannot meet 50ms synchronously on cache miss. Strategy: Redis
6464
cache with background refresh; HTTP returns cached data or 202 Accepted.
6565

66+
**Rate limiting:** When `REDIS_URL` is configured, auth/gate/contact/logs/analytics/github/newsletter/ai routes use Redis fixed-window counters (`ratelimit:{bucket}:{ip}`) with fail-open on Redis errors. When unset, `tower_governor` in-memory limits apply unchanged.
67+
6668
| Method | Path | Handler | Module | Bottleneck | Strategy |
6769
|--------|------|---------|--------|------------|----------|
6870
| GET | `/api/roadmap/streak` | `get_streak` | `roadmap.rs` | Upstream HTTP ~100-600ms | Redis cache + background refresh |
@@ -84,7 +86,7 @@ cache with background refresh; HTTP returns cached data or 202 Accepted.
8486
| POST | `/api/newsletter/unsubscribe` | `unsubscribe` | `newsletter.rs` | DB UPDATE | OK |
8587
| POST | `/api/admin/newsletter/broadcast` | `broadcast` | `newsletter.rs` | N x email | Return 202 + async |
8688
| POST | `/api/auth/2fa/*` | various | `twofa.rs` | DB + crypto | OK |
87-
| GET | `/ws/presence` | `ws_handler` | `presence.rs` | WebSocket (long-lived) | N/A |
89+
| GET | `/ws/presence` | `ws_handler` | `presence.rs` | WebSocket (long-lived) | Redis room/total counters when `REDIS_URL` set |
8890

8991
### Exceptions (cannot meet 50ms by nature)
9092

0 commit comments

Comments
 (0)