833 lines
28 KiB
Markdown
833 lines
28 KiB
Markdown
# Marketing Command Center — Frontend Architecture
|
|
|
|
**What you want:** A self-hosted web dashboard where you log in, see everything your agents produced, cherry-pick ads to push to Postiz, and drop into a Claude session to give feedback — all from the browser. Everything runs in Docker. No external services except Postiz and Claude API.
|
|
|
|
---
|
|
|
|
## System Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ YOUR BROWSER │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────┐ ┌───────────────────┐ │
|
|
│ │ Campaign │ │ Asset │ │ Claude Chat │ │
|
|
│ │ Dashboard │ │ Gallery │ │ (feedback/edits) │ │
|
|
│ └──────┬───────┘ └────┬─────┘ └────────┬──────────┘ │
|
|
└─────────┼───────────────┼─────────────────┼──────────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ NEXT.JS APP (Docker Container) │
|
|
│ │
|
|
│ API Routes: │
|
|
│ /api/campaigns /api/assets /api/claude │
|
|
│ /api/publish /api/agents /api/auth │
|
|
│ │
|
|
│ ┌──────────┐ ┌──────────┐ ┌───────────────────────┐ │
|
|
│ │ SQLite │ │ Local │ │ Claude Agent SDK (TS) │ │
|
|
│ │ Database │ │ File │ │ Spawns Claude Code │ │
|
|
│ │ (Prisma) │ │ Storage │ │ as subprocess │ │
|
|
│ └──────────┘ └──────────┘ └───────────┬───────────┘ │
|
|
└──────────────────────────────────────────┼───────────────┘
|
|
│
|
|
┌──────────────────────┼──────────┐
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌──────────┐ ┌──────────┐ ┌────────┐
|
|
│ Postiz │ │ Tavily │ │Nextdoor│
|
|
│ (Docker) │ │ API │ │ API │
|
|
│ IG+TikTok│ │ Research │ │ Direct │
|
|
│ +Media │ └──────────┘ └────────┘
|
|
│ Hosting │
|
|
└──────────┘
|
|
```
|
|
|
|
**Zero external services for core functionality.** Postiz and your Next.js app both run in Docker on your machine. The only outbound calls are to Tavily (research), Nextdoor API (publishing), and Anthropic (Claude).
|
|
|
|
---
|
|
|
|
## Tech Stack
|
|
|
|
| Layer | Technology | Why |
|
|
|-------|-----------|-----|
|
|
| **Framework** | Next.js 15+ (App Router) | Full-stack React, API routes, SSR |
|
|
| **UI** | shadcn/ui + Tailwind CSS | Clean components, fast to build |
|
|
| **Auth** | NextAuth.js (Auth.js v5) | Self-contained — credentials provider, no external auth service |
|
|
| **Database** | SQLite via Prisma | Zero infrastructure — single file, runs in Docker volume |
|
|
| **File Storage** | Local filesystem (Docker volume) | Pipeline outputs stored locally, Postiz handles public hosting |
|
|
| **Claude** | Claude Agent SDK (TypeScript) | Spawn Claude sessions, stream responses |
|
|
| **Publishing** | Postiz (@postiz/node SDK) | Media upload + hosting + scheduling + IG/TikTok publishing |
|
|
| **Realtime** | Server-Sent Events (SSE) | Pipeline progress streaming — no Redis/WebSocket service needed |
|
|
| **Deployment** | Docker Compose | Everything self-hosted, single `docker compose up` |
|
|
|
|
---
|
|
|
|
## Database Schema (SQLite via Prisma)
|
|
|
|
```prisma
|
|
// prisma/schema.prisma
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
url = "file:./data/marketing.db"
|
|
}
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
password String // bcrypt hashed
|
|
name String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Campaign {
|
|
id String @id @default(cuid())
|
|
name String
|
|
status String @default("draft") // draft, running, review, approved, published
|
|
prompt String? // the trigger prompt
|
|
platforms String // JSON array: ["instagram","tiktok","nextdoor"]
|
|
config String? // JSON: full campaign config from form
|
|
outputPath String? // local path to outputs/
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
agentRuns AgentRun[]
|
|
assets Asset[]
|
|
claudeSessions ClaudeSession[]
|
|
}
|
|
|
|
model AgentRun {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id])
|
|
agentName String // trend-scout, research, script-writer, etc.
|
|
status String @default("pending") // pending, running, completed, failed
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
durationMs Int?
|
|
outputSummary String?
|
|
outputPath String?
|
|
tokenUsage Int?
|
|
error String?
|
|
assets Asset[]
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Asset {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id])
|
|
agentRunId String?
|
|
agentRun AgentRun? @relation(fields: [agentRunId], references: [id])
|
|
type String // image, video, copy, research, script
|
|
platform String? // instagram, tiktok, nextdoor, all
|
|
format String? // png, mp4, json, txt, html, md
|
|
filePath String // local path in outputs/
|
|
fileName String
|
|
dimensions String? // 1080x1080, 1080x1920, etc.
|
|
metadata String? // JSON: caption, hashtags, hook text, scene plan
|
|
status String @default("draft") // draft, approved, rejected, published
|
|
publishedTo String? // JSON array of platforms published to
|
|
postizPostId String? // ID from Postiz after scheduling
|
|
postizMediaId String? // media ID from Postiz upload
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model ClaudeSession {
|
|
id String @id @default(cuid())
|
|
campaignId String
|
|
campaign Campaign @relation(fields: [campaignId], references: [id])
|
|
sessionId String? // Claude Code session ID for --resume
|
|
messages String? // JSON array of conversation history
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model TrendReport {
|
|
id String @id @default(cuid())
|
|
name String
|
|
filePath String // path to the HTML/JSON report
|
|
summary String? // brief description
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
**Why SQLite over Postgres:**
|
|
- Zero config — no separate database container
|
|
- Single file — easy to backup (just copy `marketing.db`)
|
|
- Fast enough for a single-user dashboard
|
|
- Runs anywhere Docker runs
|
|
- Prisma makes it trivial to switch to Postgres later if needed
|
|
|
|
---
|
|
|
|
## Authentication (NextAuth.js — Self-Contained)
|
|
|
|
```typescript
|
|
// lib/auth.ts
|
|
import NextAuth from "next-auth";
|
|
import Credentials from "next-auth/providers/credentials";
|
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
import bcrypt from "bcryptjs";
|
|
import { prisma } from "./prisma";
|
|
|
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
adapter: PrismaAdapter(prisma),
|
|
providers: [
|
|
Credentials({
|
|
credentials: {
|
|
email: { label: "Email", type: "email" },
|
|
password: { label: "Password", type: "password" },
|
|
},
|
|
async authorize(credentials) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: credentials.email as string },
|
|
});
|
|
if (!user) return null;
|
|
const valid = await bcrypt.compare(
|
|
credentials.password as string,
|
|
user.password
|
|
);
|
|
return valid ? user : null;
|
|
},
|
|
}),
|
|
],
|
|
session: { strategy: "jwt" },
|
|
secret: process.env.NEXTAUTH_SECRET,
|
|
});
|
|
```
|
|
|
|
**First-run setup:** A seed script creates your admin user. No external auth provider needed.
|
|
|
|
```typescript
|
|
// prisma/seed.ts
|
|
import bcrypt from "bcryptjs";
|
|
import { prisma } from "../lib/prisma";
|
|
|
|
async function main() {
|
|
await prisma.user.upsert({
|
|
where: { email: process.env.ADMIN_EMAIL! },
|
|
update: {},
|
|
create: {
|
|
email: process.env.ADMIN_EMAIL!,
|
|
password: await bcrypt.hash(process.env.ADMIN_PASSWORD!, 12),
|
|
name: "Admin",
|
|
},
|
|
});
|
|
}
|
|
main();
|
|
```
|
|
|
|
---
|
|
|
|
## Real-Time Pipeline Progress (Server-Sent Events)
|
|
|
|
No Redis, no WebSocket server, no Supabase Realtime. Just native SSE from a Next.js API route:
|
|
|
|
```typescript
|
|
// app/api/campaigns/[id]/stream/route.ts
|
|
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
const encoder = new TextEncoder();
|
|
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
// Subscribe to pipeline events for this campaign
|
|
const unsubscribe = pipelineEvents.subscribe(params.id, (event) => {
|
|
controller.enqueue(
|
|
encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
|
|
);
|
|
});
|
|
|
|
// Keep connection alive
|
|
const keepAlive = setInterval(() => {
|
|
controller.enqueue(encoder.encode(`: keepalive\n\n`));
|
|
}, 15000);
|
|
|
|
req.signal.addEventListener("abort", () => {
|
|
unsubscribe();
|
|
clearInterval(keepAlive);
|
|
controller.close();
|
|
});
|
|
},
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
Frontend hook:
|
|
|
|
```typescript
|
|
// hooks/usePipelineProgress.ts
|
|
export function usePipelineProgress(campaignId: string) {
|
|
const [agents, setAgents] = useState<AgentRun[]>([]);
|
|
|
|
useEffect(() => {
|
|
const source = new EventSource(`/api/campaigns/${campaignId}/stream`);
|
|
source.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
setAgents((prev) => updateAgentStatus(prev, data));
|
|
};
|
|
return () => source.close();
|
|
}, [campaignId]);
|
|
|
|
return agents;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Claude Integration
|
|
|
|
```typescript
|
|
// lib/claude.ts
|
|
import { spawn } from "child_process";
|
|
import { EventEmitter } from "events";
|
|
|
|
export const pipelineEvents = new EventEmitter();
|
|
|
|
export async function launchPipeline(campaignId: string, prompt: string, cwd: string) {
|
|
// Update campaign status
|
|
await prisma.campaign.update({
|
|
where: { id: campaignId },
|
|
data: { status: "running" },
|
|
});
|
|
|
|
const claude = spawn("claude", [
|
|
"-p", prompt,
|
|
"--output-format", "stream-json",
|
|
"--verbose",
|
|
"--allowedTools", "Read,Edit,Write,Bash,Grep,Glob",
|
|
], { cwd, env: { ...process.env } });
|
|
|
|
claude.stdout.on("data", async (chunk) => {
|
|
const lines = chunk.toString().split("\n").filter(Boolean);
|
|
for (const line of lines) {
|
|
try {
|
|
const event = JSON.parse(line);
|
|
|
|
// Detect agent starts/completions from tool use events
|
|
const agentEvent = parseAgentEvent(event);
|
|
if (agentEvent) {
|
|
// Save to database
|
|
await upsertAgentRun(campaignId, agentEvent);
|
|
// Broadcast to SSE clients
|
|
pipelineEvents.emit(campaignId, agentEvent);
|
|
}
|
|
|
|
// Detect new files created
|
|
const fileEvent = parseFileCreatedEvent(event);
|
|
if (fileEvent) {
|
|
await createAsset(campaignId, fileEvent);
|
|
pipelineEvents.emit(campaignId, { type: "asset_created", ...fileEvent });
|
|
}
|
|
} catch {}
|
|
}
|
|
});
|
|
|
|
claude.on("close", async (code) => {
|
|
const status = code === 0 ? "review" : "failed";
|
|
await prisma.campaign.update({
|
|
where: { id: campaignId },
|
|
data: { status },
|
|
});
|
|
pipelineEvents.emit(campaignId, { type: "pipeline_complete", status });
|
|
});
|
|
}
|
|
|
|
// Claude chat session for feedback
|
|
export async function sendChatMessage(
|
|
sessionId: string | null,
|
|
message: string,
|
|
cwd: string
|
|
): AsyncGenerator<string> {
|
|
const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
|
|
if (sessionId) args.push("--resume", sessionId);
|
|
args.push("--allowedTools", "Read,Edit,Write,Bash,Grep,Glob");
|
|
|
|
const claude = spawn("claude", args, { cwd, env: { ...process.env } });
|
|
|
|
// Stream text deltas back
|
|
for await (const chunk of claude.stdout) {
|
|
yield chunk.toString();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Postiz Integration (Media Upload + Publishing)
|
|
|
|
```typescript
|
|
// lib/postiz.ts
|
|
import { Postiz } from "@postiz/node";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import FormData from "form-data";
|
|
|
|
const postiz = new Postiz({
|
|
apiKey: process.env.POSTIZ_API_KEY!,
|
|
baseUrl: process.env.POSTIZ_URL!,
|
|
});
|
|
|
|
// Upload a local file to Postiz and get back a hosted URL + media ID
|
|
export async function uploadToPostiz(filePath: string) {
|
|
const form = new FormData();
|
|
form.append("file", fs.createReadStream(filePath));
|
|
|
|
const res = await fetch(`${process.env.POSTIZ_URL}/public/v1/upload`, {
|
|
method: "POST",
|
|
headers: { Authorization: process.env.POSTIZ_API_KEY! },
|
|
body: form,
|
|
});
|
|
|
|
const { id, path: publicUrl } = await res.json();
|
|
return { mediaId: id, publicUrl };
|
|
}
|
|
|
|
// Push an approved asset to Postiz for scheduling
|
|
export async function pushToPostiz(asset: Asset, scheduledAt: string) {
|
|
// Upload media first
|
|
const { mediaId, publicUrl } = await uploadToPostiz(asset.filePath);
|
|
|
|
// Get the right Postiz integration (channel) ID
|
|
const integrations = await postiz.integrations.list();
|
|
const integration = integrations.find(
|
|
(i) => i.providerIdentifier === asset.platform
|
|
);
|
|
|
|
if (!integration) throw new Error(`No Postiz channel for ${asset.platform}`);
|
|
|
|
// Parse caption from asset metadata
|
|
const metadata = JSON.parse(asset.metadata || "{}");
|
|
|
|
// Create the scheduled post
|
|
const post = await postiz.posts.create({
|
|
type: "schedule",
|
|
date: scheduledAt,
|
|
integration: integration.id,
|
|
content: metadata.caption || "",
|
|
image: [{ id: mediaId, path: publicUrl }],
|
|
});
|
|
|
|
return post;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## File Serving (Local Assets → Browser Preview)
|
|
|
|
Your pipeline outputs (images, videos) live on the local filesystem. The dashboard needs to serve them for preview:
|
|
|
|
```typescript
|
|
// app/api/files/[...path]/route.ts
|
|
import { readFile } from "fs/promises";
|
|
import path from "path";
|
|
import { auth } from "@/lib/auth";
|
|
|
|
const PIPELINE_ROOT = process.env.PIPELINE_ROOT || "/app/pipeline";
|
|
|
|
export async function GET(req: Request, { params }: { params: { path: string[] } }) {
|
|
// Auth check — only logged-in users can view files
|
|
const session = await auth();
|
|
if (!session) return new Response("Unauthorized", { status: 401 });
|
|
|
|
const filePath = path.join(PIPELINE_ROOT, ...params.path);
|
|
|
|
// Security: prevent path traversal
|
|
if (!filePath.startsWith(PIPELINE_ROOT)) {
|
|
return new Response("Forbidden", { status: 403 });
|
|
}
|
|
|
|
try {
|
|
const buffer = await readFile(filePath);
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const contentType =
|
|
{ ".png": "image/png", ".jpg": "image/jpeg", ".mp4": "video/mp4",
|
|
".html": "text/html", ".json": "application/json", ".md": "text/markdown",
|
|
".txt": "text/plain", ".css": "text/css" }[ext] || "application/octet-stream";
|
|
|
|
return new Response(buffer, { headers: { "Content-Type": contentType } });
|
|
} catch {
|
|
return new Response("Not found", { status: 404 });
|
|
}
|
|
}
|
|
```
|
|
|
|
Asset gallery uses: `<img src="/api/files/outputs/campaign_123/ads/instagram_feed.png" />`
|
|
|
|
Video preview uses: `<video src="/api/files/outputs/campaign_123/video/tiktok_ad.mp4" />`
|
|
|
|
---
|
|
|
|
## Docker Setup
|
|
|
|
### Dockerfile (Next.js App)
|
|
|
|
```dockerfile
|
|
# Dockerfile
|
|
FROM node:20-alpine AS base
|
|
|
|
# Install Claude Code CLI
|
|
RUN npm install -g @anthropic-ai/claude-code
|
|
|
|
# Install Playwright browsers (for ad creative generation)
|
|
RUN npx playwright install chromium --with-deps
|
|
|
|
FROM base AS deps
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
RUN npm ci
|
|
|
|
FROM base AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
RUN npx prisma generate
|
|
RUN npm run build
|
|
|
|
FROM base AS runner
|
|
WORKDIR /app
|
|
ENV NODE_ENV=production
|
|
|
|
COPY --from=builder /app/.next/standalone ./
|
|
COPY --from=builder /app/.next/static ./.next/static
|
|
COPY --from=builder /app/public ./public
|
|
COPY --from=builder /app/prisma ./prisma
|
|
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
|
|
|
# Copy the marketing pipeline
|
|
COPY pipeline/ ./pipeline/
|
|
|
|
EXPOSE 3000
|
|
CMD ["node", "server.js"]
|
|
```
|
|
|
|
### Docker Compose (Full Stack)
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
version: "3.8"
|
|
|
|
services:
|
|
# Your marketing dashboard + pipeline
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- NEXTAUTH_URL=http://localhost:3000
|
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
|
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
|
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
|
- POSTIZ_URL=http://postiz:5000
|
|
- POSTIZ_API_KEY=${POSTIZ_API_KEY}
|
|
- NEXTDOOR_API_TOKEN=${NEXTDOOR_API_TOKEN}
|
|
- NEXTDOOR_ADVERTISER_ID=${NEXTDOOR_ADVERTISER_ID}
|
|
- PIPELINE_ROOT=/app/pipeline
|
|
volumes:
|
|
- app-data:/app/prisma/data # SQLite database
|
|
- pipeline-outputs:/app/pipeline/outputs # Generated content
|
|
- pipeline-knowledge:/app/pipeline/knowledge # Brand files (editable)
|
|
depends_on:
|
|
- postiz
|
|
|
|
# Self-hosted Postiz (social media scheduling + media hosting)
|
|
postiz:
|
|
image: ghcr.io/gitroomhq/postiz-app:latest
|
|
ports:
|
|
- "5000:5000"
|
|
environment:
|
|
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postiz-db:5432/postiz
|
|
- REDIS_URL=redis://redis:6379
|
|
- NEXT_PUBLIC_BACKEND_URL=http://postiz:5000
|
|
- STORAGE_PROVIDER=local
|
|
- UPLOAD_DIRECTORY=/uploads
|
|
volumes:
|
|
- postiz-uploads:/uploads
|
|
- postiz-config:/config
|
|
depends_on:
|
|
- postiz-db
|
|
- redis
|
|
|
|
# Postiz database (Postgres — required by Postiz, not by your app)
|
|
postiz-db:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
- POSTGRES_DB=postiz
|
|
volumes:
|
|
- postiz-pgdata:/var/lib/postgresql/data
|
|
|
|
# Postiz cache (Redis — required by Postiz, not by your app)
|
|
redis:
|
|
image: redis:7-alpine
|
|
volumes:
|
|
- redis-data:/data
|
|
|
|
volumes:
|
|
app-data: # Your SQLite database
|
|
pipeline-outputs: # All generated content (images, videos, reports)
|
|
pipeline-knowledge: # Brand identity, platform guidelines, campaign docs
|
|
postiz-uploads: # Postiz media storage
|
|
postiz-config: # Postiz configuration
|
|
postiz-pgdata: # Postiz database
|
|
redis-data: # Postiz cache
|
|
```
|
|
|
|
### Environment File
|
|
|
|
```bash
|
|
# .env
|
|
|
|
# --- Your App ---
|
|
NEXTAUTH_SECRET=generate-a-random-32-char-string-here
|
|
ADMIN_EMAIL=you@yourdomain.com
|
|
ADMIN_PASSWORD=your-secure-password
|
|
|
|
# --- Claude (pipeline engine) ---
|
|
ANTHROPIC_API_KEY=sk-ant-your-key
|
|
|
|
# --- Research ---
|
|
TAVILY_API_KEY=tvly-your-key
|
|
|
|
# --- Postiz (publishing + media hosting) ---
|
|
POSTIZ_API_KEY=your-postiz-key
|
|
POSTGRES_PASSWORD=a-secure-password-for-postiz-db
|
|
|
|
# --- Nextdoor (direct API — Postiz doesn't support it) ---
|
|
NEXTDOOR_API_TOKEN=your-token
|
|
NEXTDOOR_ADVERTISER_ID=your-id
|
|
```
|
|
|
|
### Startup
|
|
|
|
```bash
|
|
# First run
|
|
docker compose up -d
|
|
docker compose exec app npx prisma db push # Create tables
|
|
docker compose exec app npx prisma db seed # Create admin user
|
|
|
|
# Open in browser
|
|
open http://localhost:3000
|
|
```
|
|
|
|
After first run, just `docker compose up -d` and everything starts.
|
|
|
|
---
|
|
|
|
## Project Folder Structure
|
|
|
|
```
|
|
marketing-command-center/
|
|
├── app/ # Next.js App Router
|
|
│ ├── (auth)/
|
|
│ │ ├── login/page.tsx
|
|
│ │ └── layout.tsx
|
|
│ ├── (dashboard)/
|
|
│ │ ├── layout.tsx # Sidebar + header
|
|
│ │ ├── page.tsx # Dashboard home
|
|
│ │ ├── campaigns/
|
|
│ │ │ ├── page.tsx # Campaign list
|
|
│ │ │ ├── new/page.tsx # New campaign form
|
|
│ │ │ └── [id]/
|
|
│ │ │ ├── page.tsx # Pipeline progress tab
|
|
│ │ │ ├── assets/page.tsx # Asset gallery tab
|
|
│ │ │ └── chat/page.tsx # Claude feedback tab
|
|
│ │ ├── assets/page.tsx # Global asset library
|
|
│ │ ├── trends/page.tsx # Trend reports
|
|
│ │ └── queue/page.tsx # Postiz queue / calendar
|
|
│ └── api/
|
|
│ ├── auth/[...nextauth]/route.ts
|
|
│ ├── campaigns/
|
|
│ │ ├── route.ts # CRUD
|
|
│ │ ├── [id]/
|
|
│ │ │ ├── launch/route.ts # Trigger pipeline
|
|
│ │ │ └── stream/route.ts # SSE pipeline progress
|
|
│ ├── assets/
|
|
│ │ └── route.ts # Asset CRUD + filtering
|
|
│ ├── claude/
|
|
│ │ └── route.ts # Chat sessions (stream)
|
|
│ ├── postiz/
|
|
│ │ └── route.ts # Upload + schedule to Postiz
|
|
│ └── files/
|
|
│ └── [...path]/route.ts # Serve local pipeline files
|
|
├── components/
|
|
│ ├── ui/ # shadcn/ui
|
|
│ ├── campaign-card.tsx
|
|
│ ├── asset-gallery.tsx
|
|
│ ├── asset-card.tsx
|
|
│ ├── pipeline-progress.tsx
|
|
│ ├── claude-chat.tsx
|
|
│ ├── postiz-push-modal.tsx
|
|
│ ├── trend-report-viewer.tsx
|
|
│ └── campaign-form.tsx
|
|
├── hooks/
|
|
│ ├── usePipelineProgress.ts # SSE hook
|
|
│ └── useClaudeChat.ts # Chat streaming hook
|
|
├── lib/
|
|
│ ├── auth.ts # NextAuth config
|
|
│ ├── prisma.ts # Prisma client
|
|
│ ├── claude.ts # Claude SDK wrapper
|
|
│ ├── postiz.ts # Postiz SDK wrapper
|
|
│ └── utils.ts
|
|
├── prisma/
|
|
│ ├── schema.prisma # Database schema
|
|
│ ├── seed.ts # Admin user creation
|
|
│ └── data/ # SQLite file lives here
|
|
│ └── marketing.db
|
|
├── pipeline/ # Your marketing pipeline
|
|
│ ├── .claude/
|
|
│ ├── skills/
|
|
│ │ ├── trend-scout/SKILL.md
|
|
│ │ ├── marketing-research-agent/SKILL.md
|
|
│ │ ├── script-writer/SKILL.md
|
|
│ │ ├── ad-creative-designer/SKILL.md
|
|
│ │ ├── video-ad-producer/SKILL.md
|
|
│ │ ├── copywriter-agent/SKILL.md
|
|
│ │ └── distribution-agent/SKILL.md
|
|
│ ├── knowledge/
|
|
│ │ ├── brand_identity.md
|
|
│ │ ├── platform_guidelines.md
|
|
│ │ └── product_campaign.md
|
|
│ ├── assets/ # Brand images, logos
|
|
│ ├── outputs/ # All generated content
|
|
│ ├── remotion-ad/ # Remotion video project
|
|
│ ├── CLAUDE.md
|
|
│ └── package.json
|
|
├── public/
|
|
├── .env
|
|
├── docker-compose.yml
|
|
├── Dockerfile
|
|
├── next.config.ts
|
|
├── tailwind.config.ts
|
|
├── tsconfig.json
|
|
└── package.json
|
|
```
|
|
|
|
---
|
|
|
|
## Pages & Features
|
|
|
|
### 1. Dashboard Home (`/`)
|
|
|
|
- Active campaigns with status badges (Running / Review Needed / Published)
|
|
- Recent trend reports from Trend Scout
|
|
- Quick stats: assets created, published this week, pending review
|
|
- "New Campaign" button
|
|
|
|
### 2. Campaign View (`/campaigns/[id]`)
|
|
|
|
**Pipeline Tab** — real-time agent execution progress via SSE:
|
|
```
|
|
✅ Trend Scout 12s "5 trending hooks found"
|
|
✅ Research Agent 2m "Market report generated"
|
|
✅ Script Writer 3m "15 scripts written"
|
|
🔄 Ad Creative 1m... "Generating static ads..."
|
|
⏳ Video Producer — waiting
|
|
⏳ Copywriter — waiting
|
|
⏳ Distribution — waiting (pending approval)
|
|
```
|
|
|
|
**Assets Tab** — gallery of everything produced:
|
|
- Grid of image/video previews with platform badges
|
|
- Filter by: platform, type (image/video/copy), status
|
|
- Click to expand: full preview + caption + metadata
|
|
- Approve / Reject buttons per asset
|
|
- "Push to Postiz" on approved assets
|
|
|
|
**Claude Tab** — live feedback chat:
|
|
- Streaming chat interface connected to Claude Code
|
|
- Session persists via `--resume`
|
|
- Type: "make it snarkier", "add competitor X to research", "regenerate hook B"
|
|
- New/updated assets auto-appear in the gallery
|
|
|
|
### 3. Asset Library (`/assets`)
|
|
|
|
- All assets across all campaigns
|
|
- Search by hook text, caption, platform
|
|
- Bulk approve / reject / push to Postiz
|
|
|
|
### 4. Trend Reports (`/trends`)
|
|
|
|
- Auto-generated trend reports (iframe HTML viewer)
|
|
- "Create Campaign from This Report" button
|
|
|
|
### 5. Postiz Queue (`/queue`)
|
|
|
|
- Everything pushed to Postiz with status (scheduled / published / failed)
|
|
- Calendar view of upcoming posts
|
|
- Link to Postiz dashboard at `localhost:5000` for visual calendar
|
|
|
|
---
|
|
|
|
## What Runs Where
|
|
|
|
| Component | Runs In | External Calls |
|
|
|-----------|---------|---------------|
|
|
| Next.js dashboard | Docker (your machine) | None — fully self-contained |
|
|
| SQLite database | Docker volume | None |
|
|
| Auth (NextAuth) | Docker (your machine) | None |
|
|
| File serving | Docker (your machine) | None |
|
|
| Pipeline progress (SSE) | Docker (your machine) | None |
|
|
| Claude Code sessions | Docker (spawns subprocess) | → Anthropic API |
|
|
| Research agent | Claude subprocess | → Tavily API |
|
|
| Image generation | Claude subprocess | → NanoBanana (Gemini API) |
|
|
| Video rendering | Claude subprocess | → Local Remotion render |
|
|
| Postiz | Docker (your machine) | → Instagram API, TikTok API |
|
|
| Nextdoor publishing | Your app API route | → Nextdoor API |
|
|
|
|
**Outbound network calls:** Anthropic, Tavily, Gemini (NanoBanana), Instagram (via Postiz), TikTok (via Postiz), Nextdoor. Everything else is local.
|
|
|
|
---
|
|
|
|
## Build Phases
|
|
|
|
### Phase 1: Scaffold (Week 1)
|
|
- [ ] Next.js + shadcn/ui + Tailwind setup
|
|
- [ ] Prisma + SQLite schema
|
|
- [ ] NextAuth credentials provider
|
|
- [ ] Docker Compose with Postiz
|
|
- [ ] Basic dashboard layout with sidebar nav
|
|
|
|
### Phase 2: Pipeline Integration (Week 2)
|
|
- [ ] Campaign CRUD pages
|
|
- [ ] Campaign launch form → spawns Claude Code subprocess
|
|
- [ ] SSE pipeline progress streaming
|
|
- [ ] File serving API route for asset preview
|
|
- [ ] Auto-scan outputs folder → populate assets table
|
|
|
|
### Phase 3: Asset Gallery + Claude Chat (Week 3)
|
|
- [ ] Asset gallery with grid view + filtering
|
|
- [ ] Image/video inline preview
|
|
- [ ] Approve / reject workflow
|
|
- [ ] Claude chat component with streaming
|
|
- [ ] Session persistence (--resume)
|
|
|
|
### Phase 4: Postiz Publishing (Week 4)
|
|
- [ ] Postiz upload integration
|
|
- [ ] "Push to Postiz" modal (select → schedule → confirm)
|
|
- [ ] Queue page with Postiz status sync
|
|
- [ ] Nextdoor direct API publishing
|
|
- [ ] Trend report viewer
|
|
|
|
### Phase 5: Polish (Week 5)
|
|
- [ ] Bulk actions (approve all, reject all, push batch)
|
|
- [ ] Asset search
|
|
- [ ] Mobile-responsive
|
|
- [ ] Trend Scout auto-scheduling (cron in Docker)
|
|
- [ ] Error handling + retry logic
|