feat: add seasons from request details
Magent CI/CD / verify (push) Successful in 1m51s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-16 17:15:40 +12:00
parent 05a540ecbb
commit aed1bf9256
6 changed files with 532 additions and 5 deletions
+30
View File
@@ -2028,6 +2028,7 @@ button:disabled,
.request-overview,
.request-repair-activity,
.request-add-seasons,
.request-journey,
.request-advanced {
border: 1px solid var(--ops-line);
@@ -2203,6 +2204,30 @@ button:disabled,
}
.request-action-feedback.is-error { color: var(--request-red); background: rgba(255, 86, 113, 0.08); }
.request-add-seasons {
display: grid;
gap: 18px;
padding: 20px;
border-color: rgba(126, 215, 255, 0.32);
background:
radial-gradient(circle at 5% 0%, rgba(14, 165, 233, 0.13), transparent 34%),
rgba(255, 255, 255, 0.018);
}
.request-add-seasons-heading,
.request-add-seasons-submit { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.request-add-seasons-heading > div,
.request-add-seasons-submit > div { display: grid; gap: 5px; }
.request-add-seasons-heading h2 { margin: 0; font-size: clamp(1.25rem, 2.2vw, 1.8rem); }
.request-add-seasons-heading p { max-width: 82ch; margin: 0; color: var(--ops-muted); line-height: 1.5; }
.request-add-seasons-submit {
padding: 15px;
border: 1px solid var(--ops-line-soft);
border-radius: var(--ops-radius-lg);
background: rgba(255, 255, 255, 0.024);
}
.request-add-seasons-submit small { color: var(--ops-muted); line-height: 1.45; }
.request-add-seasons-submit button { flex: 0 0 auto; min-width: 190px; min-height: 44px; }
.request-operation-progress {
grid-column: 1 / -1;
display: grid;
@@ -3717,6 +3742,11 @@ textarea:focus {
.request-ready-actions { grid-template-columns: 1fr; }
.request-ready-actions > section + section { border-top: 1px solid var(--ops-line-soft); border-left: 0; }
.request-ready-actions > section > p { min-height: 0; }
.request-add-seasons-heading,
.request-add-seasons-submit { align-items: stretch; flex-direction: column; }
.request-add-seasons-heading .request-live-indicator { align-self: flex-start; }
.request-add-seasons .request-season-grid { grid-template-columns: 1fr; }
.request-add-seasons-submit button { width: 100%; }
.issue-prefilled-request { align-items: stretch; flex-direction: column; }
.issue-prefilled-request > a { text-align: center; }
}
+103 -4
View File
@@ -1,15 +1,15 @@
'use client'
import PageHeading from '../../ui/PageHeading'
import RequestLanguage from './RequestLanguage'
import { lockBodyScroll } from '../../lib/scrollLock'
import LatestActivity from './LatestActivity'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
import { canAccess } from '../../lib/features'
import { lockBodyScroll } from '../../lib/scrollLock'
import PageHeading from '../../ui/PageHeading'
import LatestActivity from './LatestActivity'
import RequestLanguage from './RequestLanguage'
type TimelineHop = {
service: string
@@ -36,6 +36,8 @@ type PipelineStage = {
missing?: number
total?: number
seasons?: Array<{ seasonNumber: number; available: number; missing: number; total: number }>
unmonitoredSeasons?: Array<{ seasonNumber: number; episodeCount: number; available: number }>
canAddSeasons?: boolean
missingEpisodes?: Record<string, number[]>
actionIds?: string[]
visible?: boolean
@@ -327,6 +329,7 @@ export default function RequestTimelinePage() {
const [operationProgress, setOperationProgress] = useState<OperationProgress | null>(null)
const [isAdmin, setIsAdmin] = useState(false)
const [canReportIssues, setCanReportIssues] = useState(false)
const [selectedAdditionalSeasons, setSelectedAdditionalSeasons] = useState<number[]>([])
const awaitingMediaIndex = Boolean(
snapshot?.presentation?.pipeline?.some(
(stage) => stage.id === 'available' && stage.state === 'active'
@@ -536,6 +539,8 @@ export default function RequestTimelinePage() {
const presentation = snapshot.presentation ?? {}
const pipeline = presentation.pipeline?.length ? presentation.pipeline : fallbackPipeline(snapshot)
const libraryStage = pipeline.find((stage) => stage.id === 'library')
const unmonitoredSeasons = libraryStage?.unmonitoredSeasons ?? []
const availableStage = pipeline.find((stage) => stage.id === 'available')
const mediaServerLink = availableStage?.state === 'complete' && availableStage.link
? availableStage.link
@@ -685,6 +690,41 @@ export default function RequestTimelinePage() {
}
}
const addSelectedSeasons = async () => {
if (!selectedAdditionalSeasons.length) return
setBusyAction('add_seasons')
setActionError(null)
setActionMessage(null)
try {
const response = await trackedPost(
'Add seasons to this request',
`${getApiBase()}/requests/${snapshot.request_id}/actions/add-seasons`,
{
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ season_numbers: selectedAdditionalSeasons }),
},
)
if (response.status === 401) {
clearToken()
router.push('/login')
return
}
if (!response.ok) throw new Error(await readApiError(response, 'The selected seasons could not be added.'))
const data = await response.json()
if (!isSnapshotPayload(data?.snapshot)) {
throw new Error('The seasons were added, but Magent did not return an updated request.')
}
setSnapshot(data.snapshot)
setSelectedAdditionalSeasons([])
setActionMessage(data?.message ?? 'The selected seasons were added and will now be monitored.')
} catch (error) {
console.error(error)
setActionError(error instanceof Error ? error.message : 'The selected seasons could not be added.')
} finally {
setBusyAction(null)
}
}
const runAction = async (action: RequestAction, searchOffset = 0) => {
if (action.requires_confirmation && !window.confirm(`Run “${action.label}”?`)) return
const actionPaths: Record<string, string> = {
@@ -867,6 +907,65 @@ export default function RequestTimelinePage() {
)}
</section>
{snapshot.request_type === 'tv' && unmonitoredSeasons.length > 0 && (
<section className="request-add-seasons" aria-labelledby="request-add-seasons-heading">
<div className="request-add-seasons-heading">
<div>
<span className="section-kicker">Collection expansion</span>
<h2 id="request-add-seasons-heading">Add more seasons</h2>
<p>
Sonarr knows about {unmonitoredSeasons.length} additional season{unmonitoredSeasons.length === 1 ? '' : 's'} that {unmonitoredSeasons.length === 1 ? 'is' : 'are'} not currently part of this request.
</p>
</div>
<span className="request-live-indicator"><i />Available to add</span>
</div>
<fieldset className="request-season-picker">
<legend>Choose seasons to monitor and search</legend>
<div className="request-season-actions">
<button type="button" disabled={Boolean(busyAction)} onClick={() => setSelectedAdditionalSeasons(unmonitoredSeasons.map((season) => season.seasonNumber))}>Select all</button>
<button type="button" disabled={Boolean(busyAction) || selectedAdditionalSeasons.length === 0} onClick={() => setSelectedAdditionalSeasons([])}>Clear</button>
</div>
<div className="request-season-grid">
{unmonitoredSeasons.map((season) => {
const selected = selectedAdditionalSeasons.includes(season.seasonNumber)
const episodeLabel = season.episodeCount > 0
? `${season.episodeCount} known episode${season.episodeCount === 1 ? '' : 's'}`
: 'Episodes not announced yet'
return (
<label className={selected ? 'is-selected' : ''} key={season.seasonNumber}>
<input
type="checkbox"
checked={selected}
disabled={Boolean(busyAction) || libraryStage?.canAddSeasons === false}
onChange={() => setSelectedAdditionalSeasons((current) => selected
? current.filter((number) => number !== season.seasonNumber)
: [...current, season.seasonNumber].sort((left, right) => left - right))}
/>
<span>
<strong>Season {season.seasonNumber}</strong>
<small>{episodeLabel}{season.available > 0 ? ` · ${season.available} already collected` : ''}</small>
</span>
</label>
)
})}
</div>
</fieldset>
<div className="request-add-seasons-submit">
<div>
<strong>{selectedAdditionalSeasons.length ? `${selectedAdditionalSeasons.length} season${selectedAdditionalSeasons.length === 1 ? '' : 's'} selected` : 'Choose one or more seasons'}</strong>
<small>Selected seasons will be monitored in Sonarr and released missing episodes will be searched immediately.</small>
</div>
{libraryStage?.canAddSeasons === false ? (
<span className="request-ready-unavailable">Automatic collection searches are not enabled for your account.</span>
) : (
<button type="button" disabled={Boolean(busyAction) || selectedAdditionalSeasons.length === 0} onClick={() => void addSelectedSeasons()}>
{busyAction === 'add_seasons' ? 'Adding seasons…' : 'Add selected seasons'}
</button>
)}
</div>
</section>
)}
{repairActivity?.visible && (
<section className={`request-repair-activity is-${repairActivity.state ?? 'searching'}`} aria-live="polite">
<div className="request-repair-heading">