From 0760450a6d46d5398f9dfda350a236089a4e4746 Mon Sep 17 00:00:00 2001 From: cawcenter Date: Mon, 15 Dec 2025 12:21:30 -0500 Subject: [PATCH] feat: Add God Mode Command Center dashboard - Created unified admin dashboard at /admin/god-mode - Added search API endpoint for cross-collection queries - Added bulk actions API for publish/draft/archive/delete - Built 5 React components: - GodModeCommandCenter: Main dashboard with 4-tab interface - UnifiedSearchBar: Global search across all collections - BulkActionsToolbar: Bulk operations UI - ContentTable: Data table with selection and preview links - StatsPanel: Live statistics for all collections Features: - Search across sites, pages, posts, and articles - Bulk edit hundreds of items at once - Live stats with auto-refresh - Preview links for all content types - Status badges and metadata display --- .../admin/god/BulkActionsToolbar.tsx | 122 +++++ .../src/components/admin/god/ContentTable.tsx | 169 +++++++ .../admin/god/GodModeCommandCenter.tsx | 107 +++++ .../admin/god/SchemaTestDashboard.tsx | 432 ++++++++++++++++++ .../src/components/admin/god/StatsPanel.tsx | 78 ++++ .../components/admin/god/UnifiedSearchBar.tsx | 75 +++ frontend/src/pages/admin/god-mode.astro | 8 + frontend/src/pages/api/god/bulk-actions.ts | 138 ++++++ frontend/src/pages/api/god/search.ts | 86 ++++ 9 files changed, 1215 insertions(+) create mode 100644 frontend/src/components/admin/god/BulkActionsToolbar.tsx create mode 100644 frontend/src/components/admin/god/ContentTable.tsx create mode 100644 frontend/src/components/admin/god/GodModeCommandCenter.tsx create mode 100644 frontend/src/components/admin/god/SchemaTestDashboard.tsx create mode 100644 frontend/src/components/admin/god/StatsPanel.tsx create mode 100644 frontend/src/components/admin/god/UnifiedSearchBar.tsx create mode 100644 frontend/src/pages/admin/god-mode.astro create mode 100644 frontend/src/pages/api/god/bulk-actions.ts create mode 100644 frontend/src/pages/api/god/search.ts diff --git a/frontend/src/components/admin/god/BulkActionsToolbar.tsx b/frontend/src/components/admin/god/BulkActionsToolbar.tsx new file mode 100644 index 0000000..18e0f8d --- /dev/null +++ b/frontend/src/components/admin/god/BulkActionsToolbar.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { CheckSquare, Square, Trash2, Archive, Eye, EyeOff } from 'lucide-react'; + +interface BulkActionsToolbarProps { + selectedIds: string[]; + collection: string; + onActionComplete?: () => void; +} + +export function BulkActionsToolbar({ selectedIds, collection, onActionComplete }: BulkActionsToolbarProps) { + const [isProcessing, setIsProcessing] = useState(false); + const [lastAction, setLastAction] = useState(''); + + const handleBulkAction = async (action: string) => { + if (selectedIds.length === 0) { + alert('No items selected'); + return; + } + + const confirmMessage = `${action.toUpperCase()} ${selectedIds.length} item(s)?`; + if (action === 'delete' && !confirm(`⚠️ ${confirmMessage}\nThis cannot be undone!`)) { + return; + } else if (!confirm(confirmMessage)) { + return; + } + + setIsProcessing(true); + setLastAction(action); + + try { + const response = await fetch('/api/god/bulk-actions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action, + collection, + ids: selectedIds + }) + }); + + const result = await response.json(); + + if (result.success) { + alert(`✅ ${action.toUpperCase()} completed\n✓ Success: ${result.results.success}\n✗ Failed: ${result.results.failed}`); + onActionComplete?.(); + } else { + alert(`❌ Action failed: ${result.error}`); + } + } catch (error) { + console.error('Bulk action error:', error); + alert('❌ Action failed. Check console for details.'); + } finally { + setIsProcessing(false); + setLastAction(''); + } + }; + + if (selectedIds.length === 0) { + return ( +
+ + Select items to perform bulk actions +
+ ); + } + + return ( +
+
+
+ + {selectedIds.length} item(s) selected +
+ +
+ + + + + + + +
+
+ + {isProcessing && ( +
+
+ Processing {lastAction}... +
+ )} +
+ ); +} diff --git a/frontend/src/components/admin/god/ContentTable.tsx b/frontend/src/components/admin/god/ContentTable.tsx new file mode 100644 index 0000000..f15a49d --- /dev/null +++ b/frontend/src/components/admin/god/ContentTable.tsx @@ -0,0 +1,169 @@ +import { useState, useEffect } from 'react'; +import { useQuery } from '@tantml:parameter name="query'; +import { getDirectusClient, readItems } from '@/lib/directus/client'; +import { ExternalLink, CheckSquare, Square } from 'lucide-react'; + +interface ContentTableProps { + collection: string; + searchResults?: any[]; + onSelectionChange?: (ids: string[]) => void; +} + +export function ContentTable({ collection, searchResults, onSelectionChange }: ContentTableProps) { + const [selectedIds, setSelectedIds] = useState([]); + + // Fetch data if no search results provided + const { data, isLoading, refetch } = useQuery({ + queryKey: [collection], + queryFn: async () => { + const directus = getDirectusClient(); + return await directus.request(readItems(collection, { + limit: 100, + sort: ['-date_created'], + fields: ['*'] + })); + }, + enabled: !searchResults + }); + + const items = searchResults?.filter(r => r._collection === collection) || (data as any[]) || []; + + useEffect(() => { + onSelectionChange?.(selectedIds); + }, [selectedIds, onSelectionChange]); + + const toggleSelection = (id: string) => { + setSelectedIds(prev => + prev.includes(id) + ? prev.filter(i => i !== id) + : [...prev, id] + ); + }; + + const toggleAll = () => { + if (selectedIds.length === items.length) { + setSelectedIds([]); + } else { + setSelectedIds(items.map((i: any) => i.id)); + } + }; + + const getPreviewUrl = (item: any) => { + if (collection === 'sites') return item.url; + if (collection === 'pages') return `/preview/page/${item.id}`; + if (collection === 'posts') return `/preview/post/${item.id}`; + if (collection === 'generated_articles') return `/preview/article/${item.id}`; + return null; + }; + + const getTitle = (item: any) => { + return item.title || item.name || item.headline || item.slug || item.id; + }; + + const getStatus = (item: any) => { + return item.status || item.is_published ? 'published' : 'draft'; + }; + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+
+
+ ))} +
+ ); + } + + if (items.length === 0) { + return ( +
+ No {collection} found +
+ ); + } + + return ( +
+ {/* Header with select all */} +
+ + + {items.length} item(s) | {selectedIds.length} selected + +
+ + {/* Items */} + {items.map((item: any) => { + const isSelected = selectedIds.includes(item.id); + const previewUrl = getPreviewUrl(item); + const title = getTitle(item); + const status = getStatus(item); + + return ( +
+ + +
+
+ {title} +
+
+ + {status} + + {item.word_count && ( + {item.word_count} words + )} + {item.location_city && ( + 📍 {item.location_city} + )} +
+
+ + {previewUrl && ( + + + + )} +
+ ); + })} +
+ ); +} diff --git a/frontend/src/components/admin/god/GodModeCommandCenter.tsx b/frontend/src/components/admin/god/GodModeCommandCenter.tsx new file mode 100644 index 0000000..fd33aad --- /dev/null +++ b/frontend/src/components/admin/god/GodModeCommandCenter.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react'; +import { StatsPanel } from './StatsPanel'; +import { UnifiedSearchBar } from './UnifiedSearchBar'; +import { BulkActionsToolbar } from './BulkActionsToolbar'; +import { ContentTable } from './ContentTable'; +import { Database, FileText, File, Sparkles } from 'lucide-react'; + +type TabValue = 'sites' | 'pages' | 'posts' | 'articles'; + +const TABS = [ + { value: 'sites' as const, label: 'Sites', icon: Database, collection: 'sites' }, + { value: 'pages' as const, label: 'Pages', icon: FileText, collection: 'pages' }, + { value: 'posts' as const, label: 'Posts', icon: File, collection: 'posts' }, + { value: 'articles' as const, label: 'Articles', icon: Sparkles, collection: 'generated_articles' } +]; + +export function GodModeCommandCenter() { + const [activeTab, setActiveTab] = useState('sites'); + const [selectedIds, setSelectedIds] = useState([]); + const [searchResults, setSearchResults] = useState(undefined); + const [refreshKey, setRefreshKey] = useState(0); + + const activeCollection = TABS.find(t => t.value === activeTab)?.collection || 'sites'; + + const handleActionComplete = () => { + // Trigger refresh + setRefreshKey(prev => prev + 1); + setSelectedIds([]); + setSearchResults(undefined); + }; + + const handleSearch = (results: any[]) => { + setSearchResults(results.length > 0 ? results : undefined); + setSelectedIds([]); + }; + + return ( +
+ {/* Header */} +
+

+ 🔱 God Mode Command Center +

+

+ Unified control center for managing all sites, pages, posts, and generated articles +

+
+ + {/* Stats Panel */} + + + {/* Search Bar */} + + + {/* Tabs */} +
+ {TABS.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.value; + + return ( + + ); + })} +
+ + {/* Bulk Actions Toolbar */} + + + {/* Content Table */} + + + {/* Footer Info */} +
+
God Mode Features:
+
    +
  • • Search across all collections (sites, pages, posts, articles)
  • +
  • • Bulk publish, draft, archive, or delete items
  • +
  • • Direct database access via God Mode API
  • +
  • • Real-time stats and live preview links
  • +
+
+
+ ); +} diff --git a/frontend/src/components/admin/god/SchemaTestDashboard.tsx b/frontend/src/components/admin/god/SchemaTestDashboard.tsx new file mode 100644 index 0000000..832927a --- /dev/null +++ b/frontend/src/components/admin/god/SchemaTestDashboard.tsx @@ -0,0 +1,432 @@ +/** + * Schema Test Dashboard Component + * + * UI for running the God Mode build test and displaying results + */ + +import React, { useState } from 'react'; + +interface TestResults { + success: boolean; + steps: { + schemaValidation: boolean; + dataSeeding: boolean; + articleGeneration: boolean; + outputValidation: boolean; + }; + metrics: { + siteId?: string; + campaignId?: string; + templateId?: string; + articleId?: string; + wordCount?: number; + fragmentsCreated?: number; + previewUrl?: string; + }; + errors: string[]; + warnings: string[]; +} + +export function SchemaTestDashboard() { + const [running, setRunning] = useState(false); + const [results, setResults] = useState(null); + const [logs, setLogs] = useState([]); + + const runTest = async () => { + setRunning(true); + setLogs([]); + setResults(null); + + try { + setLogs(prev => [...prev, '🔷 Starting God Mode Build Test...']); + + const response = await fetch('/api/god/run-build-test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (!response.ok) { + throw new Error(`Test API failed: ${response.statusText}`); + } + + const data = await response.json(); + setResults(data); + + if (data.success) { + setLogs(prev => [...prev, '✅ BUILD TEST PASSED']); + } else { + setLogs(prev => [...prev, '❌ BUILD TEST FAILED']); + } + } catch (error: any) { + setLogs(prev => [...prev, `❌ Error: ${error.message}`]); + } finally { + setRunning(false); + } + }; + + return ( +
+
+

🔷 God Mode Schema Test

+

Validates database schema and tests complete 2000+ word article generation workflow

+
+ +
+ +
+ + {logs.length > 0 && ( +
+

📋 Test Logs

+
+ {logs.map((log, i) => ( +
{log}
+ ))} +
+
+ )} + + {results && ( +
+

{results.success ? '✅ Test Passed' : '❌ Test Failed'}

+ + {/* Steps Progress */} +
+
+ {results.steps.schemaValidation ? '✅' : '❌'} + Schema Validation +
+
+ {results.steps.dataSeeding ? '✅' : '❌'} + Data Seeding +
+
+ {results.steps.articleGeneration ? '✅' : '❌'} + Article Generation +
+
+ {results.steps.outputValidation ? '✅' : '❌'} + Output Validation +
+
+ + {/* Metrics */} + {results.metrics && Object.keys(results.metrics).length > 0 && ( +
+

📊 Test Metrics

+
+ {results.metrics.siteId && ( +
+ Site ID: + {results.metrics.siteId} +
+ )} + {results.metrics.campaignId && ( +
+ Campaign ID: + {results.metrics.campaignId} +
+ )} + {results.metrics.articleId && ( +
+ Article ID: + {results.metrics.articleId} +
+ )} + {results.metrics.wordCount && ( +
+ Word Count: + = 2000 ? 'success' : 'warning'}> + {results.metrics.wordCount} words + +
+ )} + {results.metrics.fragmentsCreated && ( +
+ Fragments Created: + {results.metrics.fragmentsCreated} +
+ )} +
+ + {results.metrics.previewUrl && ( + + )} +
+ )} + + {/* Errors */} + {results.errors.length > 0 && ( +
+

❌ Errors

+
    + {results.errors.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} + + {/* Warnings */} + {results.warnings.length > 0 && ( +
+

⚠️ Warnings

+
    + {results.warnings.map((warning, i) => ( +
  • {warning}
  • + ))} +
+
+ )} +
+ )} + + +
+ ); +} diff --git a/frontend/src/components/admin/god/StatsPanel.tsx b/frontend/src/components/admin/god/StatsPanel.tsx new file mode 100644 index 0000000..f7e3eb7 --- /dev/null +++ b/frontend/src/components/admin/god/StatsPanel.tsx @@ -0,0 +1,78 @@ +import { useQuery } from '@tanstack/react-query'; + +interface StatsData { + sites: number; + pages: number; + posts: number; + articles: number; +} + +export function StatsPanel() { + const { data, isLoading } = useQuery({ + queryKey: ['god-mode-stats'], + queryFn: async () => { + const response = await fetch('/api/god/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + collections: ['sites', 'pages', 'posts', 'generated_articles'], + limit: 1 + }) + }); + const result = await response.json(); + + // Count by collection + const stats: StatsData = { + sites: 0, + pages: 0, + posts: 0, + articles: 0 + }; + + if (result.results) { + result.results.forEach((item: any) => { + if (item._collection === 'sites') stats.sites++; + else if (item._collection === 'pages') stats.pages++; + else if (item._collection === 'posts') stats.posts++; + else if (item._collection === 'generated_articles') stats.articles++; + }); + } + + return stats; + }, + refetchInterval: 30000 // Refresh every 30 seconds + }); + + if (isLoading) { + return ( +
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+ ))} +
+ ); + } + + const stats = [ + { label: 'Sites', value: data?.sites || 0, color: 'text-blue-400' }, + { label: 'Pages', value: data?.pages || 0, color: 'text-green-400' }, + { label: 'Posts', value: data?.posts || 0, color: 'text-purple-400' }, + { label: 'Articles', value: data?.articles || 0, color: 'text-yellow-400' } + ]; + + return ( +
+ {stats.map((stat) => ( +
+
{stat.label}
+
+ {stat.value.toLocaleString()} +
+
+ ))} +
+ ); +} diff --git a/frontend/src/components/admin/god/UnifiedSearchBar.tsx b/frontend/src/components/admin/god/UnifiedSearchBar.tsx new file mode 100644 index 0000000..75c851e --- /dev/null +++ b/frontend/src/components/admin/god/UnifiedSearchBar.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react'; +import { Search } from 'lucide-react'; + +interface UnifiedSearchBarProps { + onSearch: (results: any[]) => void; + onLoading?: (loading: boolean) => void; +} + +export function UnifiedSearchBar({ onSearch, onLoading }: UnifiedSearchBarProps) { + const [query, setQuery] = useState(''); + const [isSearching, setIsSearching] = useState(false); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!query.trim()) { + // Empty search = show all + onSearch([]); + return; + } + + setIsSearching(true); + onLoading?.(true); + + try { + const response = await fetch('/api/god/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: query.trim(), + collections: ['sites', 'pages', 'posts', 'generated_articles'], + limit: 100 + }) + }); + + const result = await response.json(); + + if (result.success) { + onSearch(result.results || []); + } else { + console.error('Search failed:', result.error); + onSearch([]); + } + } catch (error) { + console.error('Search error:', error); + onSearch([]); + } finally { + setIsSearching(false); + onLoading?.(false); + } + }; + + return ( +
+
+ setQuery(e.target.value)} + placeholder="Search across all collections... (title, name, content, slug)" + className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-4 py-3 pl-12 text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600" + /> + + {isSearching && ( +
+
+
+ )} +
+
+ Searches: Sites, Pages, Posts, Generated Articles +
+
+ ); +} diff --git a/frontend/src/pages/admin/god-mode.astro b/frontend/src/pages/admin/god-mode.astro new file mode 100644 index 0000000..8b87eb8 --- /dev/null +++ b/frontend/src/pages/admin/god-mode.astro @@ -0,0 +1,8 @@ +--- +import AdminLayout from '@/layouts/AdminLayout.astro'; +import { GodModeCommandCenter } from '@/components/admin/god/GodModeCommandCenter'; +--- + + + + diff --git a/frontend/src/pages/api/god/bulk-actions.ts b/frontend/src/pages/api/god/bulk-actions.ts new file mode 100644 index 0000000..badacfc --- /dev/null +++ b/frontend/src/pages/api/god/bulk-actions.ts @@ -0,0 +1,138 @@ +import type { APIRoute } from 'astro'; +import { getDirectusClient, updateItem, deleteItem } from '@/lib/directus/client'; + +/** + * God Mode Bulk Actions + * + * Perform bulk operations on multiple items + */ +export const POST: APIRoute = async ({ request }) => { + try { + const { action, collection, ids, options } = await request.json(); + + if (!action || !collection || !ids || ids.length === 0) { + return new Response(JSON.stringify({ + error: 'action, collection, and ids are required' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + const directus = getDirectusClient(); + const results = { + success: 0, + failed: 0, + errors: [] as any[] + }; + + switch (action) { + case 'publish': + for (const id of ids) { + try { + await directus.request(updateItem(collection, id, { + status: 'published', + published_at: new Date().toISOString() + })); + results.success++; + } catch (error: any) { + results.failed++; + results.errors.push({ id, error: error.message }); + } + } + break; + + case 'unpublish': + case 'draft': + for (const id of ids) { + try { + await directus.request(updateItem(collection, id, { + status: 'draft' + })); + results.success++; + } catch (error: any) { + results.failed++; + results.errors.push({ id, error: error.message }); + } + } + break; + + case 'archive': + for (const id of ids) { + try { + await directus.request(updateItem(collection, id, { + status: 'archived' + })); + results.success++; + } catch (error: any) { + results.failed++; + results.errors.push({ id, error: error.message }); + } + } + break; + + case 'delete': + for (const id of ids) { + try { + await directus.request(deleteItem(collection, id)); + results.success++; + } catch (error: any) { + results.failed++; + results.errors.push({ id, error: error.message }); + } + } + break; + + case 'update': + // Custom update with fields from options + if (!options?.fields) { + return new Response(JSON.stringify({ + error: 'options.fields required for update action' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + for (const id of ids) { + try { + await directus.request(updateItem(collection, id, options.fields)); + results.success++; + } catch (error: any) { + results.failed++; + results.errors.push({ id, error: error.message }); + } + } + break; + + default: + return new Response(JSON.stringify({ + error: `Unknown action: ${action}` + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response(JSON.stringify({ + success: true, + action, + collection, + total: ids.length, + results + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error: any) { + console.error('Bulk action error:', error); + return new Response(JSON.stringify({ + success: false, + error: error.message + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; diff --git a/frontend/src/pages/api/god/search.ts b/frontend/src/pages/api/god/search.ts new file mode 100644 index 0000000..1a7b36c --- /dev/null +++ b/frontend/src/pages/api/god/search.ts @@ -0,0 +1,86 @@ +import type { APIRoute } from 'astro'; +import { getDirectusClient, readItems } from '@/lib/directus/client'; + +/** + * God Mode Unified Search + * + * Searches across multiple collections with filters + */ +export const POST: APIRoute = async ({ request }) => { + try { + const { query, collections, filters, limit = 100 } = await request.json(); + + if (!collections || collections.length === 0) { + return new Response(JSON.stringify({ + error: 'collections array is required' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + const directus = getDirectusClient(); + const results: any[] = []; + + for (const collection of collections) { + try { + // Build filter + const filter: any = {}; + + // Add text search if query provided + if (query) { + filter._or = [ + { title: { _contains: query } }, + { name: { _contains: query } }, + { headline: { _contains: query } }, + { content: { _contains: query } }, + { slug: { _contains: query } } + ]; + } + + // Merge additional filters + if (filters) { + Object.assign(filter, filters); + } + + const items = await directus.request(readItems(collection, { + filter: Object.keys(filter).length > 0 ? filter : undefined, + limit: Math.min(limit, 100), + fields: ['*'] + })); + + // Add collection name to each item + const itemsWithCollection = (items as any[]).map(item => ({ + ...item, + _collection: collection + })); + + results.push(...itemsWithCollection); + } catch (error) { + console.error(`Error searching ${collection}:`, error); + // Continue with other collections + } + } + + return new Response(JSON.stringify({ + success: true, + results, + total: results.length, + query, + collections + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error: any) { + console.error('Search error:', error); + return new Response(JSON.stringify({ + success: false, + error: error.message + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +};