Skip to content

Commit 27e045d

Browse files
authored
Merge branch 'main' into codex/implement-web-vitals-tracking-and-api-jt9zi9
2 parents c08538a + dbd8d3b commit 27e045d

File tree

88 files changed

+7222
-692
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+7222
-692
lines changed

.github/workflows/ci.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,76 @@ jobs:
102102

103103
- name: Build
104104
run: npm run build
105+
106+
performance-budget:
107+
runs-on: ubuntu-latest
108+
needs:
109+
- frontend-tests
110+
env:
111+
PERF_BUDGET_HEADLESS: 'true'
112+
steps:
113+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
114+
115+
- name: Set up Node.js
116+
uses: actions/setup-node@0a44ba78451273a1ed8ac2fee4e347c72dfd377f
117+
with:
118+
node-version: '20'
119+
cache: 'npm'
120+
cache-dependency-path: ./frontend/package-lock.json
121+
122+
- name: Install dependencies
123+
working-directory: ./frontend
124+
run: npm ci
125+
126+
- name: Start application stack
127+
run: |
128+
docker compose -f docker-compose.dev.yml up -d --build
129+
130+
- name: Wait for API
131+
run: |
132+
for i in {1..60}; do curl -sf http://localhost:8000/healthcheck && break || sleep 2; done
133+
134+
- name: Wait for Frontend
135+
run: |
136+
for i in {1..60}; do curl -sf http://localhost:3000/models/manifest.json && break || sleep 2; done
137+
138+
- name: Run performance budget checks
139+
working-directory: ./frontend
140+
env:
141+
PERF_BUDGET_OUTPUT_DIR: ../test-results/perf
142+
run: npm run perf:budget
143+
144+
- name: Upload performance budget report
145+
if: always()
146+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
147+
with:
148+
name: perf-budget
149+
path: test-results/perf
150+
151+
- name: Shutdown stack
152+
if: always()
153+
run: |
154+
docker compose -f docker-compose.dev.yml down
155+
156+
observability-budgets:
157+
runs-on: ubuntu-latest
158+
needs:
159+
- performance-budget
160+
steps:
161+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
162+
163+
- name: Set up Python
164+
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c
165+
with:
166+
python-version: '3.12'
167+
168+
- name: Install dependencies
169+
run: pip install pyyaml
170+
171+
- name: Check observability budgets
172+
env:
173+
PROMETHEUS_URL: ${{ secrets.PROMETHEUS_URL }}
174+
PROMETHEUS_BEARER_TOKEN: ${{ secrets.PROMETHEUS_BEARER_TOKEN }}
175+
TEMPO_URL: ${{ secrets.TEMPO_URL }}
176+
TEMPO_BEARER_TOKEN: ${{ secrets.TEMPO_BEARER_TOKEN }}
177+
run: python tools/ci/check_observability_budgets.py --config observability-budgets.yml

.github/workflows/perf.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: perf
2+
3+
permissions:
4+
contents: read
5+
6+
concurrency:
7+
group: ${{ github.workflow }}-${{ github.ref }}
8+
cancel-in-progress: true
9+
10+
on:
11+
workflow_dispatch:
12+
13+
jobs:
14+
playwright-perf:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 60
17+
defaults:
18+
run:
19+
working-directory: ./frontend
20+
steps:
21+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
22+
23+
- name: Set up Node.js
24+
uses: actions/setup-node@0a44ba78451273a1ed8ac2fee4e347c72dfd377f
25+
with:
26+
node-version: '20'
27+
cache: 'npm'
28+
cache-dependency-path: ./frontend/package-lock.json
29+
30+
- name: Install dependencies
31+
run: npm ci
32+
33+
- name: Install Playwright browsers
34+
run: npx playwright install --with-deps
35+
36+
- name: Build application
37+
run: npm run build
38+
39+
- name: Start application
40+
run: |
41+
npm run start -- --hostname 0.0.0.0 --port 3000 &
42+
echo $! > ../.next-server-pid
43+
for i in {1..60}; do
44+
if curl -sSf http://127.0.0.1:3000 > /dev/null; then
45+
break
46+
fi
47+
sleep 2
48+
done
49+
if ! curl -sSf http://127.0.0.1:3000 > /dev/null; then
50+
echo "Application failed to start" >&2
51+
exit 1
52+
fi
53+
54+
- name: Run Playwright perf tests
55+
env:
56+
BASE_URL: http://127.0.0.1:3000
57+
run: npx playwright test --reporter=line,html
58+
59+
- name: Stop application
60+
if: always()
61+
run: |
62+
if [ -f ../.next-server-pid ]; then
63+
kill $(cat ../.next-server-pid) || true
64+
rm -f ../.next-server-pid
65+
fi
66+
67+
- name: Upload Playwright artifacts
68+
if: always()
69+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
70+
with:
71+
name: playwright-perf-artifacts
72+
path: |
73+
frontend/playwright-report
74+
frontend/test-results
75+
if-no-files-found: warn

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ yarn-error.log*
3636
.next/
3737
out/
3838
.cache/
39+
artifacts/
3940

4041
# Env files (keep example)
4142
.env

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ The frontend uses Next.js App Router architecture:
102102
docker compose --env-file .env.development -f docker-compose.dev.yml up
103103
```
104104

105+
To launch the observability stack alongside your application, start a second
106+
Compose project in a separate terminal:
107+
108+
```bash
109+
docker compose -f docker-compose.observability.yml up
110+
```
111+
112+
This brings up Prometheus, Grafana, Loki, Tempo, and the OpenTelemetry
113+
Collector with their configuration mounted from the `ops/` directory.
114+
105115
### Production
106116

107117
```bash

artifacts/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

backend/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ docker compose --env-file .env.development -f docker-compose.dev.yml up backend-
2525

2626
The backend expects the following environment variables (or entries in `.env.development`):
2727

28+
- `API_WRITE_TOKEN` – Bearer token required for privileged sync and admin endpoints.
2829
- `HYGRAPH_WEBHOOK_SECRET` – Shared secret used to validate incoming Hygraph webhook signatures.
30+
- `API_WRITE_TOKEN` – Bearer token required for privileged sync endpoints such as `/api/sync/hygraph/pull`.
2931

3032
## Project Structure
3133

backend/api/config.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Configuration settings for the FastAPI application."""
22

3-
from functools import lru_cache
4-
from typing import Any, Dict
3+
import os
4+
from typing import Any, Dict, Optional
55

66
from pydantic import Field, field_validator
77
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -16,6 +16,11 @@ class Settings(BaseSettings):
1616
default=3000,
1717
description="Port for the frontend service",
1818
)
19+
api_write_token: str | None = Field(
20+
default=None,
21+
description="Bearer token required for write-protected API routes.",
22+
repr=False,
23+
)
1924

2025
# Database and integration settings
2126
database_url: str = Field(
@@ -34,6 +39,11 @@ class Settings(BaseSettings):
3439
default="",
3540
description="Hygraph access token",
3641
)
42+
api_write_token: str = Field(
43+
default="",
44+
description="Bearer token required for privileged write routes",
45+
repr=False,
46+
)
3747
hygraph_webhook_secret: str = Field(
3848
description=(
3949
"Shared secret used to verify Hygraph webhook signatures."
@@ -42,6 +52,14 @@ class Settings(BaseSettings):
4252
min_length=1,
4353
repr=False,
4454
)
55+
api_write_token: str | None = Field(
56+
default=None,
57+
description=(
58+
"Bearer token required for privileged API write operations."
59+
" Provide via the API_WRITE_TOKEN environment variable."
60+
),
61+
repr=False,
62+
)
4563

4664
model_config = SettingsConfigDict(
4765
env_file=".env.development",
@@ -61,12 +79,31 @@ def _validate_hygraph_webhook_secret(cls, value: str) -> str:
6179
)
6280
return value.strip()
6381

82+
@field_validator("api_write_token")
83+
@classmethod
84+
def _normalize_api_write_token(cls, value: str | None) -> str | None:
85+
"""Normalize optional API write tokens."""
86+
87+
if value is None:
88+
return None
89+
value = value.strip()
90+
return value or None
91+
92+
93+
_SETTINGS_CACHE: Optional[Settings] = None
94+
_LAST_SECRET: Optional[str] = None
95+
6496

65-
@lru_cache()
6697
def get_settings() -> Settings:
67-
"""Return a cached instance of :class:`Settings`."""
98+
"""Return a cached instance of :class:`Settings`, refreshing on secret changes."""
6899

69-
return Settings()
100+
global _SETTINGS_CACHE, _LAST_SECRET
101+
current_secret = os.getenv("HYGRAPH_WEBHOOK_SECRET")
102+
if _SETTINGS_CACHE is None or _LAST_SECRET != current_secret:
103+
settings = Settings()
104+
_SETTINGS_CACHE = settings
105+
_LAST_SECRET = settings.hygraph_webhook_secret
106+
return _SETTINGS_CACHE
70107

71108

72109
def get_fastapi_settings() -> Dict[str, Any]:

backend/api/db.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def _normalize_postgres_url(url: str) -> str:
2929

3030
def get_engine_url() -> str:
3131
"""Resolve the database URL from env or settings with sane defaults."""
32+
# Use the cached settings instance instead of instantiating ``Settings``
33+
# directly so configuration is read a single time across the app.
3234
settings = get_settings()
35+
# ``get_settings`` returns the cached Settings instance from api.config.
3336
url = os.getenv("DATABASE_URL", settings.database_url)
3437
if url.startswith("sqlite"):
3538
return url
@@ -56,4 +59,4 @@ def get_db() -> Generator:
5659
try:
5760
yield db
5861
finally:
59-
db.close()
62+
db.close()

0 commit comments

Comments
 (0)