From a3b0c6ad56b4d465de9411b38b62f97d2f4926ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 26 Apr 2026 09:40:42 +0200 Subject: [PATCH] Archive add-explore-page change and promote specs Promotes the deltas from openspec/changes/add-explore-page/ into top-level specs after the implementation landed in #321. - New top-level spec: openspec/specs/explore/spec.md (paginated local user directory at /explore, with the "Active recently" sub-section, exclusion rules for private profiles and the demo persona, offset pagination, and the navbar entry rule). - Updated openspec/specs/journal-landing/spec.md with the new "Visitor home links to /explore" requirement. - Updated openspec/CAPABILITIES.md with an entry under Social. - Moved openspec/changes/add-explore-page/ to openspec/changes/archive/2026-04-26-add-explore-page/. Co-Authored-By: Claude Opus 4.7 (1M context) --- openspec/CAPABILITIES.md | 1 + .../.openspec.yaml | 0 .../2026-04-26-add-explore-page}/design.md | 0 .../2026-04-26-add-explore-page}/proposal.md | 0 .../specs/explore/spec.md | 0 .../specs/journal-landing/spec.md | 0 .../2026-04-26-add-explore-page}/tasks.md | 0 openspec/specs/explore/spec.md | 106 ++++++++++++++++++ openspec/specs/journal-landing/spec.md | 12 ++ 9 files changed, 119 insertions(+) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/.openspec.yaml (100%) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/design.md (100%) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/proposal.md (100%) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/specs/explore/spec.md (100%) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/specs/journal-landing/spec.md (100%) rename openspec/changes/{add-explore-page => archive/2026-04-26-add-explore-page}/tasks.md (100%) create mode 100644 openspec/specs/explore/spec.md diff --git a/openspec/CAPABILITIES.md b/openspec/CAPABILITIES.md index cd85fd1..0ea1266 100644 --- a/openspec/CAPABILITIES.md +++ b/openspec/CAPABILITIES.md @@ -22,6 +22,7 @@ When adding a new spec, slot it into the most relevant group below and update th - [`social-follows`](specs/social-follows/spec.md) — follow API, follower/following collections (with locked-account access rules), Pending request lifecycle, `/feed`. - [`activity-feed`](specs/activity-feed/spec.md) — the `/feed` aggregation behavior (note: also referenced from `social-follows`; this spec covers feed-specific concerns). +- [`explore`](specs/explore/spec.md) — `/explore` page: paginated local user directory (public profiles only) ordered by recent public activity, plus an "Active recently" sub-section. Reachable to both signed-in users and anonymous visitors. ## Notifications & realtime diff --git a/openspec/changes/add-explore-page/.openspec.yaml b/openspec/changes/archive/2026-04-26-add-explore-page/.openspec.yaml similarity index 100% rename from openspec/changes/add-explore-page/.openspec.yaml rename to openspec/changes/archive/2026-04-26-add-explore-page/.openspec.yaml diff --git a/openspec/changes/add-explore-page/design.md b/openspec/changes/archive/2026-04-26-add-explore-page/design.md similarity index 100% rename from openspec/changes/add-explore-page/design.md rename to openspec/changes/archive/2026-04-26-add-explore-page/design.md diff --git a/openspec/changes/add-explore-page/proposal.md b/openspec/changes/archive/2026-04-26-add-explore-page/proposal.md similarity index 100% rename from openspec/changes/add-explore-page/proposal.md rename to openspec/changes/archive/2026-04-26-add-explore-page/proposal.md diff --git a/openspec/changes/add-explore-page/specs/explore/spec.md b/openspec/changes/archive/2026-04-26-add-explore-page/specs/explore/spec.md similarity index 100% rename from openspec/changes/add-explore-page/specs/explore/spec.md rename to openspec/changes/archive/2026-04-26-add-explore-page/specs/explore/spec.md diff --git a/openspec/changes/add-explore-page/specs/journal-landing/spec.md b/openspec/changes/archive/2026-04-26-add-explore-page/specs/journal-landing/spec.md similarity index 100% rename from openspec/changes/add-explore-page/specs/journal-landing/spec.md rename to openspec/changes/archive/2026-04-26-add-explore-page/specs/journal-landing/spec.md diff --git a/openspec/changes/add-explore-page/tasks.md b/openspec/changes/archive/2026-04-26-add-explore-page/tasks.md similarity index 100% rename from openspec/changes/add-explore-page/tasks.md rename to openspec/changes/archive/2026-04-26-add-explore-page/tasks.md diff --git a/openspec/specs/explore/spec.md b/openspec/specs/explore/spec.md new file mode 100644 index 0000000..5c61c68 --- /dev/null +++ b/openspec/specs/explore/spec.md @@ -0,0 +1,106 @@ +# explore Specification + +## Purpose +Local user discovery for the Journal: a paginated directory of local users with `profile_visibility = 'public'`, plus a small "Active recently" surface, served at `/explore`. Closes the gap between "I want to follow someone on this instance" and "I have a username from outside the app." Federation of remote actor discovery is out of scope here and lives in `social-federation`. + +## Requirements + +### Requirement: Local user directory at `/explore` +The Journal SHALL expose a route at `/explore` that renders a paginated directory of local users with `profile_visibility = 'public'`. The directory SHALL be reachable to both signed-in users and anonymous visitors. Each row SHALL display the user's display name, handle (`@username`), follower count (accepted only — see `social-follows` spec), and a short truncated bio when present. For signed-in viewers each row SHALL also render a Follow / Requested / Unfollow button reflecting the viewer's current relationship to that user (using the same locked-account semantics as the profile page); for anonymous viewers the button SHALL NOT render. + +#### Scenario: Signed-in user loads /explore +- **WHEN** a signed-in user requests `/explore` +- **THEN** the page renders a list of public local users with display name, handle, follower count, truncated bio, and a Follow / Requested / Unfollow button per row + +#### Scenario: Anonymous visitor loads /explore +- **WHEN** an unauthenticated visitor requests `/explore` +- **THEN** the page renders the same list of public local users (display name, handle, follower count, truncated bio), but no Follow / Requested / Unfollow button is rendered on any row + +#### Scenario: Truncated bio +- **WHEN** a user with a bio longer than 120 characters appears in the directory +- **THEN** the row shows the first 120 characters followed by an ellipsis + +#### Scenario: User with no bio +- **WHEN** a user with no bio (`bio IS NULL` or empty string) appears in the directory +- **THEN** the row simply omits the bio area; the row remains valid + +### Requirement: Directory is ordered by most-recent public activity +The directory SHALL be ordered by `MAX(activities.created_at) DESC` per user, considering only activities with `visibility = 'public'`. Users with no public activities SHALL sort to the end of the list, ordered among themselves by `users.created_at DESC` (newer registrations first). The ordering SHALL be deterministic — ties on the primary sort key SHALL be broken by `users.id` (descending) so pagination doesn't omit or duplicate rows. + +#### Scenario: Active user appears before inactive user +- **WHEN** user A has a public activity from yesterday and user B has no public activities +- **THEN** A appears before B in the directory listing + +#### Scenario: More-recently-active user appears first +- **WHEN** user A's most recent public activity is from today and user B's is from last week +- **THEN** A appears before B in the directory listing + +#### Scenario: Tied recency falls back to user id +- **WHEN** users A and B share the same `MAX(activities.created_at)` (or both have no public activities) +- **THEN** the ordering between them is determined by `users.id` descending (deterministic), so neither is omitted nor duplicated when paginating + +### Requirement: Excluded users +The directory SHALL exclude: + +1. Users with `profile_visibility = 'private'` — they have explicitly opted out of public discovery (Mastodon-style locked accounts). +2. The instance's demo persona, identified by the username returned by `loadPersona()` — the demo bot is not a real user and should not appear in real discovery. +3. (Forward-compat) Users in any future banned/suspended state — when such a status column exists, it SHALL be added to the exclusion filter. + +Excluded users SHALL NOT appear on `/explore` even if they have public activities and would otherwise sort to the top of the directory. + +#### Scenario: Private profile is excluded from the directory +- **WHEN** user A has `profile_visibility = 'private'` and any activity history +- **THEN** A does not appear in the `/explore` directory regardless of which page is requested + +#### Scenario: Demo persona is excluded +- **WHEN** the demo persona username (per `loadPersona()`) matches a row that would otherwise appear +- **THEN** that row is filtered out of the directory + +#### Scenario: Public user with no activities is included +- **WHEN** user A has `profile_visibility = 'public'` but has never created an activity +- **THEN** A still appears in the directory (sorted toward the end by the recency rule) + +### Requirement: "Active recently" sub-section +The `/explore` page SHALL render an "Active recently" sub-section at the top of the directory, listing up to N (default 5) public users who have created at least one public activity in the last 30 days, ordered by `MAX(activities.created_at) DESC`. The sub-section SHALL apply the same exclusion rules as the main directory (private profiles, demo persona, future banned/suspended users). When fewer than 1 user qualifies, the sub-section SHALL be omitted entirely (no empty header). + +#### Scenario: Active-recently strip rendered with qualifying users +- **WHEN** at least one public local user (excluding private/demo) has a public activity within the last 30 days +- **THEN** the page renders the "Active recently" sub-section above the main directory, with up to 5 such users + +#### Scenario: No qualifying users hides the strip +- **WHEN** no public local user (excluding private/demo) has any public activity within the last 30 days +- **THEN** the "Active recently" sub-section is not rendered at all; the main directory is the only listing on the page + +#### Scenario: Strip respects exclusion rules +- **WHEN** a private profile or the demo persona has a recent public activity +- **THEN** they are not included in the "Active recently" strip — the same exclusion rules apply as for the main directory + +### Requirement: Pagination +The directory SHALL paginate via `?page=N` (1-indexed) and `?perPage=K` query parameters. Page size SHALL default to 20 per page and SHALL be capped at 100; `perPage` values outside `[1, 100]` SHALL be clamped to that range without raising an error. The response SHALL surface a "Next page" link when more rows exist past the current page, and a "Previous page" link when `page > 1`. + +#### Scenario: Default pagination +- **WHEN** a visitor loads `/explore` with no query params +- **THEN** the page renders the first 20 users in the directory ordering + +#### Scenario: Subsequent page +- **WHEN** a visitor loads `/explore?page=2` +- **THEN** the page renders rows 21–40 in the directory ordering, with a "Previous page" link to `?page=1` and (if more rows exist) a "Next page" link to `?page=3` + +#### Scenario: Out-of-range perPage is clamped +- **WHEN** a visitor loads `/explore?perPage=10000` or `/explore?perPage=0` +- **THEN** the loader clamps `perPage` to `[1, 100]` and renders normally — no 400 response + +#### Scenario: Page beyond the last +- **WHEN** a visitor loads `/explore?page=N` where N is past the last populated page +- **THEN** the response renders the empty list and a "Previous page" link; no 404 + +### Requirement: Navbar entry for signed-in users +The Journal navbar SHALL include an "Explore" entry for signed-in users, linking to `/explore`. Anonymous visitors SHALL NOT see this entry in the navbar — they reach `/explore` via the visitor home (see `journal-landing` spec). The visual treatment of the entry is governed by the navbar redesign (Stream C in `docs/information-architecture.md`); this requirement only mandates that the entry exists. + +#### Scenario: Signed-in user sees the Explore entry +- **WHEN** a signed-in user loads any page +- **THEN** the navbar includes an "Explore" link to `/explore` + +#### Scenario: Anonymous user does not see the Explore entry in the navbar +- **WHEN** an unauthenticated visitor loads any page +- **THEN** the navbar does not include an Explore entry; the visitor home provides the in-app path to `/explore` instead diff --git a/openspec/specs/journal-landing/spec.md b/openspec/specs/journal-landing/spec.md index ea9fd2f..6da35e6 100644 --- a/openspec/specs/journal-landing/spec.md +++ b/openspec/specs/journal-landing/spec.md @@ -67,3 +67,15 @@ The navbar SHALL render a single inbox entry — a bell icon — for signed-in u #### Scenario: No standalone Follow requests entry - **WHEN** a signed-in user with one or more pending follow requests loads any page - **THEN** the navbar does NOT render a separate "Follow requests" link; the bell's unread badge already counts the corresponding `follow_request_received` notifications, and the Requests tab inside `/notifications` is the actionable surface + + +### Requirement: Visitor home links to /explore +For signed-out visitors on the flagship instance and self-hosted instances alike, the visitor home (`/`) SHALL include a link to `/explore` so anonymous visitors have an in-app path to browse the local user directory before signing up. The link SHALL be visually secondary to the auth CTAs (it does not compete with Register / Sign In as the conversion goal) but reachable on the same first-fold page section. + +#### Scenario: Visitor home renders the Explore link +- **WHEN** a signed-out visitor loads `/` +- **THEN** the page includes a link to `/explore` somewhere within the hero / CTAs section, visually secondary to Register and Sign In + +#### Scenario: Signed-in user does not see the visitor-home Explore link +- **WHEN** a signed-in user loads `/` +- **THEN** the visitor-home layout is not rendered (the personal dashboard takes its place); the navbar's Explore entry is the signed-in user's path to `/explore`