feat: the announcement link, on the card and in a review queue (388 E5)
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 5s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 1m1s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m11s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 5s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 1m1s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m11s
Rule 27 — E5's other half. The matcher can propose; this is where the operator
decides, and where an accepted link actually shows up.
**The review queue** (Settings → Ingestion & filters). Each proposal shows the
per-signal breakdown, not just the total: "why did it suggest this" is the
question the operator actually has, and a lone percentage cannot answer it. So
a row reads "72% · timing 95% · says so 60%", and the copy states outright that
a pair always needs two reasons — which is the property that stops a busy
posting day from producing false pairs.
The empty state says so explicitly. Nothing proposed is the EXPECTED state most
of the time, and an empty queue that looks like a failure invites turning the
threshold down until it produces noise.
**On the card**, both directions, and accepted links only: the teaser gets "The
full set is in Discord", the drop gets "Announced on Patreon". A pending
proposal is a question for the review queue, never a claim to render beside the
artwork — that distinction is the whole confirm-only design, so it is asserted
in the backend (only `linked` rows reach the payload) and again here.
One detail worth the comment it carries: the link's target is `{ query: {
post_id } }` with no name or path. In vue-router that means "the current route
with these query params", so it works identically from Latest and from Browse —
and, more usefully, the card never reaches for `useRoute()`, which it has no
other reason to know about and which is not available when it is mounted in a
test without a router.
Backend CI on 235393c was green: all 13 E5 tests and migration 0094.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LNXXULQDjVZmbuNa2G9mD9
This commit is contained in:
@@ -128,6 +128,25 @@
|
||||
@click="toggleDesc"
|
||||
>{{ descExpanded ? 'Show less' : 'Show more' }}</button>
|
||||
|
||||
<!-- #388 E5: the accepted announcement link, both directions. Only
|
||||
ACCEPTED ones reach the payload, so anything rendered here is
|
||||
something the operator agreed to — never a proposal. -->
|
||||
<!-- A location with no name/path is "the current route, with these
|
||||
query params" — so this works from Latest or Browse alike without
|
||||
the card reaching for useRoute(), which it has no other reason to
|
||||
know about. -->
|
||||
<div v-if="associations.length" class="fc-post-card__assoc">
|
||||
<RouterLink
|
||||
v-for="a in associations" :key="a.id"
|
||||
:to="{ query: { post_id: a.post_id } }"
|
||||
class="fc-post-card__assoc-link"
|
||||
>
|
||||
<v-icon icon="mdi-link-variant" size="x-small" />
|
||||
{{ a.role === 'announces'
|
||||
? 'The full set is in Discord' : 'Announced on Patreon' }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- Off-platform file-host links (mega/gdrive/…) found in the body.
|
||||
Shown once expanded; clickable now, auto-downloaded by the worker
|
||||
slice. -->
|
||||
@@ -234,6 +253,10 @@ const sortDateIso = computed(() => props.post.post_date || props.post.downloaded
|
||||
// Tuesday but gained images this morning" is the whole signal that chat
|
||||
// content is trickling in.
|
||||
const grewAt = computed(() => (synthesized.value ? props.post.last_grew_at : null))
|
||||
// #388 E5. Accepted links only — a pending proposal lives in the review queue,
|
||||
// never beside the artwork. Defaults to [] so a post dict from before the
|
||||
// feature (or composed by hand) renders without a link rather than throwing.
|
||||
const associations = computed(() => props.post.associations || [])
|
||||
const grewRelative = computed(() => (grewAt.value ? relativeFrom(grewAt.value) : ''))
|
||||
const absoluteDate = computed(() => new Date(sortDateIso.value).toLocaleString())
|
||||
function relativeFrom (iso) {
|
||||
@@ -394,6 +417,17 @@ function formatBytes (n) {
|
||||
/* Growth is news, so it gets the accent the rest of the meta line doesn't —
|
||||
but it is still the meta line, not a badge competing with the artwork. */
|
||||
.fc-post-card__grew { color: rgb(var(--v-theme-accent)); }
|
||||
|
||||
.fc-post-card__assoc { margin-top: 8px; }
|
||||
.fc-post-card__assoc-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.8125rem;
|
||||
color: rgb(var(--v-theme-accent));
|
||||
text-decoration: none;
|
||||
}
|
||||
.fc-post-card__assoc-link:hover { text-decoration: underline; }
|
||||
.fc-post-card__date,
|
||||
.fc-post-card__meta { white-space: nowrap; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user