commit fb18dd2ded1ba3fa5de3e44dd5c18e58692d687a
parent e5994b34c7951a2f33c3f808b18326034caf767e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 13:01:44 -0400
channelWork: the server shell draws the shared channel table
ChannelWorkTable keeps the section, heading, description and `<id> empty`
paragraph, and hands its rows to the shared ChannelsTable with WORK_COLUMNS
(`extra` dropped when the section has none), `rowLabelPrefix={id}` and
`labels={{count, extra, report: "Last report"}}`. The section's functions
stay on the server: each row's count, extra figure and actions (the primary
action + refresh report) cross as a number, a string and an element. The
private `Row` is deleted, and with it `isStaleOrMissing` (its last caller);
the report cell is the rack's (`report age for`, stale / missing), dates are
`formatStamp`. Never `role="status"`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 69 insertions(+), 112 deletions(-)
diff --git a/editor/app/components/channelWork/ChannelWorkTable.tsx b/editor/app/components/channelWork/ChannelWorkTable.tsx
@@ -1,22 +1,35 @@
-import Link from "next/link";
-import {
- isStaleOrMissing,
- type ActionableRow,
- type ActionableSummary,
+import type {
+ ActionableRow,
+ ActionableSummary,
} from "../../lib/actionable/loadActionable";
+import {
+ buildChannelRowView,
+ neutralChannelPriority,
+ type ChannelRowView,
+} from "yt-dlp-transcript-common/views/channelRow";
import { InlineActionButton } from "../actions/InlineActionButton";
+import { ChannelsTable } from "../../channels/components/ChannelsTable";
+import {
+ WORK_COLUMNS,
+ type ChannelRowExtras,
+} from "../../channels/components/channelColumns";
import type { SectionConfig } from "./sections";
// A server component on purpose: `SectionConfig.primaryAction` is a function
// that returns an element, and a function cannot cross the server/client
-// boundary as a prop.
+// boundary as a prop. So this is the SHELL — section, heading, description,
+// empty paragraph — and the rows are the shared channel table
+// (channels/components/ChannelsTable.tsx) with the WORK_COLUMNS preset, fed
+// each row's count, extra figure and actions as data and elements.
//
// THE ARIA CONTRACT BELOW IS LOAD-BEARING. `channel-work.spec`,
// `cleanup-actionable.spec`, `incomplete-transcript.spec` and
// `download-format-guard.spec` all locate by these exact strings:
// <section aria-label="<id>">, <h2>{title}</h2>,
// <p aria-label="<id> empty">, <tr aria-label="<id> row <slug>">,
-// columns Slug / countLabel / [extra] / Last report / Last sync / Actions,
+// columns Slug / countLabel / [extra] / Last report / Last sync / Actions
+// (the report cell is the rack's: `report age for <slug>`, "stale" /
+// "missing" in the warning colour),
// and a `refresh report <slug>` InlineActionButton on every row.
//
// NEVER add `role="status"` in here. This renders on the RUNNER operation
@@ -51,6 +64,27 @@ function Section({
config: SectionConfig;
rows: ActionableRow[];
}) {
+ // The functions stay HERE, on the server: each row's count, extra figure and
+ // actions are computed now and cross to the shared (client) table as a
+ // number, a string and an element.
+ const rowExtras: Record<string, ChannelRowExtras> = {};
+ for (const row of rows) {
+ rowExtras[row.channel.slug] = {
+ count: config.getCount(row),
+ extra: config.extraColumn?.getValue(row),
+ actions: (
+ <span className="inline-flex items-center gap-3 flex-wrap">
+ {config.primaryAction(row)}
+ <InlineActionButton
+ variant={{ kind: "refreshReport", slug: row.channel.slug }}
+ />
+ </span>
+ ),
+ };
+ }
+ const columns = config.extraColumn
+ ? WORK_COLUMNS
+ : WORK_COLUMNS.filter((id) => id !== "extra");
return (
<section
aria-label={config.id}
@@ -69,110 +103,37 @@ function Section({
{config.emptyLabel}
</p>
) : (
- <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible">
- <table className="text-sm border-y md:border md:border-border border-border md:rounded-md md:overflow-hidden w-full">
- <thead className="bg-muted">
- <tr>
- <th className="text-left font-medium px-3 py-2">Slug</th>
- <th className="text-right font-medium px-3 py-2">
- {config.countLabel}
- </th>
- {config.extraColumn && (
- <th className="text-right font-medium px-3 py-2 whitespace-nowrap">
- {config.extraColumn.label}
- </th>
- )}
- <th className="text-left font-medium px-3 py-2 whitespace-nowrap">
- Last report
- </th>
- <th className="text-left font-medium px-3 py-2 whitespace-nowrap">
- Last sync
- </th>
- <th className="text-right font-medium px-3 py-2">Actions</th>
- </tr>
- </thead>
- <tbody>
- {rows.map((row) => (
- <Row
- key={row.channel.slug}
- row={row}
- count={config.getCount(row)}
- extraValue={config.extraColumn?.getValue(row) ?? null}
- primaryAction={config.primaryAction(row)}
- sectionId={config.id}
- />
- ))}
- </tbody>
- </table>
- </div>
+ <ChannelsTable
+ rows={rows.map(workRowOf)}
+ columns={columns}
+ rowLabelPrefix={config.id}
+ rowExtras={rowExtras}
+ labels={{
+ count: config.countLabel,
+ extra: config.extraColumn?.label,
+ report: "Last report",
+ }}
+ sort={false}
+ selection={false}
+ />
)}
</section>
);
}
-function Row({
- row,
- count,
- extraValue,
- primaryAction,
- sectionId,
-}: {
- row: ActionableRow;
- count: number;
- extraValue: string | null;
- primaryAction: React.ReactNode;
- sectionId: string;
-}) {
- const { channel, snapshot } = row;
- const lastSync = channel.config.lastSyncedAt;
- const lastReport = snapshot?.generatedAt ?? null;
- // The loader's own definition, which this used to carry a copy of.
- const isStale = isStaleOrMissing(row);
- return (
- <tr
- aria-label={`${sectionId} row ${channel.slug}`}
- className="border-t border-border"
- >
- <td className="px-3 py-2 font-mono">
- <Link
- href={`/channels/${channel.slug}`}
- className="underline hover:text-foreground"
- >
- {channel.slug}
- </Link>
- </td>
- <td className="px-3 py-2 text-right tabular-nums">{count}</td>
- {extraValue !== null && (
- <td className="px-3 py-2 text-right tabular-nums whitespace-nowrap text-muted-foreground">
- {extraValue}
- </td>
- )}
- <td
- className={`px-3 py-2 text-xs whitespace-nowrap ${
- isStale
- ? "text-warning"
- : "text-muted-foreground"
- }`}
- >
- {lastReport ? (
- <time dateTime={lastReport}>
- {new Date(lastReport).toLocaleString()}
- </time>
- ) : (
- "never"
- )}
- </td>
- <td className="px-3 py-2 text-xs text-muted-foreground whitespace-nowrap">
- {lastSync ? new Date(lastSync).toLocaleString() : "never"}
- </td>
- <td className="px-3 py-2 text-right">
- <span className="inline-flex items-center justify-end gap-3 flex-wrap">
- {primaryAction}
- <InlineActionButton
- variant={{ kind: "refreshReport", slug: channel.slug }}
- />
- </span>
- </td>
- </tr>
- );
+// The shared row, for a table that draws no bands, no tier, no media badge and
+// no Location column: those inputs are neutral rather than read. The report
+// state is the builder's `reportStateOf` — the loader's own definition, which
+// this table used to carry a copy of.
+function workRowOf(row: ActionableRow): ChannelRowView {
+ return buildChannelRowView({
+ slug: row.channel.slug,
+ config: row.channel.config,
+ snapshot: row.snapshot,
+ playlistCount: null,
+ bands: [],
+ priority: neutralChannelPriority(),
+ media: null,
+ volume: { id: "", label: "" },
+ });
}
diff --git a/editor/app/lib/actionable/loadActionable.ts b/editor/app/lib/actionable/loadActionable.ts
@@ -44,10 +44,6 @@ export type ActionableSummary = {
// builder that reads it; re-exported so every importer keeps its path.
export { reportStateOf };
-export function isStaleOrMissing(row: ActionableRow): boolean {
- return reportStateOf(row.channel) !== "current";
-}
-
// THE COUNT HELPERS, as wrappers. The counting itself is
// common/views/actionableCounts.ts — pure functions over one snapshot — which
// the channel-row builder calls too, so a dashboard cell and this census cannot