diff --git a/god-mode/src/pages/admin/content-factory.astro b/god-mode/src/pages/admin/content-factory.astro new file mode 100644 index 0000000..43868ca --- /dev/null +++ b/god-mode/src/pages/admin/content-factory.astro @@ -0,0 +1,33 @@ +--- +import Layout from '@/layouts/AdminLayout.astro'; +import KanbanBoard from '@/components/admin/factory/KanbanBoard'; +import { Button } from '@/components/ui/button'; +import { Plus } from 'lucide-react'; +--- + + +
+
+
+

+ 🏭 Content Factory + Pro +

+

+ Drag and drop articles to move them through the production pipeline. +

+
+
+ + + +
+
+ +
+ +
+
+
diff --git a/god-mode/src/pages/api/collections/[table].ts b/god-mode/src/pages/api/collections/[table].ts new file mode 100644 index 0000000..2570f13 --- /dev/null +++ b/god-mode/src/pages/api/collections/[table].ts @@ -0,0 +1,217 @@ +/** + * Generic Collections API - CRUD for all God Mode tables + * GET /api/collections/avatars - List items + * GET /api/collections/avatars/[id] - Get single item + * POST /api/collections/avatars - Create item + * PATCH /api/collections/avatars/[id] - Update item + * DELETE /api/collections/avatars/[id] - Delete item + */ + +import type { APIRoute } from 'astro'; +import pkg from 'pg'; +const { Pool } = pkg; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL +}); + +// God Mode Token validation +function validateGodToken(request: Request): boolean { + const token = request.headers.get('X-God-Token') || + request.headers.get('Authorization')?.replace('Bearer ', '') || + new URL(request.url).searchParams.get('token'); + + const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN; + + if (!godToken) { + console.warn('⚠️ GOD_MODE_TOKEN not set - backdoor is open!'); + return true; + } + + return token === godToken; +} + +// Allowed tables (whitelist for security) +const ALLOWED_TABLES = [ + 'sites', + 'posts', + 'pages', + 'avatars', + 'content_blocks', + 'campaign_masters', + 'spintax_dictionaries', + 'spintax_patterns', + 'generation_jobs', + 'geo_clusters', + 'geo_locations' +]; + +function json(data: any, status = 200) { + return new Response(JSON.stringify(data, null, 2), { + status, + headers: { 'Content-Type': 'application/json' } + }); +} + +export const GET: APIRoute = async ({ params, request, url }) => { + if (!validateGodToken(request)) { + return json({ error: 'Unauthorized' }, 401); + } + + const table = params.table; + + if (!table || !ALLOWED_TABLES.includes(table)) { + return json({ error: 'Invalid table name' }, 400); + } + + try { + // Pagination + const limit = parseInt(url.searchParams.get('limit') || '50'); + const offset = parseInt(url.searchParams.get('offset') || '0'); + + // Sorting + const sort = url.searchParams.get('sort') || 'date_created'; + const order = url.searchParams.get('order') || 'DESC'; + + // Search + const search = url.searchParams.get('search'); + + let query = `SELECT * FROM ${table}`; + const queryParams: any[] = []; + + // Add search if provided + if (search) { + query += ` WHERE name ILIKE $1`; + queryParams.push(`%${search}%`); + } + + query += ` ORDER BY ${sort} ${order} LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`; + queryParams.push(limit, offset); + + const result = await pool.query(query, queryParams); + + // Get total count + const countQuery = search + ? `SELECT COUNT(*) FROM ${table} WHERE name ILIKE $1` + : `SELECT COUNT(*) FROM ${table}`; + const countParams = search ? [`%${search}%`] : []; + const countResult = await pool.query(countQuery, countParams); + + return json({ + data: result.rows, + meta: { + total: parseInt(countResult.rows[0].count), + limit, + offset + } + }); + } catch (error: any) { + return json({ error: error.message }, 500); + } +}; + +export const POST: APIRoute = async ({ params, request }) => { + if (!validateGodToken(request)) { + return json({ error: 'Unauthorized' }, 401); + } + + const table = params.table; + + if (!table || !ALLOWED_TABLES.includes(table)) { + return json({ error: 'Invalid table name' }, 400); + } + + try { + const body = await request.json(); + + // Build INSERT query + const columns = Object.keys(body); + const values = Object.values(body); + const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); + + const query = ` + INSERT INTO ${table} (${columns.join(', ')}) + VALUES (${placeholders}) + RETURNING * + `; + + const result = await pool.query(query, values); + + return json({ data: result.rows[0] }, 201); + } catch (error: any) { + return json({ error: error.message }, 500); + } +}; + +export const PATCH: APIRoute = async ({ params, request, url }) => { + if (!validateGodToken(request)) { + return json({ error: 'Unauthorized' }, 401); + } + + const table = params.table; + const id = url.searchParams.get('id'); + + if (!table || !ALLOWED_TABLES.includes(table)) { + return json({ error: 'Invalid table name' }, 400); + } + + if (!id) { + return json({ error: 'ID required' }, 400); + } + + try { + const body = await request.json(); + + // Build UPDATE query + const columns = Object.keys(body); + const values = Object.values(body); + const setClause = columns.map((col, i) => `${col} = $${i + 1}`).join(', '); + + const query = ` + UPDATE ${table} + SET ${setClause} + WHERE id = $${values.length + 1} + RETURNING * + `; + + const result = await pool.query(query, [...values, id]); + + if (result.rows.length === 0) { + return json({ error: 'Not found' }, 404); + } + + return json({ data: result.rows[0] }); + } catch (error: any) { + return json({ error: error.message }, 500); + } +}; + +export const DELETE: APIRoute = async ({ params, request, url }) => { + if (!validateGodToken(request)) { + return json({ error: 'Unauthorized' }, 401); + } + + const table = params.table; + const id = url.searchParams.get('id'); + + if (!table || !ALLOWED_TABLES.includes(table)) { + return json({ error: 'Invalid table name' }, 400); + } + + if (!id) { + return json({ error: 'ID required' }, 400); + } + + try { + const query = `DELETE FROM ${table} WHERE id = $1 RETURNING id`; + const result = await pool.query(query, [id]); + + if (result.rows.length === 0) { + return json({ error: 'Not found' }, 404); + } + + return json({ success: true }); + } catch (error: any) { + return json({ error: error.message }, 500); + } +}; diff --git a/god-mode/src/pages/preview/article/[articleId].astro b/god-mode/src/pages/preview/article/[articleId].astro new file mode 100644 index 0000000..64f1cd8 --- /dev/null +++ b/god-mode/src/pages/preview/article/[articleId].astro @@ -0,0 +1,287 @@ +--- +import { getDirectusClient, readItems } from '@/lib/directus/client'; + +const { articleId } = Astro.params; + +if (!articleId) { + return Astro.redirect('/404'); +} + +const client = getDirectusClient(); + +let article; +try { + // @ts-ignore + const articles = await client.request(readItems('generated_articles', { + filter: { id: { _eq: articleId } }, + limit: 1 + })); + + article = articles[0]; + + if (!article) { + return Astro.redirect('/404'); + } +} catch (error) { + console.error('Error fetching article:', error); + return Astro.redirect('/404'); +} +--- + + + + + + + {article.title} - Preview + + + + +
+ 🔍 PREVIEW MODE - This is how your article will appear +
+ +
+
+
+

{article.title}

+ +
+ +
+ +
+ + +
+
+ + diff --git a/god-mode/src/pages/preview/page/[pageId].astro b/god-mode/src/pages/preview/page/[pageId].astro new file mode 100644 index 0000000..c09343f --- /dev/null +++ b/god-mode/src/pages/preview/page/[pageId].astro @@ -0,0 +1,135 @@ +--- +/** + * Preview Page Route + * Shows a single page in preview mode + */ + +import { getDirectusClient, readItem } from '@/lib/directus/client'; +import BlockRenderer from '@/components/engine/BlockRenderer'; + +const { pageId } = Astro.params; + +if (!pageId) { + return Astro.redirect('/admin/pages'); +} + +interface Page { + id: string; + title: string; + content: string; + blocks: any[]; + status: string; +} + +let page: Page | null = null; +let error: string | null = null; + +try { + const client = getDirectusClient(); + const result = await client.request(readItem('pages', pageId)); + page = result as Page; +} catch (err) { + error = err instanceof Error ? err.message : 'Failed to load page'; + console.error('Preview error:', err); +} + +import '@/styles/global.css'; + +if (!page && !error) { + return Astro.redirect('/admin/pages'); +} +--- + + + + + + + Preview: {page?.title || 'Page'} + + + + +
+
+ + + + + PREVIEW MODE + {page && {page.status || 'Draft'}} +
+
+ {page?.title} + +
+
+ + +
+ {error ? ( +
+

Error Loading Preview

+

{error}

+
+ ) : page ? ( + <> + + {(!page.blocks || page.blocks.length === 0) && page.content && ( + // Fallback for content +
+
+ )} + + ) : null} +
+ + diff --git a/god-mode/src/pages/preview/site/[siteId].astro b/god-mode/src/pages/preview/site/[siteId].astro new file mode 100644 index 0000000..9e5c09c --- /dev/null +++ b/god-mode/src/pages/preview/site/[siteId].astro @@ -0,0 +1,282 @@ +--- +/** + * Preview Site Route + * Shows all pages for a site in preview mode + */ + +import { getDirectusClient, readItems } from '@/lib/directus/client'; + +const { siteId } = Astro.params; + +if (!siteId) { + return Astro.redirect('/admin/sites'); +} + +interface Site { + id: string; + name: string; + domain: string; + status: string; + date_created: string; +} + +interface Page { + id: string; + title: string; + status: string; + permalink: string; + slug: string; + seo_description: string; +} + +let site: Site | null = null; +let pages: Page[] = []; +let error: string | null = null; + +try { + const client = getDirectusClient(); + + // Fetch site + const siteResult = await client.request(readItems('sites', { + filter: { id: { _eq: siteId } }, + limit: 1 + })); + site = siteResult[0] as Site; + + // Fetch pages for this site + // Note: directus-shim buildWhere supports _eq + const pagesResult = await client.request(readItems('pages', { + filter: { site: { _eq: siteId } }, + limit: -1 + })); + pages = pagesResult as Page[]; + +} catch (err) { + error = err instanceof Error ? err.message : 'Failed to load site'; + console.error('Preview error:', err); +} + +if (!site && !error) { + return Astro.redirect('/admin/sites'); +} +--- + + + + + + + Preview: {site?.name || 'Site'} + + + + +
+ + +
+ + +
+ {error ? ( +
+

Error Loading Site

+

{error}

+
+ ) : site ? ( + <> + + +

Pages

+ + {pages.length > 0 ? ( +
+ {pages.map((page) => ( +
+

{page.title}

+

{page.seo_description || 'No description'}

+
+ + {page.status || 'draft'} + + + /{page.permalink || page.slug} + +
+
+ ))} +
+ ) : ( +
+ + + +

No pages created yet for this site.

+

+ Create your first page → +

+
+ )} + + ) : null} +
+ +