feat: complete marketing command center with pipeline, UI, and asset generation
- Dashboard with campaign management, asset gallery, and publishing queue - 7-agent pipeline: trend scout, research, scripts, ad creative, video, copy, distribution - Campaign form with screenshot upload, goal picker, platform selection - Campaign detail view with Details/Pipeline/Assets/Chat tabs - Two-set image generation: Gemini AI (NanoBanana MCP) + Canvas Design posters - Remotion video rendering with phone.png frame and real screenshot alignment - honeyDue branding: blue #0079FF, orange #FF9400, Inter font, warm off-white - Asset cards with source badges (Gemini/Canvas/Remotion/Playwright) - Markdown/JSON render endpoint for viewing pipeline outputs as HTML - Settings page with Tavily, Gemini, Postiz, Nextdoor integration management - Claude Chat for campaign feedback loop with streaming SSE - Postiz publishing modal with scheduling - Auth with NextAuth credentials + JWT sessions - SQLite via Prisma with better-sqlite3 adapter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
// Settings keys and their env var fallbacks
|
||||
const SETTINGS_KEYS = {
|
||||
// Postiz
|
||||
POSTIZ_URL: { envVar: "POSTIZ_URL", label: "Postiz URL", placeholder: "http://localhost:5000" },
|
||||
POSTIZ_API_KEY: { envVar: "POSTIZ_API_KEY", label: "Postiz API Key", placeholder: "your-postiz-api-key", secret: true },
|
||||
|
||||
// Tavily (Research)
|
||||
TAVILY_API_KEY: { envVar: "TAVILY_API_KEY", label: "Tavily API Key", placeholder: "tvly-...", secret: true },
|
||||
|
||||
// Google Gemini (NanoBanana image generation)
|
||||
GEMINI_API_KEY: { envVar: "GEMINI_API_KEY", label: "Google Gemini API Key", placeholder: "AIza...", secret: true },
|
||||
|
||||
// Nextdoor
|
||||
NEXTDOOR_API_TOKEN: { envVar: "NEXTDOOR_API_TOKEN", label: "Nextdoor API Token", placeholder: "your-nextdoor-token", secret: true },
|
||||
NEXTDOOR_ADVERTISER_ID: { envVar: "NEXTDOOR_ADVERTISER_ID", label: "Nextdoor Advertiser ID", placeholder: "your-advertiser-id" },
|
||||
} as const;
|
||||
|
||||
export type SettingKey = keyof typeof SETTINGS_KEYS;
|
||||
|
||||
export const SETTINGS_CONFIG = SETTINGS_KEYS;
|
||||
|
||||
// Grouped for UI
|
||||
export const SETTINGS_GROUPS = [
|
||||
{
|
||||
name: "Postiz",
|
||||
description: "Self-hosted social media scheduling. Handles Instagram and TikTok publishing.",
|
||||
docsUrl: "https://postiz.com",
|
||||
keys: ["POSTIZ_URL", "POSTIZ_API_KEY"] as SettingKey[],
|
||||
},
|
||||
{
|
||||
name: "Tavily",
|
||||
description: "AI-powered web research. Used by the Trend Scout and Research agents. Free tier: 1,000 searches/month.",
|
||||
docsUrl: "https://tavily.com",
|
||||
keys: ["TAVILY_API_KEY"] as SettingKey[],
|
||||
},
|
||||
{
|
||||
name: "Gemini",
|
||||
description: "Google Gemini powers NanoBanana MCP for AI image generation in static ads. ~$0.04-0.13/image.",
|
||||
docsUrl: "https://aistudio.google.com/apikey",
|
||||
keys: ["GEMINI_API_KEY"] as SettingKey[],
|
||||
},
|
||||
{
|
||||
name: "Nextdoor",
|
||||
description: "Direct Nextdoor Ads API integration for local advertising.",
|
||||
docsUrl: "https://developer.nextdoor.com",
|
||||
keys: ["NEXTDOOR_API_TOKEN", "NEXTDOOR_ADVERTISER_ID"] as SettingKey[],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a setting value. Checks DB first, falls back to env var.
|
||||
*/
|
||||
export async function getSetting(key: SettingKey): Promise<string> {
|
||||
try {
|
||||
const setting = await prisma.setting.findUnique({ where: { key } });
|
||||
if (setting?.value) return setting.value;
|
||||
} catch {
|
||||
// DB not available, fall through to env
|
||||
}
|
||||
|
||||
const config = SETTINGS_KEYS[key];
|
||||
return process.env[config.envVar] || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings as a map.
|
||||
*/
|
||||
export async function getAllSettings(): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const [key, config] of Object.entries(SETTINGS_KEYS)) {
|
||||
try {
|
||||
const setting = await prisma.setting.findUnique({ where: { key } });
|
||||
result[key] = setting?.value || process.env[config.envVar] || "";
|
||||
} catch {
|
||||
result[key] = process.env[config.envVar] || "";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a setting to the database.
|
||||
*/
|
||||
export async function saveSetting(key: SettingKey, value: string) {
|
||||
await prisma.setting.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connectivity status for each integration.
|
||||
*/
|
||||
export async function checkIntegrationStatus(): Promise<Record<string, { connected: boolean; error?: string }>> {
|
||||
const settings = await getAllSettings();
|
||||
const status: Record<string, { connected: boolean; error?: string }> = {};
|
||||
|
||||
// Postiz
|
||||
if (settings.POSTIZ_URL && settings.POSTIZ_API_KEY) {
|
||||
try {
|
||||
const res = await fetch(`${settings.POSTIZ_URL}/public/v1/integrations`, {
|
||||
headers: { Authorization: `Bearer ${settings.POSTIZ_API_KEY}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
status.postiz = { connected: res.ok };
|
||||
if (!res.ok) status.postiz.error = `HTTP ${res.status}`;
|
||||
} catch (e) {
|
||||
status.postiz = { connected: false, error: e instanceof Error ? e.message : "Connection failed" };
|
||||
}
|
||||
} else {
|
||||
status.postiz = { connected: false, error: "Not configured" };
|
||||
}
|
||||
|
||||
// Tavily
|
||||
if (settings.TAVILY_API_KEY) {
|
||||
status.tavily = { connected: true }; // No ping endpoint, just check if key exists
|
||||
} else {
|
||||
status.tavily = { connected: false, error: "Not configured" };
|
||||
}
|
||||
|
||||
// Gemini
|
||||
if (settings.GEMINI_API_KEY) {
|
||||
status.gemini = { connected: true };
|
||||
} else {
|
||||
status.gemini = { connected: false, error: "Not configured" };
|
||||
}
|
||||
|
||||
// Nextdoor
|
||||
if (settings.NEXTDOOR_API_TOKEN && settings.NEXTDOOR_ADVERTISER_ID) {
|
||||
status.nextdoor = { connected: true };
|
||||
} else {
|
||||
status.nextdoor = { connected: false, error: "Not configured" };
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
Reference in New Issue
Block a user