Add a card to the operator promo band (GitHub
POST /admin/featured/promo/items
Appends one asset or collection to the band, at the end of its own ordering — band positions are computed WITHIN the band, so adding a card never reads or writes a rail position.
Removal and reordering are the EXISTING endpoints:
DELETE /admin/featured/{id} takes any placement id, band card
or rail entry, and PUT /admin/featured/order assigns positions
by id. Minting band-only twins of two operations that already do
exactly the right thing would have been two more places for the
next change to miss.
409 when the subject is already a card in this band. The same subject may still be on the rail, and may be in another band — those are different placements on different surfaces, which is what the widened uniqueness key in migration 00053 exists to allow.
404 when the install has no band to add to.
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”A card to add to the band (#1118).
NO scope, deliberately. A band card takes its audience from
its band, and accepting one here would store a value that looks
authoritative and is never read — an operator setting a card to
public, seeing it stored, and believing they had widened the
band. POST /admin/featured refuses the combination for the
same reason.
NO team kind either. A team tile belongs to the signed-in
teams rail (GET /featured/teams), whose render-time hero
re-check (#982) is the only admissible route to a team’s
picture; the band renders covers, so it takes the two subject
kinds that resolve one.
object
Responses
Section titled “ Responses ”The created placement.
GitHub #341 — one admin-curated featured entry pointing at an
asset, a collection or a team, with an explicit ordering
position and the resolved subject title. Assets and collections
carry thumbnail hints so a tile can render a cover: assets from
themselves, collections via ADR 0027’s hero-card fallback
(#559). A team subject (#1084) resolves its NAME only and
carries no hints — a team’s picture is admissible solely through
the render-time hero re-check (#982), and resolving it here
would be a second copy of that rule inside a query whose gates
are deliberately weaker than the public rail’s.
object
The featured_items row id (used for remove/reorder).
Must list every value FeaturedItemInput.subject_kind
accepts. It is a separate schema and was the place this was
nearly missed in #1084: the server would have gone on
serialising team (Go emits the string it holds), while the
generated client’s type narrowed it away and a strict
response validator rejected the row — a placement the
operator could see in the database and not in the UI.
0-based display order.
Resolved subject title (asset title or collection name). Empty when the subject was deleted out from under the row.
The wide card’s second line (#1110): the COLLECTION’s own
description, empty when it has none — and empty for every
other subject kind.
WITHHELD EXACTLY WHEN title IS, structurally rather than
by a second rule. It is selected from the same c join
that produces title for a collection subject, and that
join carries the caller’s ADR 0063 collection predicate in
its ON clause: a collection this caller may not see fails
to join, contributes neither name nor description, and the
placement row is then dropped entirely. There is no
arrangement in which a description arrives without the name
it belongs to.
ASSET SUBJECTS CARRY NO SUBTITLE, deliberately. ADR 0020
makes a non-public asset title-only on this surface — the
rail already suppresses its thumbnail hints for exactly
that reason — so sourcing this from assets.description
would widen an embargo tile from “title” to “title and the
author’s notes”. #1110 specifies the collection’s
description; the asset half of that widening is not a
rendering choice this field gets to make.
How many members of a COLLECTION subject THIS CALLER can
see — the wide card’s subtitle fallback when subtitle is
empty (#1110). Null for every non-collection subject.
Caller-scoped, not a raw COUNT(*). The membership is the
same one the mosaic composes from — pinned, unexpired
collection_posts rows, gated by the caller’s own post read
predicate. A bare count would publish the SIZE of the
withheld part of a public collection, which is the
derived-copy leak class #902 closed for search text and
#1066 closed for embeddings, arriving through a subtitle.
#1236 dropped the collection_resources half that used to
be added to this. The tile describes what the page shows,
and the page shows posts; a tile advertising more items than
the collection opens to is the count disagreeing with the
thing it counts.
The asset whose col variant renders this tile (#559).
For an asset subject this is the subject itself. For a
COLLECTION it is the hero-card fallback of ADR 0027 — the
most-recent post’s cover asset — which is why the id is
needed separately: subject_id is the collection, and the
variant endpoint is keyed by asset.
Null when nothing is servable to this caller, in which case
the tile is title-only. Subject to the same gating as
asset_file_hash, so it never names an asset the caller
cannot see.
SINCE #1207 A COLLECTION’S IS THE CURATOR’S CHOICE WHEN
THERE IS ONE. The rail resolves it in preference order —
featured_cover_asset_id, then cover_asset_id, then ADR
0027’s derived cover — with every rung carrying the caller’s
own asset predicate, so a chosen cover this caller may not
picture falls through to the next rung instead of leaking.
Before that the rail read neither chosen column and always
derived, which is #1200: the strip showed one picture and
every other collection surface showed another.
Where to centre this tile’s crop horizontally, as a fraction
of the cover picture’s width (#1207) — the curator’s
featured_cover_focal_x, ready to be written straight into
CSS object-position.
Null means centre, which is the CSS default and what every tile did before this existed, so a client that ignores it renders exactly what it rendered before.
ONLY EVER SET WHEN THE TILE IS SHOWING A CHOSEN COVER. A derived cover is a picture the curator never saw in the editor, so positioning chosen for a different picture is not carried onto it; that arm returns null and the tile centres. Null for every non-collection subject.
The vertical half of the tile’s focal point (#1207). See
cover_focal_x.
How far this tile’s crop is tightened, as a multiplier on
the fitting rectangle (#1212) — the curator’s
featured_cover_zoom, ready to be applied alongside
cover_focal_x/_y.
Null means fit, which is what every tile did before this existed. ONLY EVER SET WHEN THE TILE IS SHOWING A CHOSEN COVER, for the reason the focal point is: a derived cover is a picture the curator never framed, so a framing chosen for a different picture is not carried onto it.
Thumbnail hint for the tile’s cover asset; null when there is nothing servable for this caller. Populated for collection subjects too since #559.
True iff a servable col variant exists for the tile’s
cover asset AND the caller passes the content plane (ADR
0064). The rail renders a thumbnail only when true, else a
title-only tile with no byte request (#471). Computed from
live variant existence rather than from a stored flag, which
is what makes it trustworthy — the asset_has_image field
that used to sit beside this one was the projection of a
column nothing wrote, and was removed with it in #579.
Populated for collection subjects too since #559.
True iff every variant in the operator’s CONFIGURED preview ladder exists for the tile’s cover asset AND the caller passes the content plane. Identical contract to Asset.ladder_available — see there for why this is computed from config rather than from the default rung list.
The subject’s owner, as the promo band’s card chip draws it (#1118) — the asset’s owner for an asset subject, the collection’s for a collection.
POPULATED ONLY BY GET /featured/promo, and the absence
elsewhere is a statement rather than an oversight. The
featured rail’s card prints a subtitle, not an author
(#1110), and a value nothing consumes is a value nothing
tests; the admin curation list identifies a row by its
subject id, which is what an operator needs to prune it.
The name inside is resolved by users.ResolveDisplayName
through users.LookupAuthors — the one home of the
display-name ladder, with its authenticated rung and its
anonymous arm. It is NOT re-derived per surface: #1023
exists because that ladder had been transcribed four times
and three copies were wrong.
ABSENT — not redacted — for an author who took ADR 0024’s opt-out and for a hard-deleted account, exactly as on a post card. “No author object” means “no name to show”, never “look it up another way”.
object
The handle. Also the profile link target
(/users/by-username/{username}).
Resolved display string ready to render — never empty.
RESOLUTION IS CALLER-DEPENDENT and matches UserPublic’s
exactly, because it is literally the same expression
(users.ResolveDisplayName):
authenticated: profile.display_name → user.fullname → username anonymous: profile.display_name → username
The anonymous rung skips fullname on purpose. Real name
is authenticated-only (ADR 0070 §3), and a display-name
fallback that reaches it would be that rule defeated by a
COALESCE.
Null for users with no uploaded avatar; the card renders a generated initials disc in that case.
Malformed request
object
Human-readable error summary
Example
the request could not be completedAuthentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Authenticated but missing required capabilities
object
Human-readable error summary
Example
the request could not be completedResource not found
object
Human-readable error summary
Example
the request could not be completedThat subject is already a card in this band.
object
Human-readable error summary
Example
the request could not be completed