feat: Social page — aggregated post queues + X/video history (#345)

* feat(api): x/video post history endpoints

Approved or rejected drafts vanished from both queues permanently --
the listers exclude terminal statuses and no history surface existed,
so a posted tweet or video was only findable in the raw task list.
GET /x/posts/history and GET /video/posts/history (CEO-gated, bounded)
return acted-on drafts newest-first with the posted platform ids and
reject reasons from the draft markers. Route tests assert by identity,
not emptiness: approve/reject commits the whole session, so prior
tests' rows legitimately persist in the shared test DB.

* feat(panel): Social page aggregating post queues and history

New dashboard page composing the X and video post queues with one
unified history section beneath them -- both platforms interleaved
newest-first, kind and outcome badges, posted X ids linking to the
live tweet, reject reasons shown. The command center's two full queue
cards become a compact pending-counts card linking to the page, so the
queues have one home instead of duplicated surfaces.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-09 00:44:51 +02:00
committed by GitHub
co-authored by Renn F
parent 5886336259
commit f0b6390189
26 changed files with 1289 additions and 30 deletions
+7 -1
View File
@@ -28,15 +28,21 @@ export type {
XPost,
XMentionRef,
XPostExecuteResult,
XPostHistoryEntry,
XCredentialsStatus,
} from "./x";
export { roadmapApi } from "./roadmap";
export type { RoadmapCycle, RoadmapItem, RoadmapItemActionResult } from "./roadmap";
export type {
RoadmapCycle,
RoadmapItem,
RoadmapItemActionResult,
} from "./roadmap";
export { videoApi, videoMediaUrl } from "./video";
export type {
VideoCut,
VideoPost,
VideoPostExecuteResult,
VideoPostHistoryEntry,
VideoRequestResult,
TikTokCredentialsStatus,
} from "./video";
+26 -4
View File
@@ -30,6 +30,22 @@ export interface VideoPostExecuteResult {
detail: string;
}
// One acted-on draft (posted or rejected) — the CEO's history view.
export interface VideoPostHistoryEntry {
task_id: string;
source: string; // "video_post"
title: string;
status: string; // "completed" | "cancelled"
occasion: string;
script: string;
platforms: string[];
x_caption?: string | null;
tiktok_caption?: string | null;
reject_reason?: string | null;
posted: Record<string, string>; // platform -> posted id
acted_at: string;
}
export interface VideoRequestResult {
status: string; // "opened" | "disabled" | "not_opened"
task_id?: string | null;
@@ -56,6 +72,15 @@ export const videoApi = {
const { data } = await api.get<VideoPost[]>("/video/posts");
return data;
},
// Posted or rejected drafts, newest-acted-first. Fixed default limit (50);
// no "load more" — pass a higher limit if the panel ever needs it.
listHistory: async (limit = 50): Promise<VideoPostHistoryEntry[]> => {
const { data } = await api.get<VideoPostHistoryEntry[]>(
"/video/posts/history",
{ params: { limit } },
);
return data;
},
// Fetches the rendered cut as a Blob via axios (carrying the auth headers
// a plain <video src> GET can't) so the caller can drive <video> off an
// object URL instead of pointing it at the route directly.
@@ -88,10 +113,7 @@ export const videoApi = {
brief: string;
platforms: string[];
}): Promise<VideoRequestResult> => {
const { data } = await api.post<VideoRequestResult>(
"/video/request",
body,
);
const { data } = await api.post<VideoRequestResult>("/video/request", body);
return data;
},
getCredentialsStatus: async (): Promise<TikTokCredentialsStatus> => {
+24
View File
@@ -37,6 +37,22 @@ export interface XPostExecuteResult {
detail: string;
}
// One acted-on draft (posted or rejected) — the CEO's history view.
export interface XPostHistoryEntry {
task_id: string;
source: "x_post" | "x_reply" | "x_feature";
title: string;
status: string; // "completed" | "cancelled"
body: string;
char_count: number;
release_version?: string | null;
mention?: XMentionRef | null;
feature?: XFeatureRef | null;
tweet_id?: string | null;
reject_reason?: string | null;
acted_at: string;
}
export interface XCredentialsStatus {
has_credentials: boolean;
}
@@ -46,6 +62,14 @@ export const xApi = {
const { data } = await api.get<XPost[]>("/x/posts");
return data;
},
// Posted or rejected drafts, newest-acted-first. Fixed default limit (50);
// no "load more" — pass a higher limit if the panel ever needs it.
listHistory: async (limit = 50): Promise<XPostHistoryEntry[]> => {
const { data } = await api.get<XPostHistoryEntry[]>("/x/posts/history", {
params: { limit },
});
return data;
},
approve: async (
taskId: string,
editedBody?: string,