Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: en-US
reviews:
path_filters:
- "!samples/**"
- "!uploads/**"
- "!.env*"
- "!playwright-report/**"
- "!test-results/**"
- "!**/*.zip"
- "!**/*.fit"
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Backend only; never prefix these with NEXT_PUBLIC_.
LOADIQ_API_BASE_URL=http://127.0.0.1:3001
LOADIQ_INTERNAL_API_KEY=
WORKOS_API_KEY=
WORKOS_CLIENT_ID=
WORKOS_COOKIE_PASSWORD=
NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/callback
# Two separate EU Sentry projects: backend and web. Empty disables monitoring.
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
# E2E flags are set only by scripts/e2e-server.mjs. Never configure in deployment.
3 changes: 3 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
set -eu
corepack pnpm --ignore-workspace quality
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Frontend quality
on:
push:
pull_request:
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: corepack enable
- run: corepack pnpm@10.15.1 --ignore-workspace install --frozen-lockfile
- run: corepack pnpm --ignore-workspace quality
- run: corepack pnpm --ignore-workspace build
- run: corepack pnpm --ignore-workspace typecheck
- run: corepack pnpm --ignore-workspace exec playwright install --with-deps chromium
- run: corepack pnpm --ignore-workspace test:e2e
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ next-env.d.ts

# contentlayer
.contentlayer

/playwright-report/
/test-results/
.env
.env.*
!.env.example
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore-workspace=true
58 changes: 58 additions & 0 deletions QUALITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Engineering quality — final review (2026-11-13)

Keep these repositories separate. Use Node22 and the existing pnpm10.15.1 lockfile:
`corepack pnpm@10.15.1 --ignore-workspace install --frozen-lockfile`.
The pnpm lockfile is authoritative; the pre-existing frontend package-lock.json is not used by CI.
No dependencies or infrastructure were added in Task3.1.

## Quality gates
`pnpm quality` runs ESLint and Prettier without fixing files.
Lint debt fingerprints include file, rule, message hash, source hash, severity and occurrence count.
New/changed findings, severity upgrades and increased counts fail. Fatal parser errors always fail.
Obsolete debt was pruned without accepting new findings. Baseline generation is forbidden on CI.
A review must approve any manual debt change; never regenerate a baseline to hide a failed PR.
Formatting exceptions apply only to exact unchanged hashes of old files.
The optional `.githooks/pre-commit` runs checks without modifying files; no hooksPath was set automatically.

## Sentry diagnostics and privacy
Separate API/web projects must belong to an EU Sentry organization. Empty DSNs disable monitoring.
Server: SENTRY_DSN. Web browser: NEXT_PUBLIC_SENTRY_DSN (public DSN, never an API/auth secret).
Only HTTPS EU ingestion DSNs without password/query/fragment are accepted.
Events are reconstructed from an allowlist: generic message, approved error category, environment,
static operation, event ID/time and reviewed code locations with bounded line/column numbers.
`monitoring/source-files.json` lists approved repository paths. Review/update it when adding modules;
unlisted paths are dropped. Backend dist aliases and strictly named Next hashed chunks are supported.
Absolute prefixes, URL hosts/query strings, function names, original error messages, source context,
locals, user identities, cookies, headers, bodies, health/GPS data, extras and attachments are discarded.
Grouping uses Sentry's default sanitized-stack grouping plus operation/category, rather than one global bucket.
Handled ingestion failures and framework/browser error handlers now retain the original technical stack
through the sanitizer. No raw message or stack context is sent. Replay, tracing, logs and metrics are disabled.
Both actual SDK envelopes are tested through an in-memory transport; no network delivery is claimed.
Before enabling DSNs, configure server-side scrubbing, prevention of IP-address storage and IP-derived
geography scrubbing in Sentry. SDK scrubbing cannot control ingestion-side metadata.
Live Sentry delivery remains unverified until EU DSNs are configured. Hosted CI status is recorded in the feature pull request checks.

## External services
GitHub Actions YAML was parsed and structurally validated locally. Feature branches run the same checks on GitHub; inspect the latest PR checks for the hosted result.
Actions are enabled (allowed_actions=all) on both repositories; no enablement change is needed.
No Garmin API credentials, personal data or WorkOS production credentials are required in CI.
There is no artifact upload. Playwright screenshots/video/traces are disabled.
CodeRabbit configuration excludes sensitive paths; installation status is unverified (API403).
Do not grant a new review app access to personal-data history without an owner decision.

## Frontend checks
```
pnpm quality
pnpm build
pnpm typecheck
pnpm exec playwright install chromium
pnpm test:e2e
```
Playwright: 13 passing tests. Coverage includes dashboard/metric units/recommendations, limited historical data,
API errors, unauthenticated redirects, two-user isolation, production auth refusal, and useful private SDK diagnostics.
Build/typecheck and quality gates pass. Production build was verified with empty auth/Sentry credentials.
The development-only test adapter requires NODE_ENV=development, explicit mode+secret, exact fixture backend URL
and loopback host. Production builds/servers reject the flag and production auth still uses WorkOS.
Local Next4318 and fixture backend4319 use only fabricated users/data. Actual WorkOS login/callback is not verified.
Build and E2E run sequentially because both use .next. Historical lint is enforced by a separate mandatory gate,
not by Next build; no new lint fingerprints are accepted. No production login was weakened.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ Data is used exclusively to generate personalized fitness insights for the user.
Users may revoke access at any time by disconnecting their Garmin account.

This application does not resell or redistribute Garmin user data.

## Engineering quality

See [QUALITY.md](QUALITY.md) for isolated database tests, CI, Playwright, lint debt and privacy-safe monitoring.
21 changes: 21 additions & 0 deletions app/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";
import { useEffect } from "react";
import { reportFailure } from "@/lib/monitoring/sentry";
export default function DashboardError({
reset,
error,
}: {
reset: () => void;
error: Error;
}) {
useEffect(() => {
reportFailure(error);
}, [error]);
return (
<main role="alert">
<h1>Dashboard unavailable</h1>
<p>Please try again later.</p>
<button onClick={reset}>Try again</button>
</main>
);
}
4 changes: 2 additions & 2 deletions app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { withAuth } from "@workos-inc/authkit-nextjs";
import { currentUser } from "@/lib/current-user";

export const dynamic = "force-dynamic";

Expand All @@ -8,7 +8,7 @@ export default async function ProductLayout({
}: {
children: ReactNode;
}) {
await withAuth({ ensureSignedIn: true });
await currentUser();

return children;
}
15 changes: 15 additions & 0 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";
import { useEffect } from "react";
import { reportFailure } from "@/lib/monitoring/sentry";
export default function GlobalError({ error }: { error: Error }) {
useEffect(() => {
reportFailure(error);
}, [error]);
return (
<html>
<body>
<h1>LoadIQ is temporarily unavailable</h1>
</body>
</html>
);
}
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./css/style.css";
import { Monitoring } from "@/components/monitoring";

import { Inter } from "next/font/google";
import localFont from "next/font/local";
Expand Down Expand Up @@ -57,6 +58,7 @@ export default function RootLayout({
<body
className={`${inter.variable} ${nacelle.variable} bg-[#171717] font-inter text-base text-[#F9FAFB] antialiased`}
>
<Monitoring />
<div className="flex min-h-screen flex-col overflow-hidden supports-[overflow:clip]:overflow-clip">
{children}
</div>
Expand Down
18 changes: 18 additions & 0 deletions components/monitoring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";
import { useEffect } from "react";
import { initializeMonitoring, reportFailure } from "@/lib/monitoring/sentry";
export function Monitoring() {
useEffect(() => {
initializeMonitoring(process.env.NEXT_PUBLIC_SENTRY_DSN);
const onError = (event: ErrorEvent) => reportFailure(event.error);
const onRejection = (event: PromiseRejectionEvent) =>
reportFailure(event.reason);
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection);
return () => {
window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onRejection);
};
}, []);
return null;
}
Loading
Loading