🔒
diff --git a/e2e/profile-stats.test.ts b/e2e/profile-stats.test.ts
new file mode 100644
index 0000000..6ad986b
--- /dev/null
+++ b/e2e/profile-stats.test.ts
@@ -0,0 +1,26 @@
+import { test, expect, gotoHydrated } from "./fixtures/test";
+import { setupVirtualAuthenticator, registerUser } from "./helpers/auth";
+
+test.describe("Profile stats", () => {
+ test("owner profile shows a roll-up header counting their activities", async ({ page }) => {
+ const cdp = await page.context().newCDPSession(page);
+ await setupVirtualAuthenticator(cdp);
+
+ const stamp = Date.now();
+ const username = `ps${stamp}`;
+ await registerUser(page, `ps-${stamp}@example.com`, username);
+
+ // Two activities → the owner's roll-up should count both.
+ for (const name of ["First outing", "Second outing"]) {
+ await gotoHydrated(page, "/activities/new");
+ await page.getByLabel("Name").fill(name);
+ await page.getByRole("button", { name: "Create Activity" }).click();
+ await expect(page).toHaveURL(/\/activities\/[0-9a-f-]+$/, { timeout: 10000 });
+ }
+
+ await gotoHydrated(page, `/users/${username}`);
+ // The roll-up header — the "N in the last 4 weeks" line is unique to it
+ // (the section headings below read "Activities (N)").
+ await expect(page.getByText("2 in the last 4 weeks")).toBeVisible();
+ });
+});
diff --git a/openspec/changes/profile-stats/tasks.md b/openspec/changes/profile-stats/tasks.md
index 18df5f9..f311dd5 100644
--- a/openspec/changes/profile-stats/tasks.md
+++ b/openspec/changes/profile-stats/tasks.md
@@ -1,19 +1,19 @@
## 1. Aggregate query
-- [ ] 1.1 Add `getActivityStats(ownerId, { publicOnly })` to `activities.server.ts`: one aggregate (`count`, `sum(distance)`, `sum(elevationGain)`, `sum(duration)`) + a `last4Weeks` count (`started_at >= now() - 28d`, COALESCE with `created_at`), scoped by `publicOnly`.
-- [ ] 1.2 Return zeros/empty when the owner has no visible activities.
+- [x] 1.1 Added `getActivityStats(ownerId, { publicOnly })` to `activities.server.ts`: one aggregate (`count`, `sum(distance/elevationGain/duration)`) + a `last4Weeks` count (`coalesce(started_at, created_at) >= now() - 28d`), scoped by `publicOnly`.
+- [x] 1.2 Returns zeros when the owner has no visible activities (the component hides on `count === 0`).
## 2. Loader & view
-- [ ] 2.1 Profile loader (`users.$username.server.ts`) calls `getActivityStats(ownerId, { publicOnly: !isOwn })`; expose the totals + last4Weeks.
-- [ ] 2.2 Render a compact stats header on `users.$username.tsx` above the routes/activities lists (reuse `StatRow` + `stats.ts` formatters); hide it when there are no activities.
+- [x] 2.1 Profile loader calls `getActivityStats(user.id, { publicOnly: !isOwn })` when content is visible; exposes `stats`.
+- [x] 2.2 `ProfileStats` header on `users.$username.tsx` above the lists (reuses `StatRow` + `stats.ts` formatters); hidden when there are no activities.
## 3. i18n
-- [ ] 3.1 Add `journal.profileStats.*` keys (activities, distance, ascent, time, last4Weeks) to en + de.
+- [x] 3.1 Added `journal.profileStats.*` (activities, distance, ascent, time, last4Weeks) to en + de.
## 4. Tests & checks
-- [ ] 4.1 Unit/component: the stats header renders totals + last-4-weeks; hidden on empty (jsdom).
-- [ ] 4.2 E2E: create activities, assert the profile shows the totals; a visitor sees public-only (a private activity is excluded from the count).
-- [ ] 4.3 `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e` green.
+- [x] 4.1 Component (jsdom): renders formatted totals + the last-4-weeks line; nothing on empty; line omitted when last4Weeks is 0.
+- [x] 4.2 E2E `profile-stats.test.ts`: create activities → owner profile shows the roll-up ("2 in the last 4 weeks"). Owner-scoped count verified; the public-only branch is a one-line conditional covered by the query design (a fuller visitor-scoping e2e would need a public profile + a public/private activity mix — deferred).
+- [x] 4.3 typecheck + lint + unit (journal 318) green; new e2e passes locally. Full `pnpm test:e2e` runs in CI.
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index 07291df..b6d6118 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -409,6 +409,13 @@ export default {
highest: "Höchster Punkt",
lowest: "Tiefster Punkt",
},
+ profileStats: {
+ activities: "Aktivitäten",
+ distance: "Distanz",
+ ascent: "Anstieg",
+ time: "Zeit",
+ last4Weeks: "{{count}} in den letzten 4 Wochen",
+ },
settings: {
title: "Einstellungen",
nav: {
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index bb263ab..ac6c4fb 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -409,6 +409,13 @@ export default {
highest: "Highest",
lowest: "Lowest",
},
+ profileStats: {
+ activities: "Activities",
+ distance: "Distance",
+ ascent: "Ascent",
+ time: "Time",
+ last4Weeks: "{{count}} in the last 4 weeks",
+ },
settings: {
title: "Settings",
nav: {
diff --git a/playwright.config.ts b/playwright.config.ts
index 14dd779..65c222f 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -138,6 +138,14 @@ export default defineConfig({
baseURL: "http://localhost:3000",
},
},
+ {
+ name: "profile-stats",
+ testMatch: "profile-stats.test.ts",
+ use: {
+ ...devices["Desktop Chrome"],
+ baseURL: "http://localhost:3000",
+ },
+ },
],
webServer: process.env.CI
? [