Add Radarr/Sonarr links and richer metadata to alert detail

- Thread titleSlug through RadarrMovie/SonarrSeries → MediaEntry →
  AlertCandidate, building a direct mediaUrl server-side at alert
  generation time (RADARR_URL/movie/slug, SONARR_URL/series/slug)
- Alert detail page: single-column full-width layout, metadata chips
  row showing opened/closed dates, media type, and a "View in Radarr/
  Sonarr" external link button when available
- Comments section sits below the full-width overview card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 11:50:23 -04:00
parent c86b8ff33a
commit 4b2c82cf90
6 changed files with 143 additions and 75 deletions
+8
View File
@@ -128,6 +128,9 @@ export function generateAlertCandidates(
if (flaggedMovies.has(tmdbId)) continue;
flaggedMovies.add(tmdbId);
const byStr = requestedBy.slice(0, 3).join(", ") + (requestedBy.length > 3 ? ` +${requestedBy.length - 3}` : "");
const mediaUrl = entry.titleSlug && process.env.RADARR_URL
? `${process.env.RADARR_URL}/movie/${entry.titleSlug}`
: undefined;
candidates.push({
key: `unfulfilled:movie:${tmdbId}`,
category: "unfulfilled",
@@ -137,6 +140,7 @@ export function generateAlertCandidates(
mediaId: tmdbId,
mediaType: "movie",
mediaTitle: entry.title,
mediaUrl,
});
}
@@ -151,6 +155,9 @@ export function generateAlertCandidates(
const description = pct !== null
? `Only ${pct}% of episodes downloaded (${sonarrEntry!.episodeFileCount}/${sonarrEntry!.totalEpisodeCount}). Approved ${formatAge(oldestAgeHours)} ago. Requested by ${byStr}.`
: `Approved ${formatAge(oldestAgeHours)} ago but no files found in Sonarr. Requested by ${byStr}.`;
const mediaUrl = sonarrEntry?.titleSlug && process.env.SONARR_URL
? `${process.env.SONARR_URL}/series/${sonarrEntry.titleSlug}`
: undefined;
candidates.push({
key: `unfulfilled:tv:${tvdbId}`,
category: "unfulfilled",
@@ -160,6 +167,7 @@ export function generateAlertCandidates(
mediaId: tvdbId,
mediaType: "tv",
mediaTitle: entry.title,
mediaUrl,
});
}
+4
View File
@@ -48,6 +48,7 @@ interface StoredAlert {
mediaId?: number;
mediaType?: string;
mediaTitle?: string;
mediaUrl?: string;
status: AlertStatus;
closeReason: AlertCloseReason | null;
suppressedUntil: string | null;
@@ -84,6 +85,7 @@ function toAlert(s: StoredAlert): Alert {
mediaId: s.mediaId,
mediaType: s.mediaType as Alert["mediaType"],
mediaTitle: s.mediaTitle,
mediaUrl: s.mediaUrl,
status: s.status,
closeReason: s.closeReason ?? null,
suppressedUntil: s.suppressedUntil,
@@ -147,6 +149,7 @@ export function upsertAlerts(candidates: AlertCandidate[]): number {
existing.description = c.description;
if (c.userName) existing.userName = c.userName;
if (c.mediaTitle) existing.mediaTitle = c.mediaTitle;
if (c.mediaUrl) existing.mediaUrl = c.mediaUrl;
} else {
store.alerts[c.key] = {
id: store.nextId++,
@@ -160,6 +163,7 @@ export function upsertAlerts(candidates: AlertCandidate[]): number {
mediaId: c.mediaId,
mediaType: c.mediaType,
mediaTitle: c.mediaTitle,
mediaUrl: c.mediaUrl,
status: "open",
closeReason: null,
suppressedUntil: null,
+1 -1
View File
@@ -13,7 +13,7 @@ export async function buildRadarrMap(): Promise<Map<number, MediaEntry>> {
return new Map(
movies.map((m) => [
m.tmdbId,
{ title: m.title, sizeOnDisk: m.sizeOnDisk, available: m.isAvailable },
{ title: m.title, titleSlug: m.titleSlug, sizeOnDisk: m.sizeOnDisk, available: m.isAvailable },
])
);
}
+1
View File
@@ -15,6 +15,7 @@ export async function buildSonarrMap(): Promise<Map<number, MediaEntry>> {
s.tvdbId,
{
title: s.title,
titleSlug: s.titleSlug,
sizeOnDisk: s.statistics.sizeOnDisk,
// "upcoming" = series hasn't started airing yet
available: s.status !== "upcoming",
+4
View File
@@ -23,6 +23,7 @@ export interface OverseerrRequest {
export interface RadarrMovie {
tmdbId: number;
title: string;
titleSlug: string;
sizeOnDisk: number; // bytes
isAvailable: boolean; // false = unreleased / below minimum availability
}
@@ -30,6 +31,7 @@ export interface RadarrMovie {
export interface SonarrSeries {
tvdbId: number;
title: string;
titleSlug: string;
status: string; // "continuing" | "ended" | "upcoming" | "deleted"
statistics: {
sizeOnDisk: number; // bytes
@@ -51,6 +53,7 @@ export interface TautulliUser {
export interface MediaEntry {
title: string;
titleSlug?: string;
sizeOnDisk: number; // bytes
available: boolean; // false = unreleased, skip unfulfilled alerts
// TV-specific (undefined for movies)
@@ -112,6 +115,7 @@ export interface AlertCandidate {
mediaId?: number;
mediaType?: "movie" | "tv";
mediaTitle?: string;
mediaUrl?: string; // direct link to the item in Radarr/Sonarr
}
export interface AlertComment {