feat(web): pipeline stats bar, summarized-first sort, Try now button for rate-limited videos
Three UX improvements for the pending-transcript state:
1. Summarized videos sort to top (ORDER BY (s.id IS NOT NULL) DESC, seen_at DESC)
so completed summaries are always immediately visible without filtering.
ListVideos default limit raised from 50 to 500 to show the full backlog.
2. Pipeline stats bar above the video list: '2 summarized · 256 fetching soon · 12
no captions' — computed from the unfiltered row set, hidden when everything is
summarized.
3. 'Try now' button on rate-limited cards replaces the passive 'Retrying later'
chip. POST /v/{id}/retry-now clears rate_limited_at then calls ProcessVideo
through the shared globalFetchGate — same rate limiting as the scheduler, safe
under concurrent use.
This commit is contained in:
@@ -388,6 +388,38 @@ func disconnectURL(provider string) templ.SafeURL {
|
||||
return templ.SafeURL("/account/disconnect/" + provider)
|
||||
}
|
||||
|
||||
// PipelineStats summarises the user's video backlog so the list page can show
|
||||
// a one-line status bar ("2 summaries · 256 fetching soon · 12 no captions").
|
||||
type PipelineStats struct {
|
||||
Summarized int
|
||||
RateLimited int // in the backoff window, will be retried
|
||||
NoText int // no caption track available
|
||||
Pending int // discovered but not yet attempted
|
||||
}
|
||||
|
||||
// pipelineStats computes a PipelineStats from all (unfiltered) rows.
|
||||
func pipelineStats(rows []store.SummaryRow) PipelineStats {
|
||||
var s PipelineStats
|
||||
for _, r := range rows {
|
||||
switch {
|
||||
case r.Summarized:
|
||||
s.Summarized++
|
||||
case r.TranscriptStatus == "rate_limited":
|
||||
s.RateLimited++
|
||||
case r.TranscriptStatus == "none":
|
||||
s.NoText++
|
||||
default:
|
||||
s.Pending++
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// retryNowURL builds the POST path for manual retry of a rate-limited video.
|
||||
func retryNowURL(videoID string) templ.SafeURL {
|
||||
return templ.SafeURL("/v/" + videoID + "/retry-now")
|
||||
}
|
||||
|
||||
// Filter holds the list-view query parameters. Empty fields mean "no constraint".
|
||||
// Dates are kept as the raw YYYY-MM-DD strings so the form re-renders the user's
|
||||
// input verbatim; parsing happens in matchFilter.
|
||||
@@ -509,6 +541,12 @@ a.btn, a.btn:visited { color: var(--accent-fg); }
|
||||
/* passive "retrying later" chip: dim/grey (CharmDim), not the accent — it is a
|
||||
status, not an action the user can take. */
|
||||
.chip-retry { background: rgba(108, 108, 108, .16); color: #6c6c6c; }
|
||||
.pipeline-bar { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; margin-bottom: var(--s3); font-size: .8rem; color: var(--muted); }
|
||||
.pipeline-bar span { display: flex; align-items: center; gap: var(--s1); }
|
||||
.pipeline-bar span + span::before { content: "·"; margin-right: var(--s1); }
|
||||
.retry-form { display: inline; }
|
||||
.btn-retry { font: inherit; font-size: .72rem; font-weight: 600; padding: .15rem .55rem; border-radius: 999px; border: 1px solid var(--accent); background: transparent; color: var(--accent); cursor: pointer; }
|
||||
.btn-retry:hover { background: var(--accent-weak); }
|
||||
.chip-warn { background: rgba(255, 110, 156, .15); color: #FF6E9C; }
|
||||
.card-state { color: var(--muted); font-size: .8rem; }
|
||||
.badge { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--badge-bg); color: var(--badge-fg); font-size: .72rem; font-weight: 600; }
|
||||
|
||||
Reference in New Issue
Block a user