# Route Spec

## Route ID
`posts-get`

## Endpoint
`GET /api/v1/posts/{postId}`

## Human Description
Returns full post detail for the post page and discussion page, including optional article, post-frame thought text, slider text, slider labels, visibility, permissions, and repost metadata. Optional authentication is used for visibility checks.

## Authentication
- Required: `no` (valid optional bearer can reveal private locked hub content visible to that user)
- Auth type: `none|bearer token`

## Request
### Headers
- `Authorization: Bearer <token>` (optional)

### Path Parameters
- `postId` (`string`, required)

## Auth-Dependent Visibility
- Missing auth is allowed and returns public/public-hub posts only.
- A valid bearer token identifies the viewer. The same `postId` can return `200 OK` for the author or an active member of one selected private locked hub, but `404 Not Found` for anonymous viewers and non-members.
- Invalid or revoked optional bearer tokens are currently treated as anonymous by the common auth middleware, so this route does not return `401 Unauthorized` for bad optional credentials.
- Hidden private locked hub posts return `404 POST_NOT_FOUND` instead of `401` or `403`, so the API does not leak whether the post exists.

## Responses
### Success: `200 OK`
```json
{
  "success": true,
  "message": "Post loaded",
  "data": {
    "id": "pst_1",
    "author": {"id": "usr_1", "username": "sara"},
    "article": {"id": "mc_1", "kind": "text", "text": "Original content being discussed"},
    "thoughtText": "My take on this",
    "sliderText": "Do you agree?",
    "slider": {"leftLabel": "Agree", "rightLabel": "Disagree"},
    "voteSummary": {
      "totalVotes": 12,
      "resultsVisible": true,
      "minimumVotesToShow": 3,
      "leftCount": 7,
      "neutralCount": 2,
      "rightCount": 3
    },
    "voteDistribution": {
      "totalVotes": 12,
      "resultsVisible": true,
      "minimumVotesToShow": 3,
      "buckets": [
        {"position": "strong_left", "count": 3},
        {"position": "left", "count": 4},
        {"position": "neutral", "count": 2},
        {"position": "right", "count": 2},
        {"position": "strong_right", "count": 1}
      ]
    },
    "viewerVote": {
      "hasVoted": true,
      "votedAt": "2026-05-19T10:00:00Z"
    },
    "isRepost": false,
    "sourcePostId": null,
    "sourcePost": null,
    "repostCount": 3,
    "visibility": "public",
    "canRepost": true,
    "canShare": true,
    "createdAt": "2026-02-18T10:00:00Z"
  }
}
```

### Error: `404 Not Found`
When returned:
- Post does not exist.
- Post is private locked hub content and the viewer is not the author or an active member of one selected hub.

Body:
```json
{"success": false, "error": {"code": "POST_NOT_FOUND", "message": "Post does not exist.", "details": {}}}
```

## Data & Caching Dependencies
- **Spanner Tables:** `posts, post_main_contents, post_hub_targets, users, hub_memberships, post_vote_counters, post_repost_counters, post_vote_receipts, post_vote_scope_receipts (Read)`
- **Redis Cache:** `None`
- **GCS Storage:** `None`
- **Edge Cache (CDN):** `Yes (if public)`

## Side Effects
- None (read-only endpoint).

## Repost Rendering
- Original posts may have article-only, thought-only, slider-only, or combined content.
- `sliderText` is the visible slider title/prompt for the current frame; there is no separate `sliderTitle` field.
- `slider` is current-frame vote-control metadata. It provides labels for voting on this card frame.
- For reposts, repost-owned display content is limited to `thoughtText` and optional `sliderText`/`slider`; top-level `article` is null.
- For reposts, `sourcePost` points to the original post. Clients render source fallback as `sourcePost.article`, then `sourcePost.thoughtText`, then `sourcePost.sliderText`.
- `sourcePost.slider` may be present as source slider metadata, but it does not make the current repost frame votable.
- Repost of repost is not allowed, so `sourcePost` points only to an original post.
- Private locked hub posts require the viewer to be the author or an active member of at least one selected hub.
- Private locked hub posts return `canRepost=false` and `canShare=false`.

## Vote Result Visibility
- Posts without `slider` are not votable.
- `voteSummary` hides bucket counts until the displayed vote scope has at least 3 votes.
- When hidden, `voteSummary.resultsVisible=false` and `leftCount`, `neutralCount`, and `rightCount` are `null`.
- Responses include `viewerVote`, which exposes only whether the viewer voted and when. It never returns the viewer's selected position.
- Slider responses include `voteDistribution` only when the authenticated viewer already voted. When aggregate results are still hidden, `voteDistribution.resultsVisible=false` and `buckets` is empty.
- `GET /api/v1/posts/{postId}/vote/me` returns the same vote status detail without exposing the selected position.
