Add YouTube transcripts tab to Mission Control
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
|
||||
const GENERAL_CHANNEL = "1476261655955378401";
|
||||
|
||||
function formatAmunBrief(data: any): string {
|
||||
return `👑 AMUN QA REPORT - ${data.date || new Date().toISOString().split('T')[0]}
|
||||
|
||||
🔍 TESTS RUN
|
||||
${(data.tests || []).map((t: string) => `- ${t}`).join('\n')}
|
||||
|
||||
🐛 BUGS FOUND
|
||||
${(data.bugs || []).map((b: string) => `- ${b}`).join('\n')}
|
||||
|
||||
📊 QUALITY SCORE: ${data.quality || 'N/A'}
|
||||
|
||||
💡 RECOMMENDATIONS
|
||||
${(data.recommendations || []).map((r: string) => `- ${r}`).join('\n')}`;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const { data, error } = await supabase
|
||||
.from("amun_sessions")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json(data || []);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const { data: insertData, error } = await supabase
|
||||
.from("amun_sessions")
|
||||
.insert([{
|
||||
date: body.date || new Date().toISOString().split('T')[0],
|
||||
tests: body.tests,
|
||||
bugs: body.bugs,
|
||||
quality: body.quality,
|
||||
recommendations: body.recommendations
|
||||
}]);
|
||||
|
||||
if (error) {
|
||||
console.error("Supabase error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
const discordMessage = formatAmunBrief(body);
|
||||
await fetch(`https://discord.com/api/v10/channels/${GENERAL_CHANNEL}/messages`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ content: discordMessage }),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Amun brief saved and sent to Discord" });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/db";
|
||||
import { morningBriefs, eodBriefs, amunSessions } from "@/db/schema";
|
||||
import { desc } from "drizzle-orm";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const days = parseInt(searchParams.get("days") || "30");
|
||||
|
||||
const [mornings, eods, amuns] = await Promise.all([
|
||||
db.select().from(morningBriefs).orderBy(desc(morningBriefs.date)).limit(days),
|
||||
db.select().from(eodBriefs).orderBy(desc(eodBriefs.date)).limit(days),
|
||||
db.select().from(amunSessions).orderBy(desc(amunSessions.date)).limit(days),
|
||||
]);
|
||||
|
||||
// Parse JSON fields for dashboard consumption
|
||||
const parsedMornings = mornings.map((b) => ({
|
||||
...b,
|
||||
marketData: safeJson(b.marketData, {}),
|
||||
aiNewsHeadlines: safeJson(b.aiNewsHeadlines, []),
|
||||
skillsToLearn: safeJson(b.skillsToLearn, []),
|
||||
sitementeStatus: safeJson(b.sitementeStatus, {}),
|
||||
warmLeads: safeJson(b.warmLeads, []),
|
||||
dayPriorities: safeJson(b.dayPriorities, []),
|
||||
skippedTasks: safeJson(b.skippedTasks, []),
|
||||
}));
|
||||
|
||||
const parsedEods = eods.map((b) => ({
|
||||
...b,
|
||||
marketUpdate: safeJson(b.marketUpdate, {}),
|
||||
completedTasks: safeJson(b.completedTasks, []),
|
||||
tomorrowPriorities: safeJson(b.tomorrowPriorities, []),
|
||||
councilFeedback: safeJson(b.councilFeedback, {}),
|
||||
projectProgress: safeJson(b.projectProgress, []),
|
||||
}));
|
||||
|
||||
const parsedAmuns = amuns.map((s) => ({
|
||||
...s,
|
||||
testsRun: safeJson(s.testsRun, []),
|
||||
bugsFound: safeJson(s.bugsFound, []),
|
||||
recommendations: safeJson(s.recommendations, []),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
mornings: parsedMornings,
|
||||
eods: parsedEods,
|
||||
amuns: parsedAmuns,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("GET /api/dashboard error:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch dashboard data" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
function safeJson<T>(str: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(str) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* POST /api/discord
|
||||
*
|
||||
* Receives a formatted message and forwards it to a Discord webhook.
|
||||
* The DISCORD_WEBHOOK_URL environment variable must be set for messages to be sent.
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* channel: "morning" | "eod" | "amun",
|
||||
* source: "horus" | "amun",
|
||||
* date: "YYYY-MM-DD",
|
||||
* formatted_text: "Markdown-style message string"
|
||||
* }
|
||||
*
|
||||
* Example curl:
|
||||
* curl -X POST http://localhost:3000/api/discord \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{
|
||||
* "channel": "morning",
|
||||
* "source": "horus",
|
||||
* "date": "2026-02-27",
|
||||
* "formatted_text": "☀ **Morning Brief — 27 Feb 2026**\n📍 Benalmádena, 22°C Sunny\n📈 BTC: $68,000 (+2.5%)\n⚡ Priorities: 5 set"
|
||||
* }'
|
||||
*
|
||||
* Note: Add Authorization: Bearer <token> header when auth is implemented.
|
||||
*/
|
||||
|
||||
interface DiscordPayload {
|
||||
channel: "morning" | "eod" | "amun";
|
||||
source: "horus" | "amun";
|
||||
date: string;
|
||||
formatted_text: string;
|
||||
}
|
||||
|
||||
const channelEmoji: Record<string, string> = {
|
||||
morning: "☀",
|
||||
eod: "🌙",
|
||||
amun: "⚙️",
|
||||
};
|
||||
|
||||
const sourceEmoji: Record<string, string> = {
|
||||
horus: "🦅",
|
||||
amun: "⚙️",
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: DiscordPayload = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!body.channel || !body.source || !body.date || !body.formatted_text) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: channel, source, date, formatted_text" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!["morning", "eod", "amun"].includes(body.channel)) {
|
||||
return NextResponse.json(
|
||||
{ error: "channel must be one of: morning, eod, amun" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!["horus", "amun"].includes(body.source)) {
|
||||
return NextResponse.json(
|
||||
{ error: "source must be one of: horus, amun" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
|
||||
|
||||
if (!webhookUrl) {
|
||||
// Webhook not configured — log and return success (non-blocking)
|
||||
console.log(
|
||||
`[Discord] Webhook not configured. Would have sent to #${body.channel}:`,
|
||||
body.formatted_text.slice(0, 100)
|
||||
);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
sent: false,
|
||||
reason: "DISCORD_WEBHOOK_URL not configured",
|
||||
});
|
||||
}
|
||||
|
||||
// Build Discord embed
|
||||
const embed = {
|
||||
title: `${channelEmoji[body.channel]} ${body.channel.charAt(0).toUpperCase() + body.channel.slice(1)} Brief — ${body.date}`,
|
||||
description: body.formatted_text,
|
||||
color:
|
||||
body.channel === "morning"
|
||||
? 0xf59e0b // amber
|
||||
: body.channel === "eod"
|
||||
? 0x3b82f6 // blue
|
||||
: 0x8b5cf6, // purple
|
||||
footer: {
|
||||
text: `${sourceEmoji[body.source]} Sent by ${body.source.charAt(0).toUpperCase() + body.source.slice(1)}`,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const discordRes = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: "Horus & Amun",
|
||||
avatar_url: "https://cdn.discordapp.com/embed/avatars/0.png",
|
||||
embeds: [embed],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!discordRes.ok) {
|
||||
const errText = await discordRes.text();
|
||||
console.error("[Discord] Webhook failed:", discordRes.status, errText);
|
||||
return NextResponse.json(
|
||||
{ error: "Discord webhook failed", status: discordRes.status },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, sent: true });
|
||||
} catch (error) {
|
||||
console.error("POST /api/discord error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
|
||||
const EOD_CHANNEL = "1476344633406656785";
|
||||
|
||||
function formatEODBrief(data: any): string {
|
||||
return `🌙 END OF DAY - ${data.date || new Date().toISOString().split('T')[0]}
|
||||
|
||||
✅ COMPLETED TODAY
|
||||
${(data.completed || []).map((c: string) => `- ${c}`).join('\n')}
|
||||
|
||||
📊 PROGRESS
|
||||
${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n')}
|
||||
|
||||
🧠 COUNCIL
|
||||
${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
|
||||
|
||||
🎯 TOMORROW
|
||||
${(data.tomorrow || []).map((t: string) => `- ${t}`).join('\n')}`;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const { data, error } = await supabase
|
||||
.from("eod_briefs")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json(data || []);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const { data: insertData, error } = await supabase
|
||||
.from("eod_briefs")
|
||||
.insert([{
|
||||
date: body.date || new Date().toISOString().split('T')[0],
|
||||
completed: body.completed,
|
||||
progress: body.progress,
|
||||
council: body.council,
|
||||
tomorrow: body.tomorrow
|
||||
}]);
|
||||
|
||||
if (error) {
|
||||
console.error("Supabase error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
const discordMessage = formatEODBrief(body);
|
||||
await fetch(`https://discord.com/api/v10/channels/${EOD_CHANNEL}/messages`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ content: discordMessage }),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "EOD brief saved and sent to Discord" });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
|
||||
const MORNING_CHANNEL = "1476344610493042698";
|
||||
|
||||
function formatMorningBrief(data: any): string {
|
||||
return `☀️ MORNING BRIEF - ${data.date || new Date().toISOString().split('T')[0]}
|
||||
|
||||
🌤️ WEATHER
|
||||
${data.weather || 'N/A'}
|
||||
|
||||
📊 MARKET
|
||||
BTC: $${data.market?.BTC || 'N/A'} (${data.market?.btcChange || '0%'})
|
||||
ETH: $${data.market?.ETH || 'N/A'} (${data.market?.ethChange || '0%'})
|
||||
SOL: $${data.market?.SOL || 'N/A'} (${data.market?.solChange || '0%'})
|
||||
|
||||
🎯 PRIORITIES
|
||||
${(data.priorities || []).map((p: string, i: number) => `${i + 1}. ${p}`).join('\n')}
|
||||
|
||||
💰 GOAL: ${data.goal || 'N/A'}
|
||||
|
||||
🔥 HOT LEADS
|
||||
${(data.leads || []).map((l: string) => `- ${l}`).join('\n')}`;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const { data, error } = await supabase
|
||||
.from("morning_briefs")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json(data || []);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Save to Supabase
|
||||
const { data, error } = await supabase
|
||||
.from("morning_briefs")
|
||||
.insert([{
|
||||
date: body.date || new Date().toISOString().split('T')[0],
|
||||
weather: body.weather,
|
||||
market: body.market,
|
||||
priorities: body.priorities,
|
||||
goal: body.goal,
|
||||
leads: body.leads
|
||||
}]);
|
||||
|
||||
if (error) {
|
||||
console.error("Supabase error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
// Send to Discord
|
||||
const discordMessage = formatMorningBrief(body);
|
||||
await fetch(`https://discord.com/api/v10/channels/${MORNING_CHANNEL}/messages`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ content: discordMessage }),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: "Morning brief saved and sent to Discord" });
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
return NextResponse.json({ error: "Failed to save brief" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json";
|
||||
|
||||
function getTranscripts() {
|
||||
if (fs.existsSync(STORAGE_FILE)) {
|
||||
return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8"));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveTranscript(data) {
|
||||
const transcripts = getTranscripts();
|
||||
// Check if already exists
|
||||
const existing = transcripts.findIndex(t => t.videoId === data.videoId);
|
||||
if (existing >= 0) {
|
||||
transcripts[existing] = data;
|
||||
} else {
|
||||
transcripts.unshift(data);
|
||||
}
|
||||
fs.writeFileSync(STORAGE_FILE, JSON.stringify(transcripts, null, 2));
|
||||
return transcripts;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const videoId = searchParams.get("videoId");
|
||||
|
||||
const transcripts = getTranscripts();
|
||||
|
||||
if (videoId) {
|
||||
const found = transcripts.find(t => t.videoId === videoId);
|
||||
return NextResponse.json(found || { error: "Not found" });
|
||||
}
|
||||
|
||||
return NextResponse.json(transcripts);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { action, videoUrl, videoId, transcript, title, categories } = body;
|
||||
|
||||
// Extract video ID if not provided
|
||||
let vid = videoId;
|
||||
if (!vid && videoUrl) {
|
||||
const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/);
|
||||
vid = match ? match[1] : videoUrl;
|
||||
}
|
||||
|
||||
if (action === "fetch") {
|
||||
// Run Python script to get transcript
|
||||
return new Promise((resolve) => {
|
||||
const scriptPath = "/root/.openclaw/workspace/scripts/youtube_transcript.py";
|
||||
const outputFile = `/tmp/transcript_${vid}.txt`;
|
||||
|
||||
const proc = spawn("python3", [scriptPath, vid], {
|
||||
cwd: "/root/.openclaw/workspace/scripts"
|
||||
});
|
||||
|
||||
let output = "";
|
||||
let error = "";
|
||||
|
||||
proc.stdout.on("data", (data) => { output += data.toString(); });
|
||||
proc.stderr.on("data", (data) => { error += data.toString(); });
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (fs.existsSync(outputFile)) {
|
||||
const transcriptText = fs.readFileSync(outputFile, "utf-8");
|
||||
resolve(NextResponse.json({
|
||||
success: true,
|
||||
videoId: vid,
|
||||
transcript: transcriptText
|
||||
}));
|
||||
} else {
|
||||
resolve(NextResponse.json({
|
||||
success: false,
|
||||
error: error || "Could not fetch transcript"
|
||||
}, { status: 500 }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (action === "save") {
|
||||
// Save transcript with metadata
|
||||
const data = {
|
||||
videoId: vid,
|
||||
videoUrl: videoUrl || `https://www.youtube.com/watch?v=${vid}`,
|
||||
title: title || `Video ${vid}`,
|
||||
transcript: transcript || "",
|
||||
categories: categories || [],
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updated = saveTranscript(data);
|
||||
return NextResponse.json({ success: true, transcripts: updated });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user