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) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-24 21:57:21 +02:00
parent 9709a0c150
commit ba6f09171d
10 changed files with 118 additions and 12 deletions

View file

@ -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

View file

@ -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

View file

@ -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