Files
Trey t 80a1ffbe4d feat: add multi-app support with app switcher, per-app branding, and filtered queries
Apps share the same backend, API keys, and publishing flow but each gets its own
branding (name, colors, icon, app URL), knowledge files (brand identity, product
info, platform guidelines), and campaigns. The pipeline dynamically writes
_knowledge/ files and copies app assets before each run.

- Add App model with slug, colors, appUrl, and knowledge markdown fields
- Add appId FK to Campaign, seed honeyDue as first app with existing knowledge
- App switcher dropdown in sidebar with icon previews
- Filter campaigns, stats, and assets by active app (cookie-based)
- De-hardcode lib/claude.ts: AppConfig interface, templated prompts, dynamic
  _knowledge/ and Remotion asset copying
- App management pages (list, create, edit) with icon upload and color pickers
- Asset library sort options (newest, oldest, name, platform, type)
- Asset cards show creation date
- Remotion HoneyDueAd accepts colors/appName props

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 22:21:45 -05:00

100 lines
3.0 KiB
TypeScript

import bcrypt from "bcryptjs";
import { PrismaClient } from "../lib/generated/prisma/client";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { readFileSync, mkdirSync, copyFileSync, existsSync } from "fs";
import path from "path";
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || "file:./prisma/data/marketing.db",
});
const prisma = new PrismaClient({ adapter });
function readKnowledgeFile(filename: string): string | null {
const filePath = path.join(__dirname, "..", "pipeline", "knowledge", filename);
try {
return readFileSync(filePath, "utf-8");
} catch {
console.warn(`Knowledge file not found: ${filePath}`);
return null;
}
}
async function main() {
// Seed admin user
const email = process.env.ADMIN_EMAIL || "admin@localhost";
const password = process.env.ADMIN_PASSWORD || "admin123";
await prisma.user.upsert({
where: { email },
update: {},
create: {
email,
password: await bcrypt.hash(password, 12),
name: "Admin",
},
});
console.log(`Admin user created: ${email}`);
// Seed honeyDue app
const brandIdentity = readKnowledgeFile("brand_identity.md");
const productInfo = readKnowledgeFile("product_campaign.md");
const platformGuidelines = readKnowledgeFile("platform_guidelines.md");
const app = await prisma.app.upsert({
where: { slug: "honeydue" },
update: {
brandIdentity,
productInfo,
platformGuidelines,
},
create: {
name: "honeyDue",
slug: "honeydue",
description: "Home maintenance tracking app",
primaryColor: "#0079FF",
accentColor: "#FF9400",
darkBg: "#1a1a2e",
brandIdentity,
productInfo,
platformGuidelines,
},
});
console.log(`App created: ${app.name} (${app.slug})`);
// Backfill existing campaigns with appId
const updated = await prisma.campaign.updateMany({
where: { appId: null },
data: { appId: app.id },
});
console.log(`Backfilled ${updated.count} campaigns with appId`);
// Copy assets to pipeline/apps/honeydue/
const pipelineRoot = path.join(__dirname, "..", "pipeline");
const appAssetsDir = path.join(pipelineRoot, "apps", "honeydue");
const screenshotsDir = path.join(appAssetsDir, "screenshots");
mkdirSync(screenshotsDir, { recursive: true });
const assetsToCopy = [
{ src: path.join(pipelineRoot, "assets", "icon.png"), dest: path.join(appAssetsDir, "icon.png") },
{ src: path.join(pipelineRoot, "assets", "phone.png"), dest: path.join(appAssetsDir, "phone.png") },
{ src: path.join(pipelineRoot, "assets", "screenshots", "tasks_overdue.png"), dest: path.join(screenshotsDir, "tasks_overdue.png") },
];
for (const { src, dest } of assetsToCopy) {
if (existsSync(src)) {
copyFileSync(src, dest);
console.log(`Copied ${path.basename(src)} → apps/honeydue/`);
} else {
console.warn(`Asset not found: ${src}`);
}
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());