mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
Add Rescan Channel option to channels tab
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
Power,
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,12 +24,14 @@ interface ChannelColumnsProps {
|
||||
onToggleActive: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onSetType: (id: string, type: "SOURCE" | "DESTINATION") => void;
|
||||
onRescan: (id: string) => void;
|
||||
}
|
||||
|
||||
export function getChannelColumns({
|
||||
onToggleActive,
|
||||
onDelete,
|
||||
onSetType,
|
||||
onRescan,
|
||||
}: ChannelColumnsProps): ColumnDef<ChannelRow, unknown>[] {
|
||||
return [
|
||||
{
|
||||
@@ -121,6 +124,14 @@ export function getChannelColumns({
|
||||
Set as Source
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.type === "SOURCE" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onRescan(row.original.id)}
|
||||
>
|
||||
<RefreshCcw className="mr-2 h-3.5 w-3.5" />
|
||||
Rescan Channel
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => onToggleActive(row.original.id)}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteChannel,
|
||||
toggleChannelActive,
|
||||
setChannelType,
|
||||
rescanChannel,
|
||||
} from "../actions";
|
||||
import { DataTable } from "@/components/shared/data-table";
|
||||
import { DeleteDialog } from "@/components/shared/delete-dialog";
|
||||
@@ -22,6 +23,7 @@ interface ChannelsTabProps {
|
||||
export function ChannelsTab({ channels, globalDestination }: ChannelsTabProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [rescanId, setRescanId] = useState<string | null>(null);
|
||||
|
||||
const columns = getChannelColumns({
|
||||
onToggleActive: (id) => {
|
||||
@@ -39,6 +41,7 @@ export function ChannelsTab({ channels, globalDestination }: ChannelsTabProps) {
|
||||
else toast.error(result.error);
|
||||
});
|
||||
},
|
||||
onRescan: (id) => setRescanId(id),
|
||||
});
|
||||
|
||||
const { table } = useDataTable({
|
||||
@@ -60,6 +63,19 @@ export function ChannelsTab({ channels, globalDestination }: ChannelsTabProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRescan = () => {
|
||||
if (!rescanId) return;
|
||||
startTransition(async () => {
|
||||
const result = await rescanChannel(rescanId);
|
||||
if (result.success) {
|
||||
toast.success("Channel scan progress reset — it will be fully rescanned on the next sync");
|
||||
setRescanId(null);
|
||||
} else {
|
||||
toast.error(result.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DestinationCard destination={globalDestination} />
|
||||
@@ -83,6 +99,16 @@ export function ChannelsTab({ channels, globalDestination }: ChannelsTabProps) {
|
||||
onConfirm={handleDelete}
|
||||
isLoading={isPending}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
open={!!rescanId}
|
||||
onOpenChange={(open) => !open && setRescanId(null)}
|
||||
title="Rescan Channel"
|
||||
description="This will reset all scan progress for this channel. On the next sync the worker will re-process every message from the beginning. Packages that are already in the library will be skipped (deduplication by hash), but any missing files will be re-downloaded and re-uploaded. This may take a long time for large channels."
|
||||
confirmLabel="Rescan"
|
||||
onConfirm={handleRescan}
|
||||
isLoading={isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,6 +297,52 @@ export async function triggerChannelSync(): Promise<ActionResult> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all scan progress for a channel so the worker will re-process it
|
||||
* from the very beginning on the next ingestion cycle.
|
||||
*
|
||||
* This clears:
|
||||
* - `lastProcessedMessageId` on every AccountChannelMap linked to this channel
|
||||
* - All TopicProgress records for those maps (for forum channels)
|
||||
*/
|
||||
export async function rescanChannel(channelId: string): Promise<ActionResult> {
|
||||
const admin = await requireAdmin();
|
||||
if (!admin.success) return admin;
|
||||
|
||||
const channel = await prisma.telegramChannel.findUnique({
|
||||
where: { id: channelId },
|
||||
});
|
||||
if (!channel) return { success: false, error: "Channel not found" };
|
||||
|
||||
try {
|
||||
// Find all account-channel maps for this channel
|
||||
const maps = await prisma.accountChannelMap.findMany({
|
||||
where: { channelId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const mapIds = maps.map((m) => m.id);
|
||||
|
||||
// Delete all topic progress records for these maps (forum channels)
|
||||
if (mapIds.length > 0) {
|
||||
await prisma.topicProgress.deleteMany({
|
||||
where: { accountChannelMapId: { in: mapIds } },
|
||||
});
|
||||
}
|
||||
|
||||
// Reset the scan cursor so the worker re-processes from the start
|
||||
await prisma.accountChannelMap.updateMany({
|
||||
where: { channelId },
|
||||
data: { lastProcessedMessageId: null },
|
||||
});
|
||||
|
||||
revalidatePath(REVALIDATE_PATH);
|
||||
return { success: true, data: undefined };
|
||||
} catch {
|
||||
return { success: false, error: "Failed to reset channel scan progress" };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Account-Channel link actions ──
|
||||
|
||||
export async function linkChannel(
|
||||
@@ -377,7 +423,7 @@ export async function triggerIngestion(
|
||||
try {
|
||||
await prisma.$queryRawUnsafe(
|
||||
`SELECT pg_notify('ingestion_trigger', $1)`,
|
||||
accounts.map((a) => a.id).join(",")
|
||||
accounts.map((a: { id: string }) => a.id).join(",")
|
||||
);
|
||||
} catch {
|
||||
// Best-effort
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function TelegramPage() {
|
||||
}),
|
||||
]);
|
||||
|
||||
const serializedHistory = sendHistory.map((r) => ({
|
||||
const serializedHistory = sendHistory.map((r: typeof sendHistory[number]) => ({
|
||||
id: r.id,
|
||||
packageName: r.package.fileName,
|
||||
recipientName: r.telegramLink.telegramName,
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
await prisma.$queryRawUnsafe(
|
||||
`SELECT pg_notify('ingestion_trigger', $1)`,
|
||||
accounts.map((a) => a.id).join(",")
|
||||
accounts.map((a: { id: string }) => a.id).join(",")
|
||||
);
|
||||
} catch {
|
||||
// pg_notify is best-effort — worker will pick up on next scheduled cycle anyway
|
||||
@@ -58,7 +58,7 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
triggered: true,
|
||||
accountIds: accounts.map((a) => a.id),
|
||||
accountIds: accounts.map((a: { id: string }) => a.id),
|
||||
message: `Ingestion triggered for ${accounts.length} account(s)`,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user