This commit is contained in:
36
scripts/count_projects.ts
Normal file
36
scripts/count_projects.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("📊 DATABASE AUDIT:");
|
||||
|
||||
// Count Users
|
||||
const userCount = await prisma.user.count();
|
||||
console.log(`- Total Users: ${userCount}`);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
include: { _count: { select: { projects: true } } }
|
||||
});
|
||||
|
||||
users.forEach(u => {
|
||||
console.log(` 👤 ${u.email} (${u.role}): ${u._count.projects} projects`);
|
||||
});
|
||||
|
||||
// Count Projects
|
||||
const projectCount = await prisma.project.count();
|
||||
console.log(`\n- Total Projects: ${projectCount}`);
|
||||
|
||||
const projects = await prisma.project.findMany({
|
||||
select: { id: true, niche: true, createdAt: true, userId: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5
|
||||
});
|
||||
|
||||
console.log("\nRecent Projects:");
|
||||
projects.forEach(p => console.log(` 📂 ${p.niche} (${p.createdAt.toISOString()}) - User: ${p.userId}`));
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main();
|
||||
59
scripts/legacy/emergency_json_fix.ts
Normal file
59
scripts/legacy/emergency_json_fix.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function fix() {
|
||||
console.log("🚑 STARTING EMERGENCY JSON REPAIR...");
|
||||
|
||||
const recoveredProjects = await prisma.project.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ niche: { contains: "Recovered Project" } },
|
||||
{ seoData: { is: null } }
|
||||
]
|
||||
},
|
||||
include: { seoData: true }
|
||||
});
|
||||
|
||||
console.log(`🔍 Found ${recoveredProjects.length} projects to tokenize.`);
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
for (const project of recoveredProjects) {
|
||||
try {
|
||||
// Force minimal valid JSON
|
||||
// If niche contains "Recovered Project", use it, else generic.
|
||||
const safeTitle = `Recovered Project ${project.niche.replace('Recovered Project ', '').substring(0, 5)}`;
|
||||
|
||||
await prisma.seoData.upsert({
|
||||
where: { projectId: project.id },
|
||||
create: {
|
||||
projectId: project.id,
|
||||
title: safeTitle,
|
||||
description: "Project recovered from storage. Metadata pending reconstruction.",
|
||||
keywords: JSON.stringify(["Recovered", "Storage", "Needs Review"]),
|
||||
jsonLd: "{}", // Empty valid JSON
|
||||
printingGuide: "Standard Guide",
|
||||
suggestedPrice: "5.00"
|
||||
},
|
||||
update: {
|
||||
// Update only if invalid or generic
|
||||
// Actually, force update to fix the "keywords string" crash
|
||||
keywords: JSON.stringify(["Recovered", "Storage", "Needs Review"]),
|
||||
jsonLd: "{}"
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.write('.');
|
||||
successCount++;
|
||||
} catch (e: any) {
|
||||
console.error(`Error ${project.id}:`, e.message);
|
||||
}
|
||||
}
|
||||
console.log(`\n✅ REPAIR COMPLETE. Tokens Fixed: ${successCount}`);
|
||||
}
|
||||
|
||||
fix();
|
||||
200
scripts/legacy/reconstruct_metadata.ts
Normal file
200
scripts/legacy/reconstruct_metadata.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import { GoogleGenAI, Type, Part } from "@google/genai";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import sharp from 'sharp'; // Correct top-level import
|
||||
|
||||
// Load environment variables manually if helpful, but usually preloaded
|
||||
dotenv.config();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
// Adjust API Key retrieval if needed
|
||||
const API_KEY = process.env.GEMINI_API_KEY || process.env.API_KEY || "";
|
||||
const ai = new GoogleGenAI({ apiKey: API_KEY });
|
||||
|
||||
const STORAGE_ROOT = path.join(process.cwd(), '..', 'storage');
|
||||
|
||||
async function reconstruct() {
|
||||
console.log("🚑 STARTING AI METADATA RECONSTRUCTION (v2)...");
|
||||
|
||||
// We target "Recovered Project" in particular
|
||||
// Note: Project model does not have strategy column, so we rely on Niche check
|
||||
const recoveredProjects = await prisma.project.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ niche: { contains: "Recovered Project" } },
|
||||
{ seoData: { is: null } }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
assets: true,
|
||||
seoData: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔍 Found ${recoveredProjects.length} projects needing reconstruction.`);
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
for (const project of recoveredProjects) {
|
||||
try {
|
||||
// Find Master Asset
|
||||
const masterAsset = project.assets.find(a => a.type === 'master' || a.type === 'MASTER');
|
||||
if (!masterAsset) {
|
||||
// console.log(`⚠️ Skiping ${project.id}: No master asset.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const imagePath = path.join(STORAGE_ROOT, masterAsset.path.replace('projects/', 'projects/'));
|
||||
// Correction: masterAsset.path is relative to storage root?
|
||||
// masterAsset.path usually is "projects/UUID/master/file.png"
|
||||
// STORAGE_ROOT is ".../storage"
|
||||
// Join safely.
|
||||
const fullPath = path.join(STORAGE_ROOT, ...masterAsset.path.split('/').slice(1));
|
||||
// Wait, if path is "projects/..." and STORAGE_ROOT has "storage", we need to check real fs path.
|
||||
// Let's try direct join first.
|
||||
let realPath = path.join(process.cwd(), '../storage', masterAsset.path);
|
||||
|
||||
if (!fs.existsSync(realPath)) {
|
||||
// Try alternative path (sometimes assets stored with leading slash?)
|
||||
realPath = path.join(process.cwd(), '../storage', masterAsset.path.replace(/^projects\//, ''));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(realPath)) {
|
||||
console.log(`❌ Image not found on disk: ${realPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read Image
|
||||
const rawBuffer = fs.readFileSync(realPath);
|
||||
|
||||
// Resize if too large (Optimization)
|
||||
const resizedBuffer = await sharp(rawBuffer)
|
||||
.resize({ width: 1024, height: 1024, fit: 'inside' })
|
||||
.toBuffer();
|
||||
|
||||
const base64Image = resizedBuffer.toString('base64');
|
||||
|
||||
// CALL GEMINI
|
||||
console.log(`🤖 Analyzing ${project.id.substring(0, 8)}...`);
|
||||
|
||||
// Construct Prompt Parts
|
||||
const parts: Part[] = [
|
||||
{
|
||||
text: `
|
||||
You are a Professional Market Analyst and SEO Specialist.
|
||||
Analyze this image (which is a digital product for Etsy) to reconstruct its lost metadata.
|
||||
|
||||
IMPORTANT:
|
||||
- Keep "description" SHORT and CONCISE (max 50 words).
|
||||
- Return exactly 13 "keywords".
|
||||
- Do not hallucinate long text.
|
||||
|
||||
Return ONLY a JSON object:
|
||||
{
|
||||
"niche": "specific niche string",
|
||||
"productType": "Wall Art",
|
||||
"title": "SEO Title",
|
||||
"description": "Short Marketing Description",
|
||||
"keywords": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9", "tag10", "tag11", "tag12", "tag13"],
|
||||
"creativity": "Balanced",
|
||||
"jsonLd": { ...valid schema... }
|
||||
}
|
||||
` },
|
||||
{ inlineData: { data: base64Image, mimeType: "image/png" } }
|
||||
];
|
||||
|
||||
const result = await ai.models.generateContent({
|
||||
model: "gemini-flash-latest", // 1.5 Flash (Confirmed available)
|
||||
contents: { parts },
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
niche: { type: Type.STRING },
|
||||
productType: { type: Type.STRING },
|
||||
title: { type: Type.STRING },
|
||||
description: { type: Type.STRING },
|
||||
keywords: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
creativity: { type: Type.STRING },
|
||||
jsonLd: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
"@context": { type: Type.STRING },
|
||||
"@type": { type: Type.STRING },
|
||||
name: { type: Type.STRING },
|
||||
description: { type: Type.STRING },
|
||||
image: { type: Type.STRING },
|
||||
brand: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let responseText = result.text;
|
||||
if (!responseText) throw new Error("Empty AI Response");
|
||||
|
||||
// Clean markdown fences
|
||||
responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = JSON.parse(responseText);
|
||||
} catch (jsonErr) {
|
||||
throw new Error("Failed to parse JSON: " + responseText.substring(0, 100));
|
||||
}
|
||||
|
||||
// Defaults if missing
|
||||
const finalTitle = metadata.title || `Recovered Project ${project.niche.substring(0, 10)}`;
|
||||
const finalDescription = metadata.description || "Project metadata recovered from image analysis.";
|
||||
const finalKeywords = Array.isArray(metadata.keywords) ? metadata.keywords : ["Recovered"];
|
||||
|
||||
// UDPATE DATABASE
|
||||
await prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
niche: metadata.niche || "Recovered Art",
|
||||
productType: metadata.productType || "Wall Art"
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.seoData.upsert({
|
||||
where: { projectId: project.id },
|
||||
create: {
|
||||
projectId: project.id,
|
||||
title: finalTitle,
|
||||
description: finalDescription,
|
||||
keywords: JSON.stringify(finalKeywords),
|
||||
jsonLd: JSON.stringify(metadata.jsonLd || {}),
|
||||
printingGuide: "Standard",
|
||||
suggestedPrice: "5.00"
|
||||
},
|
||||
update: {
|
||||
title: finalTitle,
|
||||
description: finalDescription,
|
||||
keywords: JSON.stringify(finalKeywords),
|
||||
jsonLd: JSON.stringify(metadata.jsonLd || {})
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.write('+');
|
||||
successCount++;
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Error ${project.id}:`, err.message);
|
||||
// Log preview if it was a parse error
|
||||
if (err.message.includes('JSON')) {
|
||||
console.log("Response Preview:", err.stack); // Stack is too noisy, but okay for now.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n🎉 RECONSTRUCTION COMPLETE. Fixed ${successCount} projects.`);
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
reconstruct();
|
||||
150
scripts/legacy/recover_orphaned_projects.ts
Normal file
150
scripts/legacy/recover_orphaned_projects.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const STORAGE_ROOT = '/Users/haruncan/Downloads/DEV/etsy-mastermind-engine-v13.1/storage';
|
||||
|
||||
async function recover() {
|
||||
console.log("🚑 STARTING EMERGENCY RECOVERY V2...");
|
||||
|
||||
const adminUser = await prisma.user.findUnique({ where: { email: 'admin@digicraft.app' } });
|
||||
if (!adminUser) {
|
||||
console.error("❌ Admin user not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const projectsDir = path.join(STORAGE_ROOT, 'projects');
|
||||
if (!fs.existsSync(projectsDir)) return;
|
||||
|
||||
const folders = fs.readdirSync(projectsDir).filter(f => fs.statSync(path.join(projectsDir, f)).isDirectory());
|
||||
console.log(`📂 Scanning ${folders.length} folders...`);
|
||||
|
||||
let recoveredCount = 0;
|
||||
|
||||
for (const projectId of folders) {
|
||||
// 1. Gather Metadata First
|
||||
const projectPath = path.join(projectsDir, projectId);
|
||||
const stats = fs.statSync(projectPath);
|
||||
const createdAt = stats.birthtime;
|
||||
|
||||
// Niche Extraction
|
||||
let niche = `Recovered Project (${projectId.substring(0, 8)})`;
|
||||
const guidePath = path.join(projectPath, 'docs', 'Printing_Guide.txt');
|
||||
if (fs.existsSync(guidePath)) {
|
||||
// Can be enhanced to read content later
|
||||
}
|
||||
|
||||
// Assets Scan
|
||||
const masterDir = path.join(projectPath, 'master');
|
||||
const masterFiles = fs.existsSync(masterDir)
|
||||
? fs.readdirSync(masterDir).filter(f => f.endsWith('.png') || f.endsWith('.jpg'))
|
||||
: [];
|
||||
|
||||
if (masterFiles.length === 0) {
|
||||
// Skip empty projects to avoid clutter? Or recover anyway?
|
||||
// Let's recover anyway but mark as draft? No, skip if no master.
|
||||
// console.warn(`Skipping ${projectId} (No Master)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. DB Operations
|
||||
const exists = await prisma.project.findUnique({ where: { id: projectId } });
|
||||
|
||||
if (exists) {
|
||||
// Repair Existing
|
||||
await prisma.project.update({
|
||||
where: { id: projectId },
|
||||
data: {
|
||||
userId: adminUser.id,
|
||||
status: "COMPLETED",
|
||||
niche: niche // Update niche incase it was generic
|
||||
}
|
||||
});
|
||||
process.stdout.write('.');
|
||||
} else {
|
||||
// Create New
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: projectId,
|
||||
userId: adminUser.id,
|
||||
niche: niche,
|
||||
productType: "Wall Art",
|
||||
creativity: "Balanced",
|
||||
aspectRatio: "3:4",
|
||||
status: "COMPLETED",
|
||||
createdAt: createdAt
|
||||
}
|
||||
});
|
||||
process.stdout.write('+');
|
||||
}
|
||||
|
||||
// ALWAYS ENSURE SEO DATA (UPSERT)
|
||||
const seoDataPayload = {
|
||||
title: niche,
|
||||
description: "Recovered from storage.",
|
||||
keywords: "recovered, art, vintage",
|
||||
printingGuide: "Standard",
|
||||
suggestedPrice: "5.00",
|
||||
jsonLd: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": niche,
|
||||
"description": "Recovered from storage.",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "5.00",
|
||||
"priceCurrency": "USD"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const seoExists = await prisma.seoData.findUnique({ where: { projectId: projectId } });
|
||||
if (!seoExists) {
|
||||
await prisma.seoData.create({
|
||||
data: { ...seoDataPayload, projectId: projectId }
|
||||
});
|
||||
} else {
|
||||
// Force update to ensure JSON-LD is there
|
||||
await prisma.seoData.update({
|
||||
where: { projectId: projectId },
|
||||
data: seoDataPayload
|
||||
});
|
||||
process.stdout.write('s'); // seo update
|
||||
}
|
||||
|
||||
// 4. Ensure Assets
|
||||
for (const file of masterFiles) {
|
||||
const assetPath = `projects/${projectId}/master/${file}`;
|
||||
const assetExists = await prisma.asset.findFirst({ where: { path: assetPath } });
|
||||
|
||||
if (!assetExists) {
|
||||
await prisma.asset.create({
|
||||
data: {
|
||||
projectId: projectId,
|
||||
type: "master", // LOWERCASE to match API expectation
|
||||
path: assetPath,
|
||||
createdAt: fs.statSync(path.join(masterDir, file)).birthtime,
|
||||
quality: "MASTER"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fix bad casing if exists
|
||||
if (assetExists.type === 'MASTER') {
|
||||
await prisma.asset.update({
|
||||
where: { id: assetExists.id },
|
||||
data: { type: 'master' }
|
||||
});
|
||||
process.stdout.write('f'); // fix
|
||||
}
|
||||
}
|
||||
}
|
||||
recoveredCount++;
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Processed ${recoveredCount} projects.`);
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
recover();
|
||||
34
scripts/transfer_projects.ts
Normal file
34
scripts/transfer_projects.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function transfer() {
|
||||
// Get target email from CLI args
|
||||
const targetEmail = process.argv[2];
|
||||
if (!targetEmail) {
|
||||
console.error("❌ Usage: npx tsx scripts/transfer_projects.ts <target_email>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🚚 STARTING MIGRATION to: ${targetEmail}`);
|
||||
|
||||
// 1. Find Target User
|
||||
const targetUser = await prisma.user.findUnique({ where: { email: targetEmail } });
|
||||
if (!targetUser) {
|
||||
console.error(`❌ User ${targetEmail} not found. Please register first!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Update ALL projects to point to this user
|
||||
// (Except maybe "God Mode Test" projects? No, give them everything)
|
||||
const result = await prisma.project.updateMany({
|
||||
data: { userId: targetUser.id }
|
||||
});
|
||||
|
||||
console.log(`✅ SUCCESS: Transferred ${result.count} projects to ${targetEmail}.`);
|
||||
console.log(`🎉 They should now appear in the dashboard.`);
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
transfer();
|
||||
Reference in New Issue
Block a user