chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
+149 -56
View File
@@ -1,80 +1,173 @@
'use client'
"use client";
import { useEffect, useRef, useState } from 'react'
import './latest-activity.css'
import { lockBodyScroll } from '../../lib/scrollLock'
import { useEffect, useRef, useState } from "react";
import "./latest-activity.css";
import { lockBodyScroll } from "../../lib/scrollLock";
type Event = { id: string; service: string; state: string; message: string; started_at?: string; finished_at?: string }
type Operation = { summary?: { title: string; message: string; next: string; action?: string }; id: string; label: string; status: string; events: Event[] }
type Event = { id: string; service: string; state: string; message: string; started_at?: string; finished_at?: string };
type Operation = {
summary?: { title: string; message: string; next: string; action?: string };
id: string;
label: string;
status: string;
events: Event[];
};
export default function LatestActivity({ operation, besideDownload, onDismiss }: {
operation: Operation; besideDownload: boolean; onDismiss: () => void
export default function LatestActivity({
operation,
besideDownload,
onDismiss,
}: {
operation: Operation;
besideDownload: boolean;
onDismiss: () => void;
}) {
const dialog = useRef<HTMLDialogElement>(null)
const trigger = useRef<HTMLButtonElement>(null)
const [open, setOpen] = useState(true)
useEffect(() => { setOpen(true) }, [operation.id])
const dialog = useRef<HTMLDialogElement>(null);
const trigger = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(true);
useEffect(() => {
void operation.id;
setOpen(true);
}, [operation.id]);
// Events are appended in order. Client result events have no timestamp.
const latest = operation.events.at(-1)
const working = operation.status === 'running' || operation.status === 'searching'
const message = latest?.message ?? ''
const choosing = /[1-9]\d* releases? (found|shown)/i.test(message)
const sent = operation.status === 'complete' && /sent|accepted.*release/i.test(message) && /download|release|Sonarr|Radarr/i.test(message)
const interrupted = latest?.id === 'connection-error'
const status = operation.summary?.title ?? (working ? 'Working on it' : choosing ? 'Choose a download' : operation.status === 'complete' ? 'Done' : 'Needs your attention')
const currentStep = operation.summary?.message ?? (working
? /send release/i.test(operation.label) ? 'Sending your download...'
: /search/i.test(operation.label) ? 'Looking for a download...'
: latest?.service === 'Jellyfin' ? 'Checking if it is ready to watch...'
: latest?.service === 'qBittorrent' ? 'Checking your download...'
: 'Checking your request...'
: interrupted ? 'The connection was lost.'
: choosing ? 'The search is finished. Choose a version to download.'
: sent ? 'Your download has been sent.'
: operation.status === 'error' ? 'We could not finish this step.'
: 'This check is finished.')
const nextStep = operation.summary?.next ?? (working ? 'Please wait. You can close this box while we work.'
: interrupted ? 'Close this box and check the request before trying again.'
: choosing ? 'Close this box to see the available downloads.'
: sent ? 'You can close this box. The request will update when the download starts.'
: operation.status === 'error' ? 'Close this box to review the request and its available options.'
: 'Close this box to see the updated request status.')
const progress = <div className={`activity-process is-${working ? 'working' : operation.status}`} role="progressbar" aria-label={currentStep} aria-valuemin={0} aria-valuemax={100} aria-valuenow={working || operation.status === 'error' ? undefined : 100}><span /></div>
const latest = operation.events.at(-1);
const working = operation.status === "running" || operation.status === "searching";
const message = latest?.message ?? "";
const choosing = /[1-9]\d* releases? (found|shown)/i.test(message);
const sent =
operation.status === "complete" &&
/sent|accepted.*release/i.test(message) &&
/download|release|Sonarr|Radarr/i.test(message);
const interrupted = latest?.id === "connection-error";
const status =
operation.summary?.title ??
(working
? "Working on it"
: choosing
? "Choose a download"
: operation.status === "complete"
? "Done"
: "Needs your attention");
const currentStep =
operation.summary?.message ??
(working
? /send release/i.test(operation.label)
? "Sending your download..."
: /search/i.test(operation.label)
? "Looking for a download..."
: latest?.service === "Jellyfin"
? "Checking if it is ready to watch..."
: latest?.service === "qBittorrent"
? "Checking your download..."
: "Checking your request..."
: interrupted
? "The connection was lost."
: choosing
? "The search is finished. Choose a version to download."
: sent
? "Your download has been sent."
: operation.status === "error"
? "We could not finish this step."
: "This check is finished.");
const nextStep =
operation.summary?.next ??
(working
? "Please wait. You can close this box while we work."
: interrupted
? "Close this box and check the request before trying again."
: choosing
? "Close this box to see the available downloads."
: sent
? "You can close this box. The request will update when the download starts."
: operation.status === "error"
? "Close this box to review the request and its available options."
: "Close this box to see the updated request status.");
const progress = (
<div
className={`activity-process is-${working ? "working" : operation.status}`}
role="progressbar"
aria-label={currentStep}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={working || operation.status === "error" ? undefined : 100}
>
<span />
</div>
);
useEffect(() => {
if (!open) return
const element = dialog.current
element?.showModal()
const unlock = lockBodyScroll()
if (!open) return;
const element = dialog.current;
element?.showModal();
const unlock = lockBodyScroll();
return () => {
element?.close()
unlock()
trigger.current?.focus()
}
}, [open])
element?.close();
unlock();
trigger.current?.focus();
};
}, [open]);
return (
<div className={`request-overview-block latest-activity ${besideDownload ? 'beside-download' : 'full-row'}`}>
<button ref={trigger} type="button" className="latest-activity-trigger" onClick={() => setOpen(true)} aria-haspopup="dialog" aria-expanded={open}>
<span className="latest-activity-heading"><span className="request-overview-label">Latest activity</span><span className={`latest-activity-badge is-${operation.status}`}>{status}</span></span>
<span className="latest-activity-message" role="status">{working && <span className="activity-spinner" aria-hidden="true" />}{currentStep}</span>
<div className={`request-overview-block latest-activity ${besideDownload ? "beside-download" : "full-row"}`}>
<button
ref={trigger}
type="button"
className="latest-activity-trigger"
onClick={() => setOpen(true)}
aria-haspopup="dialog"
aria-expanded={open}
>
<span className="latest-activity-heading">
<span className="request-overview-label">Latest activity</span>
<span className={`latest-activity-badge is-${operation.status}`}>{status}</span>
</span>
<span className="latest-activity-message" role="status">
{working && <span className="activity-spinner" aria-hidden="true" />}
{currentStep}
</span>
{progress}
<span className="latest-activity-more">View progress</span>
</button>
<dialog ref={dialog} className="activity-dialog" aria-labelledby="activity-dialog-title" onCancel={() => setOpen(false)} onClose={() => setOpen(false)}>
<dialog
ref={dialog}
className="activity-dialog"
aria-labelledby="activity-dialog-title"
onCancel={() => setOpen(false)}
onClose={() => setOpen(false)}
>
<div className="activity-dialog-content">
<header>
<div><span className="request-overview-label">Request progress</span><h2 id="activity-dialog-title">{status}</h2></div>
<button type="button" onClick={() => setOpen(false)}>Close</button>
<div>
<span className="request-overview-label">Request progress</span>
<h2 id="activity-dialog-title">{status}</h2>
</div>
<button type="button" onClick={() => setOpen(false)}>
Close
</button>
</header>
<div className="activity-current" role="status" aria-live="polite" aria-atomic="true">
<p className="activity-current-step">{working && <span className="activity-spinner" aria-hidden="true" />}{currentStep}</p>
<p className="activity-current-step">
{working && <span className="activity-spinner" aria-hidden="true" />}
{currentStep}
</p>
{progress}
<p className="activity-next-step">{nextStep}</p>
</div>
{!working && <footer><button type="button" onClick={() => { setOpen(false); onDismiss() }}>{operation.summary?.action ?? 'Dismiss activity'}</button></footer>}
{!working && (
<footer>
<button
type="button"
onClick={() => {
setOpen(false);
onDismiss();
}}
>
{operation.summary?.action ?? "Dismiss activity"}
</button>
</footer>
)}
</div>
</dialog>
</div>
)
);
}
+73 -29
View File
@@ -1,36 +1,80 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { authFetch, getApiBase } from '../../lib/auth'
import { useEffect, useState } from "react";
import { authFetch, getApiBase } from "../../lib/auth";
type AudioChoice = { language: { code: string } | null; originalEnabled?: boolean; canChange?: boolean; profileLanguage?: string }
type AudioChoice = {
language: { code: string } | null;
originalEnabled?: boolean;
canChange?: boolean;
profileLanguage?: string;
};
export default function RequestLanguage({ requestId, disabled, onApply }: {
requestId: string; disabled: boolean; onApply: (code: string) => Promise<void>
export default function RequestLanguage({
requestId,
disabled,
onApply,
}: {
requestId: string;
disabled: boolean;
onApply: (code: string) => Promise<void>;
}) {
const [choice, setChoice] = useState<AudioChoice | null>(null)
const [error, setError] = useState<string | null>(null)
const [revision, setRevision] = useState(0)
const [choice, setChoice] = useState<AudioChoice | null>(null);
const [error, setError] = useState<string | null>(null);
const [revision, setRevision] = useState(0);
useEffect(() => {
const controller = new AbortController()
void revision;
const controller = new AbortController();
void authFetch(`${getApiBase()}/requests/${requestId}/language`, { signal: controller.signal })
.then(async response => { if (!response.ok) throw new Error('Could not check the audio settings. Reload the request to try again.'); return response.json() })
.then(data => { if (!controller.signal.aborted) setChoice(data) })
.catch(e => { if (!controller.signal.aborted) setError(e.message) })
return () => controller.abort()
}, [requestId, revision])
if (error && !choice) return <p role="alert">{error}</p>
if (!choice?.language) return null
const code = choice.language.code
const name = new Intl.DisplayNames(['en'], { type: 'language' }).of(code) || code
return <section className="request-language-notice" aria-label="Audio language">
<h2>{name} audio {choice.originalEnabled ? 'enabled' : 'may need your approval'}</h2>
<p>This movie was originally made in {name}. An English dub may not exist. {choice.originalEnabled ? 'Radarr is set to accept its original audio.' : `The current audio requirement is ${choice.profileLanguage || 'set by the library'}. This can leave the request waiting even when an original-language release exists.`}</p>
<p>Accepting original audio keeps the quality requirements. Subtitles and individual release audio tracks are not guaranteed by title metadata.</p>
{choice.canChange && !choice.originalEnabled && <button type="button" disabled={disabled} onClick={async () => {
setError(null)
try { await onApply(code); setRevision(v => v + 1) } catch (e) { setError(e instanceof Error ? e.message : 'The audio choice could not be saved.') }
}}>Use {name} audio &amp; search</button>}
{error && <p role="alert">{error}</p>}
</section>
.then(async (response) => {
if (!response.ok) throw new Error("Could not check the audio settings. Reload the request to try again.");
return response.json();
})
.then((data) => {
if (!controller.signal.aborted) setChoice(data);
})
.catch((e) => {
if (!controller.signal.aborted) setError(e.message);
});
return () => controller.abort();
}, [requestId, revision]);
if (error && !choice) return <p role="alert">{error}</p>;
if (!choice?.language) return null;
const code = choice.language.code;
const name = new Intl.DisplayNames(["en"], { type: "language" }).of(code) || code;
return (
<section className="request-language-notice" aria-label="Audio language">
<h2>
{name} audio {choice.originalEnabled ? "enabled" : "may need your approval"}
</h2>
<p>
This movie was originally made in {name}. An English dub may not exist.{" "}
{choice.originalEnabled
? "Radarr is set to accept its original audio."
: `The current audio requirement is ${choice.profileLanguage || "set by the library"}. This can leave the request waiting even when an original-language release exists.`}
</p>
<p>
Accepting original audio keeps the quality requirements. Subtitles and individual release audio tracks are not
guaranteed by title metadata.
</p>
{choice.canChange && !choice.originalEnabled && (
<button
type="button"
disabled={disabled}
onClick={async () => {
setError(null);
try {
await onApply(code);
setRevision((v) => v + 1);
} catch (e) {
setError(e instanceof Error ? e.message : "The audio choice could not be saved.");
}
}}
>
Use {name} audio &amp; search
</button>
)}
{error && <p role="alert">{error}</p>}
</section>
);
}
File diff suppressed because it is too large Load Diff