evOo-local/apps/web/src/lib/heute-brief.ts
cynarAI 83302961ba feat(web): replace Heute iframe with native daily brief (#72)
Replace Mission Control iframe on the Heute pillar with a native React
brief that surfaces actionable runs, unread replies, and today’s threads.
Mission Control remains available via a small footer link for power users.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 12:21:05 +02:00

122 lines
3.4 KiB
TypeScript

import type { Bot, Group, RunActivityRow } from "@rakazo/contracts";
export type JetztRunItem = {
kind: "run";
run: RunActivityRow;
};
export type JetztUnreadItem = {
kind: "unread";
chatKind: "bot" | "group";
chat: Bot | Group;
};
export type JetztItem = JetztRunItem | JetztUnreadItem;
export type WeiterItem = {
chatKind: "bot" | "group";
chat: Bot | Group;
};
export type HeuteBriefData = {
jetzt: JetztItem[];
weiter: WeiterItem[];
actionCount: number;
};
export function isToday(iso: string, now = new Date()): boolean {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return false;
return (
date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear()
);
}
export function cleanBriefPreview(raw?: string | null): string {
if (!raw) return "";
let text = raw;
text = text.replace(/^#{1,6}\s+/gm, "");
text = text.replace(/\[(?:to|RECOVERY PROTOCOL|PO)[^\]]*\]/gi, "");
text = text.replace(/[*_`#]/g, "");
text = text.replace(/\\/g, "");
return text.replace(/\s+/g, " ").trim();
}
function runTargetKey(run: RunActivityRow): string {
return run.groupId ? `group:${run.groupId}` : `bot:${run.botId}`;
}
function chatTargetKey(chatKind: "bot" | "group", chatId: string): string {
return chatKind === "group" ? `group:${chatId}` : `bot:${chatId}`;
}
function isActionableRun(run: RunActivityRow): boolean {
return run.status === "waiting_input" || run.status === "waiting_takeover";
}
function isFailedRoutine(run: RunActivityRow): boolean {
return run.trigger === "routine" && run.status === "failed";
}
export function buildHeuteBrief(params: {
bots: Bot[];
groups: Group[];
activeRuns: RunActivityRow[];
recentRuns: RunActivityRow[];
now?: Date;
}): HeuteBriefData {
const { bots, groups, activeRuns, recentRuns, now = new Date() } = params;
const jetzt: JetztItem[] = [];
const coveredTargets = new Set<string>();
const actionableRuns = [
...activeRuns.filter(isActionableRun),
...recentRuns.filter(isFailedRoutine),
];
const seenRunIds = new Set<string>();
for (const run of actionableRuns) {
if (seenRunIds.has(run.runId)) continue;
seenRunIds.add(run.runId);
jetzt.push({ kind: "run", run });
coveredTargets.add(runTargetKey(run));
}
const unreadChats: JetztUnreadItem[] = [
...bots.filter((bot) => bot.unread).map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })),
...groups.filter((group) => group.unread).map((group) => ({
kind: "unread" as const,
chatKind: "group" as const,
chat: group,
})),
];
for (const item of unreadChats) {
const key = chatTargetKey(item.chatKind, item.chat.id);
if (coveredTargets.has(key)) continue;
jetzt.push(item);
coveredTargets.add(key);
}
const weiterCandidates: WeiterItem[] = [
...bots
.filter((bot) => isToday(bot.updatedAt, now))
.map((bot) => ({ chatKind: "bot" as const, chat: bot })),
...groups
.filter((group) => isToday(group.updatedAt, now))
.map((group) => ({ chatKind: "group" as const, chat: group })),
];
weiterCandidates.sort(
(a, b) => new Date(b.chat.updatedAt).getTime() - new Date(a.chat.updatedAt).getTime(),
);
const weiter = weiterCandidates
.filter((item) => !coveredTargets.has(chatTargetKey(item.chatKind, item.chat.id)))
.slice(0, 5);
return {
jetzt,
weiter,
actionCount: jetzt.length,
};
}