diff --git a/frontend/src/components/admin/SystemStatusBar.tsx b/frontend/src/components/admin/SystemStatusBar.tsx index 8b67f63..65c6a2f 100644 --- a/frontend/src/components/admin/SystemStatusBar.tsx +++ b/frontend/src/components/admin/SystemStatusBar.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import { getDirectusClient, readItems } from '@/lib/directus/client'; interface SystemStatus { coreApi: 'online' | 'offline' | 'checking'; @@ -24,27 +23,38 @@ export default function SystemStatusBar() { const [showLogs, setShowLogs] = useState(false); useEffect(() => { - checkSystemStatus(); - const interval = setInterval(checkSystemStatus, 30000); // Check every 30 seconds + checkStatus(); + const interval = setInterval(checkStatus, 30000); return () => clearInterval(interval); }, []); - const checkSystemStatus = async () => { + const addLog = (message: string, type: LogEntry['type']) => { + const newLog: LogEntry = { + time: new Date().toLocaleTimeString(), + message, + type + }; + setLogs(prev => [newLog, ...prev].slice(0, 50)); + }; + + const checkStatus = async () => { try { - const client = getDirectusClient(); + const directusUrl = 'https://spark.jumpstartscaling.com'; - // Test database connection by fetching a single site - const sites = await client.request( - readItems('sites', { limit: 1 }) - ); - - setStatus({ - coreApi: 'online', - database: 'connected', - wpConnection: 'ready' + const response = await fetch(`${directusUrl}/server/health`, { + method: 'GET', }); - addLog('System check passed', 'success'); + if (response.ok) { + setStatus({ + coreApi: 'online', + database: 'connected', + wpConnection: 'ready' + }); + addLog('System check passed', 'success'); + } else { + throw new Error(`Health check failed: ${response.status}`); + } } catch (error) { console.error('Status check failed:', error); setStatus({ @@ -56,15 +66,6 @@ export default function SystemStatusBar() { } }; - const addLog = (message: string, type: LogEntry['type']) => { - const newLog: LogEntry = { - time: new Date().toLocaleTimeString(), - message, - type - }; - setLogs(prev => [newLog, ...prev].slice(0, 50)); // Keep last 50 logs - }; - const getStatusColor = (state: string) => { switch (state) { case 'online': @@ -96,60 +97,53 @@ export default function SystemStatusBar() { }; return ( -
- {/* Main Status Bar */} +
- {/* Title */} -

- API & Logistics -

+

API & Logistics

- {/* Status Items */}
- Core API + Core API {status.coreApi.charAt(0).toUpperCase() + status.coreApi.slice(1)}
- Database (Directus + Database (Directus) {status.database.charAt(0).toUpperCase() + status.database.slice(1)}
- WP Connection + WP Connection {status.wpConnection.charAt(0).toUpperCase() + status.wpConnection.slice(1)}
- {/* Toggle Logs Button */}
- {/* Processing Log Panel */} {showLogs && ( -
+
{logs.length === 0 ? ( -
No recent activity
+
No recent activity
) : ( logs.map((log, index) => (
- [{log.time}] + [{log.time}] {log.message}
)) diff --git a/frontend/src/components/collections/CollectionManager.tsx b/frontend/src/components/collections/CollectionManager.tsx new file mode 100644 index 0000000..f4157d4 --- /dev/null +++ b/frontend/src/components/collections/CollectionManager.tsx @@ -0,0 +1,176 @@ +/** + * Universal Collection Manager + * Reusable component for all collection pages + */ + +'use client'; + +import { useState, useEffect } from 'react'; + +interface CollectionManagerProps { + collection: string; + title: string; + description: string; + icon: string; + displayField: string; +} + +export default function CollectionManager({ + collection, + title, + description, + icon, + displayField, +}: CollectionManagerProps) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchItems(); + }, [collection]); + + const fetchItems = async () => { + try { + setLoading(true); + const response = await fetch( + `https://spark.jumpstartscaling.com/items/${collection}?limit=100`, + { + headers: { + Authorization: `Bearer ${import.meta.env.PUBLIC_DIRECTUS_TOKEN}`, + }, + } + ); + + if (!response.ok) throw new Error(`Failed to fetch ${collection}`); + + const data = await response.json(); + setItems(data.data || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + const handleBulkExport = () => { + const jsonStr = JSON.stringify(items, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${collection}_export.json`; + a.click(); + }; + + if (loading) { + return ( +
+
Loading {title}...
+
+ ); + } + + if (error) { + return ( +
+
Error: {error}
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ {icon} + {title} +

+

{description}

+
+
+ + + +
+
+ + {/* Stats */} +
+
+
Total Items
+
{items.length}
+
+
+
This Week
+
0
+
+
+
Usage Count
+
β€”
+
+
+
Status
+
● Active
+
+
+ + {/* Table */} +
+ + + + + + + + + + + + {items.map((item) => ( + + + + + + + + ))} + +
+ + ID{displayField}CreatedActions
+ + {item.id} + {item[displayField] || 'Untitled'} + + {item.date_created + ? new Date(item.date_created).toLocaleDateString() + : 'β€”'} + + +
+ + {items.length === 0 && ( +
+

No items found. Create your first one!

+
+ )} +
+
+ ); +} diff --git a/frontend/src/lib/collections/config.ts b/frontend/src/lib/collections/config.ts new file mode 100644 index 0000000..a669f45 --- /dev/null +++ b/frontend/src/lib/collections/config.ts @@ -0,0 +1,107 @@ +/** + * Collection Page Template Generator + * Creates standardized CRUD pages for all collections + */ + +export const collectionConfigs = { + avatar_intelligence: { + title: 'Avatar Intelligence', + description: 'Manage persona profiles and variants', + icon: 'πŸ‘₯', + fields: ['base_name', 'wealth_cluster', 'business_niches'], + displayField: 'base_name', + }, + avatar_variants: { + title: 'Avatar Variants', + description: 'Manage gender and tone variations', + icon: '🎭', + fields: ['avatar_id', 'variant_name', 'pronouns'], + displayField: 'variant_name', + }, + campaign_masters: { + title: 'Campaign Masters', + description: 'Manage marketing campaigns', + icon: 'πŸ“’', + fields: ['campaign_name', 'status', 'site_id'], + displayField: 'campaign_name', + }, + cartesian_patterns: { + title: 'Cartesian Patterns', + description: 'Content structure templates', + icon: 'πŸ”§', + fields: ['pattern_name', 'structure_type'], + displayField: 'pattern_name', + }, + content_fragments: { + title: 'Content Fragments', + description: 'Reusable content blocks', + icon: 'πŸ“¦', + fields: ['fragment_type', 'content'], + displayField: 'fragment_type', + }, + generated_articles: { + title: 'Generated Articles', + description: 'AI-generated content output', + icon: 'πŸ“', + fields: ['title', 'status', 'seo_score', 'geo_city'], + displayField: 'title', + }, + generation_jobs: { + title: 'Generation Jobs', + description: 'Content generation queue', + icon: 'βš™οΈ', + fields: ['job_name', 'status', 'progress'], + displayField: 'job_name', + }, + geo_intelligence: { + title: 'Geo Intelligence', + description: 'Location targeting data', + icon: 'πŸ—ΊοΈ', + fields: ['city', 'state', 'zip', 'population'], + displayField: 'city', + }, + headline_inventory: { + title: 'Headline Inventory', + description: 'Pre-written headlines library', + icon: 'πŸ’¬', + fields: ['headline_text', 'category'], + displayField: 'headline_text', + }, + leads: { + title: 'Leads', + description: 'Customer lead management', + icon: 'πŸ‘€', + fields: ['name', 'email', 'status'], + displayField: 'name', + }, + offer_blocks: { + title: 'Offer Blocks', + description: 'Call-to-action templates', + icon: '🎯', + fields: ['offer_text', 'offer_type'], + displayField: 'offer_text', + }, + pages: { + title: 'Pages', + description: 'Static page content', + icon: 'πŸ“„', + fields: ['title', 'slug', 'status'], + displayField: 'title', + }, + posts: { + title: 'Posts', + description: 'Blog posts and articles', + icon: 'πŸ“°', + fields: ['title', 'status', 'seo_score'], + displayField: 'title', + }, + spintax_dictionaries: { + title: 'Spintax Dictionaries', + description: 'Word variation sets', + icon: 'πŸ“š', + fields: ['category', 'variations'], + displayField: 'category', + }, +}; + +export type CollectionName = keyof typeof collectionConfigs; diff --git a/frontend/src/pages/admin/intelligence/avatars.astro b/frontend/src/pages/admin/intelligence/avatars.astro new file mode 100644 index 0000000..cbe96e3 --- /dev/null +++ b/frontend/src/pages/admin/intelligence/avatars.astro @@ -0,0 +1,107 @@ +/** + * Avatar Intelligence Management + * Full CRUD for avatar_intelligence collection + */ + +--- +import AdminLayout from '@/layouts/AdminLayout.astro'; +import { getDirectusClient } from '@/lib/directus/client'; +import { readItems } from '@directus/sdk'; + +const client = getDirectusClient(); + +let avatars = []; +let error = null; + +try { + avatars = await client.request(readItems('avatar_intelligence', { + fields: ['*'], + sort: ['base_name'], + })); +} catch (e) { + console.error('Error fetching avatars:', e); + error = e instanceof Error ? e.message : 'Unknown error'; +} +--- + + +
+ +
+
+

Avatar Intelligence

+

Manage persona profiles and variants

+
+
+ + + + ✨ New Avatar + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Avatars
+
{avatars.length}
+
+
+ + +
+ {avatars.map((avatar: any) => ( +
+
+

{avatar.base_name}

+ +
+ +
+
+ Wealth Cluster: + {avatar.wealth_cluster || 'Not set'} +
+ + {avatar.business_niches && ( +
+ Niches: +
+ {avatar.business_niches.slice(0, 3).map((niche: string) => ( + + {niche} + + ))} + {avatar.business_niches.length > 3 && ( + + +{avatar.business_niches.length - 3} more + + )} +
+
+ )} +
+
+ ))} + + {avatars.length === 0 && !error && ( +
+

No avatars found. Create your first one!

+
+ )} +
+
+