66c2bbec8b
- 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>
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
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;
|
|
}
|