"use client"; // The social (posts) channel view: a minimal two-stage rail — Fetch → Index — // instead of the six video stages and their 18 pathology buckets, none of which // exist for an account that has no downloads, no audio and no transcripts. // Its only failure surfaces are "the fetch failed" and "we need credentials", // which is exactly what the state sidecar records. import { useState, useTransition } from "react"; import { checkPostAvailabilityAction, connectForumSessionAction, fetchPostsAction, importForumPagesAction, setPostFetcherAction, } from "../socialActions"; import type { ForumSessionStatus } from "yt-dlp-transcript-common/social/forumSession"; export type SocialChannelState = { postCount: number; shards: string[]; lastFetchedAt?: string; lastFetchedCount?: number; lastError?: string; needsCookies?: boolean; fetcherLabel: string; handle: string; accountUrl?: string; // Which fetcher currently drives this channel, and every fetcher that could. fetcherId: string; availableFetchers: { id: string; label: string }[]; // Deleted-post sweep state (the posts analogue of video availability). deletedCount: number; checkedCount: number; canCheckAvailability: boolean; // The older-posts backfill (a fetcher's `fetchOlder`): whether this fetcher // has one, and where it stands, as one line. canFetchOlder: boolean; olderStatus: string; // The channel's platform; "xenforo" is a forum thread. platform?: string; // A forum thread's browser session (its host's profile), when it is one. forumSession?: ForumSessionStatus; }; export function SocialChannelPanel({ slug, state, }: { slug: string; state: SocialChannelState; }) { const [pending, startTransition] = useTransition(); const [fetcherId, setFetcherId] = useState(state.fetcherId); const [switchNote, setSwitchNote] = useState(null); const [switchErr, setSwitchErr] = useState(null); const switchFetcher = (next: string) => startTransition(async () => { setSwitchNote(null); setSwitchErr(null); const res = await setPostFetcherAction(slug, next); if (res.ok) { setFetcherId(next); setSwitchNote("Fetcher updated — the next fetch uses it."); } else { setSwitchErr(res.error); } }); const checkDeleted = () => startTransition(async () => { const res = await checkPostAvailabilityAction(slug); if (res.ok) void res.stream.cancel(); }); const forum = state.platform === "xenforo"; // A forum thread is read page by page, newest first: the cap on pages one // run reads (blank = walk back until already-archived posts, or page 1). const [pages, setPages] = useState(""); const [forumNote, setForumNote] = useState(null); const [forumErr, setForumErr] = useState(null); const [importPath, setImportPath] = useState(""); const connect = () => startTransition(async () => { setForumNote(null); setForumErr(null); const res = await connectForumSessionAction(slug); if (res.ok) { setForumNote( res.record.cleared ? "Connected: the thread showed before the window closed. Fetch again." : "The window closed before a thread page showed; the profile kept what it had.", ); } else { setForumErr(res.error); } }); const importPages = () => startTransition(async () => { setForumNote(null); setForumErr(null); const res = await importForumPagesAction(slug, importPath); if (res.ok) { void res.stream.cancel(); setForumNote("Import queued — its log is on the jobs page."); } else { setForumErr(res.error); } }); const run = (full: boolean, older = false) => startTransition(async () => { const n = Number(pages); const res = await fetchPostsAction( slug, undefined, full, undefined, older || undefined, undefined, undefined, forum && pages.trim() && Number.isInteger(n) && n > 0 ? n : undefined, ); // The job streams to the jobs page; nothing to consume here, but the // stream must be released or its buffered chunks leak. if (res.ok) void res.stream.cancel(); }); return (

Posts

{forum ? state.handle : `@${state.handle}`} · {state.fetcherLabel}
{state.canFetchOlder && ( )}
{state.availableFetchers.length > 1 && ( )} {switchNote && (

{switchNote}

)} {switchErr && (

{switchErr}

)}
{forum && ( )} {state.canFetchOlder && ( )}
{/* The two failure surfaces a social channel actually has. */} {state.needsCookies && (

{forum ? "Needs the forum session: the forum showed a check (or a login) the headless browser could not get past. Use Connect forum session below, clear it in the window, close it, and fetch again." : "Needs credentials: the fetcher could not authenticate. Configure cookies-from-browser for this channel (or globally) and re-run."}

)} {state.lastError && (

Last fetch failed: {state.lastError}

)} {state.accountUrl && (

Source:{" "} {state.accountUrl}

)}
{forum && (

Forum session

{state.forumSession?.lastConnect ? `${state.forumSession.host}: connected ${new Date(state.forumSession.lastConnect.connectedAt).toLocaleString()}` + (state.forumSession.lastConnect.cleared ? "" : " (the thread did not show)") : state.forumSession?.hasProfile ? `${state.forumSession.host}: profile in use, never connected by hand` : `${state.forumSession?.host ?? "this forum"}: no profile yet`}

The fetcher reads the thread in a headless browser that keeps one profile per forum host, so a browser check it clears once (Kiwi Farms' KiwiFlare, say) stays cleared. When a check will not clear by itself, or the thread needs a login, Connect opens that profile in a window on the machine running the editor, at the thread: clear it (or log in), wait for the thread to show, then close the window.

{forumNote && (

{forumNote}

)} {forumErr && (

{forumErr}

)}
)}

Posts are indexed into the search corpus by the normal build — there is no per-post download or transcription stage.

); } function Stat({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }