feat(frontend): schedule history dialog with rollback, wire ConfigHistorySheet

This commit is contained in:
2026-03-17 14:48:39 +01:00
parent ba6abad602
commit 6d350940b9
4 changed files with 125 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ import {
Download, Download,
ChevronUp, ChevronUp,
ChevronDown, ChevronDown,
History,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useActiveSchedule } from "@/hooks/use-channels"; import { useActiveSchedule } from "@/hooks/use-channels";
@@ -29,6 +30,7 @@ interface ChannelCardProps {
onExport: () => void; onExport: () => void;
onMoveUp: () => void; onMoveUp: () => void;
onMoveDown: () => void; onMoveDown: () => void;
onScheduleHistory: () => void;
} }
function useScheduleStatus(channelId: string) { function useScheduleStatus(channelId: string) {
@@ -69,6 +71,7 @@ export function ChannelCard({
onExport, onExport,
onMoveUp, onMoveUp,
onMoveDown, onMoveDown,
onScheduleHistory,
}: ChannelCardProps) { }: ChannelCardProps) {
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const blockCount = Object.values(channel.schedule_config.day_blocks).reduce( const blockCount = Object.values(channel.schedule_config.day_blocks).reduce(
@@ -185,6 +188,15 @@ export function ChannelCard({
> >
<CalendarDays className="size-3.5" /> <CalendarDays className="size-3.5" />
</Button> </Button>
<Button
size="icon-sm"
variant="ghost"
onClick={onScheduleHistory}
title="Schedule history"
className="text-zinc-600 hover:text-zinc-200"
>
<History className="size-3.5" />
</Button>
<Button <Button
size="icon-sm" size="icon-sm"
asChild asChild

View File

@@ -15,6 +15,7 @@ import { RecyclePolicyEditor } from "./recycle-policy-editor";
import { WebhookEditor } from "./webhook-editor"; import { WebhookEditor } from "./webhook-editor";
import { AccessSettingsEditor } from "./access-settings-editor"; import { AccessSettingsEditor } from "./access-settings-editor";
import { LogoEditor } from "./logo-editor"; import { LogoEditor } from "./logo-editor";
import { ConfigHistorySheet } from "./config-history-sheet";
import { useChannelForm } from "@/hooks/use-channel-form"; import { useChannelForm } from "@/hooks/use-channel-form";
import { channelFormSchema, extractErrors } from "@/lib/schemas"; import { channelFormSchema, extractErrors } from "@/lib/schemas";
import type { FieldErrors } from "@/lib/schemas"; import type { FieldErrors } from "@/lib/schemas";
@@ -748,7 +749,13 @@ export function EditChannelSheet({
</Button> </Button>
</div> </div>
</div> </div>
{/* TODO: ConfigHistorySheet — wired in Task 16 */} {channel && (
<ConfigHistorySheet
channelId={channel.id}
open={configHistoryOpen}
onOpenChange={setConfigHistoryOpen}
/>
)}
</form> </form>
</SheetContent> </SheetContent>
</Sheet> </Sheet>

View File

@@ -0,0 +1,94 @@
'use client'
import { useState } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { useScheduleHistory, useRollbackSchedule } from '@/hooks/use-channels'
interface Props {
channelId: string
open: boolean
onOpenChange: (open: boolean) => void
}
const fmtDateRange = (from: string, until: string) =>
`${new Date(from).toLocaleDateString()} ${new Date(until).toLocaleDateString()}`
export function ScheduleHistoryDialog({ channelId, open, onOpenChange }: Props) {
const { data: entries } = useScheduleHistory(channelId)
const rollback = useRollbackSchedule()
const [confirmId, setConfirmId] = useState<string | null>(null)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Schedule history</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2 mt-2 max-h-[60vh] overflow-y-auto">
{(entries ?? []).map((entry, i) => (
<div
key={entry.id}
className="flex items-center gap-3 p-3 rounded border border-border"
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium">
Gen #{entry.generation}
{i === 0 && (
<span className="ml-2 text-xs text-green-400 bg-green-950 px-1.5 py-0.5 rounded">
active
</span>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{fmtDateRange(entry.valid_from, entry.valid_until)}
</div>
</div>
{i > 0 && (
confirmId === entry.id ? (
<div className="flex items-center gap-1 text-xs">
<span className="text-amber-400 whitespace-nowrap">Roll back to gen #{entry.generation}?</span>
<Button
size="sm"
variant="destructive"
disabled={rollback.isPending}
onClick={() => {
rollback.mutate({ channelId, genId: entry.id })
setConfirmId(null)
onOpenChange(false)
}}
>
Confirm
</Button>
<Button size="sm" variant="ghost" onClick={() => setConfirmId(null)}>
Cancel
</Button>
</div>
) : (
<Button
size="sm"
variant="outline"
onClick={() => setConfirmId(entry.id)}
>
Rollback to here
</Button>
)
)}
</div>
))}
{(entries ?? []).length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No schedule history yet. Generate a schedule to get started.
</p>
)}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -28,6 +28,7 @@ import {
} from "./components/import-channel-dialog"; } from "./components/import-channel-dialog";
import { IptvExportDialog } from "./components/iptv-export-dialog"; import { IptvExportDialog } from "./components/iptv-export-dialog";
import { TranscodeSettingsDialog } from "./components/transcode-settings-dialog"; import { TranscodeSettingsDialog } from "./components/transcode-settings-dialog";
import { ScheduleHistoryDialog } from "./components/schedule-history-dialog";
import type { import type {
ChannelResponse, ChannelResponse,
ProgrammingBlock, ProgrammingBlock,
@@ -59,6 +60,7 @@ export default function DashboardPage() {
const [editChannel, setEditChannel] = useState<ChannelResponse | null>(null); const [editChannel, setEditChannel] = useState<ChannelResponse | null>(null);
const [deleteTarget, setDeleteTarget] = useState<ChannelResponse | null>(null); const [deleteTarget, setDeleteTarget] = useState<ChannelResponse | null>(null);
const [scheduleChannel, setScheduleChannel] = useState<ChannelResponse | null>(null); const [scheduleChannel, setScheduleChannel] = useState<ChannelResponse | null>(null);
const [scheduleHistoryChannelId, setScheduleHistoryChannelId] = useState<string | null>(null);
const handleCreate = (data: { const handleCreate = (data: {
name: string; name: string;
@@ -186,6 +188,7 @@ export default function DashboardPage() {
onExport={() => exportChannel(channel)} onExport={() => exportChannel(channel)}
onMoveUp={() => handleMoveUp(channel.id)} onMoveUp={() => handleMoveUp(channel.id)}
onMoveDown={() => handleMoveDown(channel.id)} onMoveDown={() => handleMoveDown(channel.id)}
onScheduleHistory={() => setScheduleHistoryChannelId(channel.id)}
/> />
))} ))}
</div> </div>
@@ -246,6 +249,14 @@ export default function DashboardPage() {
}} }}
/> />
{scheduleHistoryChannelId && (
<ScheduleHistoryDialog
channelId={scheduleHistoryChannelId}
open={!!scheduleHistoryChannelId}
onOpenChange={open => !open && setScheduleHistoryChannelId(null)}
/>
)}
{deleteTarget && ( {deleteTarget && (
<DeleteChannelDialog <DeleteChannelDialog
channelName={deleteTarget.name} channelName={deleteTarget.name}