From ba6f09171d949f0e0723ded44a102c46bf58f076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 24 Apr 2026 21:57:21 +0200 Subject: [PATCH] Archive public-content-visibility Final tasks ticked post-merge: - 10.2: verified on prod that journal.routes + journal.activities still default to 'private' NOT NULL, with the only public rows being the 15-each demo-bot seeded content - 10.3: demo-activity-bot already inserts with visibility='public' directly in demo-bot.server.ts Syncs the three delta specs into main: + activity-feed: 2 added, 1 modified + public-profiles: new spec (1 added) + route-management: 2 added, 1 modified Moves change to openspec/changes/archive/2026-04-24-public-content-visibility. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/activity-feed/spec.md | 0 .../specs/public-profiles/spec.md | 0 .../specs/route-management/spec.md | 0 .../tasks.md | 6 ++- openspec/specs/activity-feed/spec.md | 47 +++++++++++++++-- openspec/specs/public-profiles/spec.md | 26 ++++++++++ openspec/specs/route-management/spec.md | 51 +++++++++++++++++-- 10 files changed, 118 insertions(+), 12 deletions(-) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/.openspec.yaml (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/design.md (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/proposal.md (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/specs/activity-feed/spec.md (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/specs/public-profiles/spec.md (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/specs/route-management/spec.md (100%) rename openspec/changes/{public-content-visibility => archive/2026-04-24-public-content-visibility}/tasks.md (86%) create mode 100644 openspec/specs/public-profiles/spec.md diff --git a/openspec/changes/public-content-visibility/.openspec.yaml b/openspec/changes/archive/2026-04-24-public-content-visibility/.openspec.yaml similarity index 100% rename from openspec/changes/public-content-visibility/.openspec.yaml rename to openspec/changes/archive/2026-04-24-public-content-visibility/.openspec.yaml diff --git a/openspec/changes/public-content-visibility/design.md b/openspec/changes/archive/2026-04-24-public-content-visibility/design.md similarity index 100% rename from openspec/changes/public-content-visibility/design.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/design.md diff --git a/openspec/changes/public-content-visibility/proposal.md b/openspec/changes/archive/2026-04-24-public-content-visibility/proposal.md similarity index 100% rename from openspec/changes/public-content-visibility/proposal.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/proposal.md diff --git a/openspec/changes/public-content-visibility/specs/activity-feed/spec.md b/openspec/changes/archive/2026-04-24-public-content-visibility/specs/activity-feed/spec.md similarity index 100% rename from openspec/changes/public-content-visibility/specs/activity-feed/spec.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/specs/activity-feed/spec.md diff --git a/openspec/changes/public-content-visibility/specs/public-profiles/spec.md b/openspec/changes/archive/2026-04-24-public-content-visibility/specs/public-profiles/spec.md similarity index 100% rename from openspec/changes/public-content-visibility/specs/public-profiles/spec.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/specs/public-profiles/spec.md diff --git a/openspec/changes/public-content-visibility/specs/route-management/spec.md b/openspec/changes/archive/2026-04-24-public-content-visibility/specs/route-management/spec.md similarity index 100% rename from openspec/changes/public-content-visibility/specs/route-management/spec.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/specs/route-management/spec.md diff --git a/openspec/changes/public-content-visibility/tasks.md b/openspec/changes/archive/2026-04-24-public-content-visibility/tasks.md similarity index 86% rename from openspec/changes/public-content-visibility/tasks.md rename to openspec/changes/archive/2026-04-24-public-content-visibility/tasks.md index 3bdf865..cda68b3 100644 --- a/openspec/changes/public-content-visibility/tasks.md +++ b/openspec/changes/archive/2026-04-24-public-content-visibility/tasks.md @@ -58,5 +58,7 @@ ## 10. Rollout - [x] 10.1 Schema ships with `drizzle-kit push --force` — column default `'private'`, no row changes on prod -- [ ] 10.2 (Post-deploy check) Confirm on prod that existing users' routes/activities remain auth-only — handled at merge time, not in this PR -- [ ] 10.3 (Next change) `demo-activity-bot` will insert with `visibility='public'` directly — not in this PR +- [x] 10.2 (Post-deploy check) Confirm on prod that existing users' routes/activities remain auth-only — handled at merge time, not in this PR + - Verified post-merge on trails.cool: `journal.routes` and `journal.activities` both have `column_default = 'private'::text`, NOT NULL. Current row counts — routes: 6 private / 15 public; activities: 11 private / 15 public — the publics on both sides are the demo-bot (bruno) seeded content per task 10.3. No existing user content leaked public. +- [x] 10.3 (Next change) `demo-activity-bot` will insert with `visibility='public'` directly — not in this PR + - Confirmed landed in the `demo-activity-bot` change: `apps/journal/app/lib/demo-bot.server.ts:564,585` set `visibility: "public"` on both route and activity inserts. diff --git a/openspec/specs/activity-feed/spec.md b/openspec/specs/activity-feed/spec.md index d7e01d5..c002b3e 100644 --- a/openspec/specs/activity-feed/spec.md +++ b/openspec/specs/activity-feed/spec.md @@ -1,9 +1,7 @@ ## Purpose Activity creation, chronological feed, detail views, and route linking in the Journal app. - ## Requirements - ### Requirement: Create activity The Journal SHALL allow authenticated users to create an activity by uploading a GPX trace and adding a description. @@ -23,12 +21,28 @@ The Journal SHALL display a chronological feed of the authenticated user's activ - **THEN** they see their activities in reverse chronological order with name, date, distance, and duration ### Requirement: Activity detail page -The Journal SHALL display an activity detail page with map, stats, and description. +The Journal SHALL display an activity detail page with map, stats, and description. Access depends on the activity's `visibility`: `public` activities are viewable by anyone including unauthenticated visitors, `unlisted` activities are viewable by anyone who has the URL, and `private` activities are viewable only by the owner. -#### Scenario: View activity detail -- **WHEN** a user navigates to an activity URL +#### Scenario: Owner views own activity +- **WHEN** a logged-in user navigates to an activity they own at any visibility - **THEN** they see the activity name, description, a map with the GPS trace, distance, duration, and elevation stats +#### Scenario: Anyone views a public activity +- **WHEN** any visitor (including unauthenticated) navigates to a `public` activity's URL +- **THEN** they see the full activity detail page as above + +#### Scenario: Anyone with the URL views an unlisted activity +- **WHEN** any visitor navigates directly to an `unlisted` activity's URL +- **THEN** they see the full activity detail page as above + +#### Scenario: Non-owner is blocked from a private activity +- **WHEN** a visitor who is not the owner requests a `private` activity URL +- **THEN** the server responds with HTTP 404 (not 403), so the existence of the private activity is not leaked + +#### Scenario: Public and unlisted activity pages emit social-share metadata +- **WHEN** a visitor loads a `public` or `unlisted` activity detail page +- **THEN** the response emits Open Graph and Twitter Card meta tags (`og:title`, `og:description`, `og:type="article"`, `og:site_name`, `twitter:card="summary"`) + ### Requirement: Link activity to route Users SHALL be able to link an existing activity to a route, or create a route from an activity trace. @@ -57,3 +71,26 @@ The Journal SHALL expose a reverse-chronological feed of every activity on the i #### Scenario: Private or unlisted activities stay out of the feed - **WHEN** an activity's visibility is `private` or `unlisted` - **THEN** it does not appear in the instance feed, regardless of who is viewing + +### Requirement: Activity visibility +The Journal SHALL persist a `visibility` value on every activity and SHALL allow the owner to change it. + +#### Scenario: New activities default to private +- **WHEN** an activity is created without an explicit visibility +- **THEN** the activity row is persisted with `visibility = 'private'` + +#### Scenario: Owner changes an activity's visibility +- **WHEN** an activity owner selects a different visibility (`private`, `unlisted`, `public`) and saves +- **THEN** the stored visibility is updated and subsequent access checks use the new value immediately + +### Requirement: Activity listings respect visibility +The Journal's own-activities feed SHALL show the owner everything regardless of visibility, while any cross-user listing SHALL only include activities with `visibility = 'public'`. + +#### Scenario: Own activity feed is unchanged +- **WHEN** a logged-in user views their own activity feed +- **THEN** the feed includes all of their own activities regardless of visibility + +#### Scenario: Public profile lists only public activities +- **WHEN** a visitor loads `/users/:username` +- **THEN** the rendered list of activities includes only the user's `public` activities; `unlisted` and `private` activities are omitted + diff --git a/openspec/specs/public-profiles/spec.md b/openspec/specs/public-profiles/spec.md new file mode 100644 index 0000000..d9321e9 --- /dev/null +++ b/openspec/specs/public-profiles/spec.md @@ -0,0 +1,26 @@ +# public-profiles Specification + +## Purpose +TBD - created by archiving change public-content-visibility. Update Purpose after archive. +## Requirements +### Requirement: Public profile page +The Journal SHALL serve a public profile page at `/users/:username` that lists the user's public routes and activities in reverse chronological order, viewable without authentication. + +#### Scenario: Logged-out visitor views a profile with public content +- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user with at least one `public` route or activity +- **THEN** the page renders that user's display name (falling back to username), the `@username@domain` handle, and a reverse-chronological list of their `public` routes and `public` activities +- **AND** items marked `unlisted` or `private` do NOT appear in the list + +#### Scenario: Profile 404 when there is no public content +- **WHEN** a visitor navigates to `/users/:username` for a user whose content is all `private` or `unlisted`, or for a username that does not exist +- **THEN** the server responds with HTTP 404 +- **AND** the response does NOT distinguish the two cases, so existence of a private-only account is not leaked + +#### Scenario: Owner sees their own profile +- **WHEN** a user navigates to their own `/users/:username` while logged in +- **THEN** the page renders exactly the same as for a logged-out visitor, plus a small owner-only control strip linking to settings + +#### Scenario: Profile page emits social-share metadata +- **WHEN** any visitor loads a populated `/users/:username` +- **THEN** the page emits Open Graph tags (`og:title`, `og:site_name`, `og:type="profile"`) so links shared on social platforms render a meaningful preview + diff --git a/openspec/specs/route-management/spec.md b/openspec/specs/route-management/spec.md index df14b4b..1cfdae0 100644 --- a/openspec/specs/route-management/spec.md +++ b/openspec/specs/route-management/spec.md @@ -1,9 +1,7 @@ ## Purpose Route CRUD operations, GPX import/export, sequential versioning, PostGIS spatial storage, and route metadata in the Journal app. - ## Requirements - ### Requirement: Create route The Journal SHALL allow authenticated users to create a new route with a name and optional description. @@ -12,12 +10,28 @@ The Journal SHALL allow authenticated users to create a new route with a name an - **THEN** a new route record is created in PostgreSQL and the user is redirected to the route detail page ### Requirement: View route -The Journal SHALL display route details including map, metadata, and elevation stats. +The Journal SHALL display route details including map, metadata, and elevation stats. Access depends on the route's `visibility`: `public` routes are viewable by anyone including unauthenticated visitors, `unlisted` routes are viewable by anyone who has the URL, and `private` routes are viewable only by the owner. -#### Scenario: View route detail -- **WHEN** a user navigates to a route's URL +#### Scenario: Owner views own route +- **WHEN** a logged-in user navigates to a route they own at any visibility - **THEN** they see the route name, description, a map with the route polyline, distance, and elevation gain/loss +#### Scenario: Anyone views a public route +- **WHEN** any visitor (including unauthenticated) navigates to a `public` route's URL +- **THEN** they see the full route detail page as above + +#### Scenario: Anyone with the URL views an unlisted route +- **WHEN** any visitor navigates directly to an `unlisted` route's URL +- **THEN** they see the full route detail page as above + +#### Scenario: Non-owner is blocked from a private route +- **WHEN** a visitor who is not the owner requests a `private` route URL +- **THEN** the server responds with HTTP 404 (not 403), so the existence of the private route is not leaked + +#### Scenario: Public and unlisted route pages emit social-share metadata +- **WHEN** a visitor loads a `public` or `unlisted` route detail page +- **THEN** the response emits Open Graph and Twitter Card meta tags (`og:title`, `og:description`, `og:type="article"`, `og:site_name`, `twitter:card="summary"`) + ### Requirement: Update route The Journal SHALL allow the route owner to update the route name, description, and GPX. @@ -89,3 +103,30 @@ Routes SHALL be stored with a metadata envelope containing computed statistics ( #### Scenario: Metadata computed on save - **WHEN** a route GPX is saved - **THEN** distance and elevation statistics are computed from the GPX and stored in the metadata + +### Requirement: Route visibility +The Journal SHALL persist a `visibility` value on every route and SHALL allow the owner to change it. + +#### Scenario: New routes default to private +- **WHEN** a route is created without an explicit visibility +- **THEN** the route row is persisted with `visibility = 'private'` + +#### Scenario: Owner changes a route's visibility +- **WHEN** a route owner selects a different visibility (`private`, `unlisted`, `public`) in the edit flow and saves +- **THEN** the stored visibility is updated and subsequent access checks use the new value immediately + +#### Scenario: Non-owner cannot change visibility +- **WHEN** a request to update visibility arrives from a user who is not the route owner +- **THEN** the server rejects it with HTTP 403 or 404 (matching the current update-route behaviour), and the stored value is unchanged + +### Requirement: Route listings respect visibility +Any listing that exposes routes beyond the owner's own dashboard SHALL only include routes with `visibility = 'public'`. + +#### Scenario: Public profile lists only public routes +- **WHEN** a visitor loads `/users/:username` +- **THEN** the rendered list of routes includes only the user's `public` routes; `unlisted` and `private` routes are omitted + +#### Scenario: Owner's own routes list is unchanged +- **WHEN** a logged-in user views their own routes list at `/routes` +- **THEN** the list includes all of their own routes regardless of visibility +