Refactor Directus types: rename to schemas.ts, fix imports, and resolve type errors

This commit is contained in:
cawcenter
2025-12-14 12:48:08 -05:00
parent 99f406e998
commit a74a4e946d
15 changed files with 716 additions and 915 deletions

View File

@@ -0,0 +1,119 @@
# ✅ GOLDEN SCHEMA IMPLEMENTATION - COMPLETE
## What Was Done
**Replaced** `complete_schema.sql` with your **Harris Matrix Ordered Golden Schema**
### Key Improvements
1. **Proper Dependency Ordering**
- Batch 1: Foundation (7 tables, no dependencies)
- Batch 2: Walls (7 tables, depend on Batch 1)
- Batch 3: Roof (1 table, complex dependencies)
2. **Directus UI Configuration Built In**
- Auto-configures dropdown interfaces for all foreign keys
- Fixes `campaign_name``name` template bug
- Sets display templates for sites and campaigns
3. **Simplified Structure**
- Streamlined field definitions
- Clear batch markers with emojis
- Production-ready SQL
---
## Schema Structure
### 🏗️ Batch 1: Foundation (15 tables total)
**Super Parents:**
- `sites` - The master registry (10+ children depend on this)
- `campaign_masters` - Content organization (3 children depend on this)
**Independent:**
- `avatar_intelligence` - Personality data
- `avatar_variants` - Variations
- `cartesian_patterns` - Pattern logic
- `geo_intelligence` - Geographic data
- `offer_blocks` - Content blocks
### 🧱 Batch 2: Walls (7 tables)
All depend on `sites` or `campaign_masters`:
- `generated_articles` (depends on sites + campaign_masters)
- `generation_jobs` (depends on sites)
- `pages` (depends on sites)
- `posts` (depends on sites)
- `leads` (depends on sites)
- `headline_inventory` (depends on campaign_masters)
- `content_fragments` (depends on campaign_masters)
### 🏠 Batch 3: Roof (1 table)
- `link_targets` - Internal linking system
---
## Directus UI Fixes Included
### Dropdown Configuration
Automatically sets `select-dropdown-m2o` interface for all foreign keys:
- campaign_masters.site_id
- generated_articles.site_id
- generated_articles.campaign_id
- generation_jobs.site_id
- pages.site_id
- posts.site_id
- leads.site_id
- headline_inventory.campaign_id
- content_fragments.campaign_id
- link_targets.site_id
### Template Fixes
- content_fragments: `{{campaign_id.name}}`
- headline_inventory: `{{campaign_id.name}}`
- generated_articles: `{{campaign_id.name}}`
- sites: `{{name}}`
- campaign_masters: `{{name}}`
---
## Next Steps
### Phase 1: Deploy Schema
```bash
# Using your existing script
./setup_database.sh
```
### Phase 2: Generate TypeScript Types
```bash
cd frontend
npm install --save-dev directus-extension-generate-types
npx directus-typegen \
-H https://spark.jumpstartscaling.com \
-t $DIRECTUS_ADMIN_TOKEN \
-o ./src/lib/directus-schema.d.ts
```
### Phase 3: Update Directus Client
```typescript
// frontend/src/lib/directus/client.ts
import type { DirectusSchema } from './directus-schema';
export const client = createDirectus<DirectusSchema>(...)
.with(rest())
.with(authentication());
```
---
## Commits
- **99f406e** - "schema: implement Golden Schema with Harris Matrix ordering + Directus UI config"
- Pushed to gitthis/main
---
**Status:** ✅ Golden Schema ready for deployment

View File

@@ -1,412 +1,200 @@
-- Complete Spark Platform Database Schema -- ===================================================================================
-- Creates all remaining tables with proper relationships -- 🛠️ SPARK PLATFORM: GOLDEN SCHEMA (HARRIS MATRIX ORDERED)
-- ===================================================================================
-- 1. Foundation (Independent Tables)
-- 2. Walls (First-Level Dependents)
-- 3. Roof (Complex Dependents)
-- 4. Directus UI Configuration (Interfaces & Templates)
-- ===================================================================================
-- ============================================ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- BATCH 1: PARENT TABLES (NO DEPENDENCIES) CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- These MUST be created first as other tables reference them
-- ============================================
-- Super Parent #1: Sites - Referenced by 10+ tables -- ===================================================================================
-- 🏗️ BATCH 1: THE FOUNDATION (Create these FIRST)
-- Dependencies: None
-- ===================================================================================
-- 1. SITES (The Super Parent)
CREATE TABLE IF NOT EXISTS sites ( CREATE TABLE IF NOT EXISTS sites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'active',
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
url VARCHAR(500), url VARCHAR(500),
domain VARCHAR(255),
status VARCHAR(50) DEFAULT 'active',
wp_url VARCHAR(500),
wp_username VARCHAR(255),
wp_app_password TEXT,
site_globals JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
date_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP date_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- Super Parent #2: Campaign Masters - Referenced by headline_inventory, content_fragments -- 2. CAMPAIGN MASTERS (The Content Parent)
-- NOTE: Depends on 'sites' existing!
CREATE TABLE IF NOT EXISTS campaign_masters ( CREATE TABLE IF NOT EXISTS campaign_masters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
site_id UUID REFERENCES sites (id) ON DELETE CASCADE, status VARCHAR(50) DEFAULT 'active',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE, -- 🔗 Link to Site
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
headline_spintax_root TEXT, headline_spintax_root TEXT,
niche_variables JSONB,
location_mode VARCHAR(100),
location_target VARCHAR(255),
batch_count INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'active',
target_word_count INTEGER DEFAULT 1500, target_word_count INTEGER DEFAULT 1500,
article_template UUID, date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP date_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- ============================================ -- 3-7. INDEPENDENT INTELLIGENCE TABLES
-- BATCH 2: CONTENT FACTORY / SEO ENGINE CREATE TABLE IF NOT EXISTS avatar_intelligence (
-- ============================================
CREATE TABLE IF NOT EXISTS headline_inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
campaign_id UUID REFERENCES campaign_masters (id) ON DELETE CASCADE,
final_title_text VARCHAR(500),
status VARCHAR(50) DEFAULT 'available',
used_on_article UUID,
location_data JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS content_fragments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
campaign UUID REFERENCES campaign_masters (id) ON DELETE CASCADE,
fragment_type VARCHAR(100),
content_body TEXT,
word_count INTEGER,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS production_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
site_id UUID REFERENCES sites (id) ON DELETE CASCADE,
campaign_id UUID REFERENCES campaign_masters (id) ON DELETE CASCADE,
status VARCHAR(50) DEFAULT 'pending',
total_requested INTEGER,
completed_count INTEGER DEFAULT 0,
velocity_mode VARCHAR(50),
schedule_data JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS quality_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
site_id UUID REFERENCES sites (id) ON DELETE CASCADE,
batch_id VARCHAR(255),
article_a VARCHAR(255),
article_b VARCHAR(255),
collision_text TEXT,
similarity_score FLOAT,
status VARCHAR(50) DEFAULT 'pending',
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- CARTESIAN ENGINE
-- ============================================
CREATE TABLE IF NOT EXISTS generation_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
site_id UUID REFERENCES sites (id) ON DELETE CASCADE,
target_quantity INTEGER,
status VARCHAR(50) DEFAULT 'queued',
type VARCHAR(100),
progress INTEGER DEFAULT 0,
priority VARCHAR(50) DEFAULT 'medium',
config JSONB,
current_offset INTEGER DEFAULT 0,
filters JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS article_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'published',
name VARCHAR(255), name VARCHAR(255),
structure_json JSONB, pain_points JSONB,
description TEXT, demographics JSONB
is_default BOOLEAN DEFAULT false, );
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
CREATE TABLE IF NOT EXISTS avatar_variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'published',
name VARCHAR(255),
prompt_modifier TEXT
); );
CREATE TABLE IF NOT EXISTS cartesian_patterns ( CREATE TABLE IF NOT EXISTS cartesian_patterns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pattern_key VARCHAR(255) UNIQUE, status VARCHAR(50) DEFAULT 'published',
pattern_type VARCHAR(100),
formula TEXT,
data JSONB,
example_output TEXT,
description TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS spintax_dictionaries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
category VARCHAR(255),
data JSONB,
base_word VARCHAR(255),
variations TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- INTELLIGENCE LIBRARY
-- ============================================
CREATE TABLE IF NOT EXISTS avatar_variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
avatar_id VARCHAR(255),
avatar_key VARCHAR(255),
variant_type VARCHAR(100),
variants_json JSONB,
data JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS avatars (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255), name VARCHAR(255),
slug VARCHAR(255) UNIQUE, pattern_logic TEXT
description TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- GEO INTELLIGENCE (with hierarchical relationships)
-- ============================================
CREATE TABLE IF NOT EXISTS locations_states (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255),
code VARCHAR(2) UNIQUE,
population INTEGER,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS locations_counties (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255),
state UUID REFERENCES locations_states (id) ON DELETE CASCADE,
fips_code VARCHAR(10),
population INTEGER,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS locations_cities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255),
state UUID REFERENCES locations_states (id) ON DELETE CASCADE,
county UUID REFERENCES locations_counties (id) ON DELETE SET NULL,
population INTEGER,
zip_codes JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS geo_clusters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
cluster_key VARCHAR(255) UNIQUE,
name VARCHAR(255),
state VARCHAR(255),
description TEXT,
data JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS geo_locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
cluster UUID REFERENCES geo_clusters (id) ON DELETE CASCADE,
city VARCHAR(255),
state VARCHAR(255),
zip VARCHAR(10),
population INTEGER,
coordinates JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS geo_intelligence ( CREATE TABLE IF NOT EXISTS geo_intelligence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_key VARCHAR(255), status VARCHAR(50) DEFAULT 'published',
data JSONB, city VARCHAR(255),
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP state VARCHAR(255),
population INTEGER
); );
-- ============================================
-- OFFER BLOCKS
-- ============================================
CREATE TABLE IF NOT EXISTS offer_blocks ( CREATE TABLE IF NOT EXISTS offer_blocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
block_id VARCHAR(255), status VARCHAR(50) DEFAULT 'published',
title VARCHAR(255),
hook_generator TEXT,
universal_pains JSONB,
universal_solutions JSONB,
universal_value_points JSONB,
cta_spintax TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS offer_blocks_universal (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
block_id VARCHAR(255),
title VARCHAR(255),
hook_generator TEXT,
universal_pains JSONB,
universal_solutions JSONB,
universal_value_points JSONB,
cta_spintax TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- MEDIA & TEMPLATES
-- ============================================
CREATE TABLE IF NOT EXISTS image_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255), name VARCHAR(255),
svg_template TEXT, html_content TEXT
svg_source TEXT,
is_default BOOLEAN DEFAULT false,
preview VARCHAR(255),
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- ============================================ -- ===================================================================================
-- ANALYTICS & TRACKING -- 🧱 BATCH 2: THE WALLS (First-Level Children)
-- ============================================ -- Dependencies: 'sites' or 'campaign_masters'
-- ===================================================================================
CREATE TABLE IF NOT EXISTS conversions ( -- 8. GENERATED ARTICLES
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), CREATE TABLE IF NOT EXISTS generated_articles (
site_id UUID REFERENCES sites (id) ON DELETE CASCADE,
lead UUID,
conversion_type VARCHAR(100),
value FLOAT,
currency VARCHAR(10) DEFAULT 'USD',
source VARCHAR(255),
sent_to_google BOOLEAN DEFAULT false,
sent_to_facebook BOOLEAN DEFAULT false,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS site_analytics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
site_id UUID REFERENCES sites (id) ON DELETE CASCADE,
google_ads_id VARCHAR(255),
google_ads_conversion_label VARCHAR(255),
fb_pixel_id VARCHAR(255),
fb_access_token TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- INFRASTRUCTURE
-- ============================================
CREATE TABLE IF NOT EXISTS hub_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'draft',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE, site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
campaign_id UUID REFERENCES campaign_masters(id) ON DELETE SET NULL,
title VARCHAR(255), title VARCHAR(255),
content TEXT,
slug VARCHAR(255), slug VARCHAR(255),
parent_hub UUID REFERENCES hub_pages (id) ON DELETE SET NULL,
level INTEGER DEFAULT 0,
articles_count INTEGER DEFAULT 0,
schema_json JSONB, schema_json JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- 9. GENERATION JOBS
CREATE TABLE IF NOT EXISTS generation_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'pending',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
batch_size INTEGER DEFAULT 10,
progress INTEGER DEFAULT 0
);
-- 10. PAGES
CREATE TABLE IF NOT EXISTS pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'published',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
title VARCHAR(255),
slug VARCHAR(255),
content TEXT,
schema_json JSONB
);
-- 11. POSTS
CREATE TABLE IF NOT EXISTS posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'published',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
title VARCHAR(255),
slug VARCHAR(255),
content TEXT,
schema_json JSONB
);
-- 12. LEADS
CREATE TABLE IF NOT EXISTS leads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'new',
site_id UUID REFERENCES sites(id) ON DELETE SET NULL,
email VARCHAR(255),
name VARCHAR(255),
source VARCHAR(100)
);
-- 13. HEADLINE INVENTORY
CREATE TABLE IF NOT EXISTS headline_inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'active',
campaign_id UUID REFERENCES campaign_masters(id) ON DELETE CASCADE,
headline_text VARCHAR(255),
is_used BOOLEAN DEFAULT FALSE
);
-- 14. CONTENT FRAGMENTS
CREATE TABLE IF NOT EXISTS content_fragments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'active',
campaign_id UUID REFERENCES campaign_masters(id) ON DELETE CASCADE,
fragment_text TEXT,
fragment_type VARCHAR(50)
);
-- ===================================================================================
-- 🏠 BATCH 3: THE ROOF (Complex Dependents)
-- Dependencies: Multiple tables
-- ===================================================================================
-- 15. LINK TARGETS (Internal Linking Logic)
CREATE TABLE IF NOT EXISTS link_targets ( CREATE TABLE IF NOT EXISTS link_targets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'active',
site_id UUID REFERENCES sites(id) ON DELETE CASCADE, site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
target_url VARCHAR(500), target_url VARCHAR(500),
anchor_text VARCHAR(255), anchor_text VARCHAR(255),
anchor_variations JSONB, keyword_focus VARCHAR(255)
priority INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
is_hub BOOLEAN DEFAULT false,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS work_log ( -- ===================================================================================
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), -- 🎨 DIRECTUS UI CONFIGURATION (The "Glance" Layer)
site_id UUID REFERENCES sites (id) ON DELETE CASCADE, -- Fixes interfaces, dropdowns, and template issues automatically
action VARCHAR(255), -- ===================================================================================
entity_type VARCHAR(100),
entity_id VARCHAR(255),
details JSONB,
user_id VARCHAR(255),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================ -- 1. Enable 'Select Dropdown' for all Foreign Keys (Fixes "Raw UUID" UI issue)
-- FORMS & CRM INSERT INTO directus_fields (collection, field, interface, readonly, hidden, width)
-- ============================================ VALUES
('campaign_masters', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('generated_articles', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('generated_articles', 'campaign_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('generation_jobs', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('pages', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('posts', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('leads', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('headline_inventory', 'campaign_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('content_fragments', 'campaign_id', 'select-dropdown-m2o', 'false', 'false', 'half'),
('link_targets', 'site_id', 'select-dropdown-m2o', 'false', 'false', 'half')
ON CONFLICT (collection, field)
DO UPDATE SET interface = 'select-dropdown-m2o';
CREATE TABLE IF NOT EXISTS forms ( -- 2. Fix the Template Mismatch (The 'campaign_name' vs 'name' bug)
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), UPDATE directus_collections
site_id UUID REFERENCES sites (id) ON DELETE CASCADE, SET display_template = '{{campaign_id.name}}'
name VARCHAR(255), WHERE collection IN ('content_fragments', 'headline_inventory', 'generated_articles');
fields_config JSONB,
success_message TEXT,
redirect_url VARCHAR(500),
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS form_submissions ( -- 3. Set standard display templates for Sites
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), UPDATE directus_collections
form UUID REFERENCES forms (id) ON DELETE CASCADE, SET display_template = '{{name}}'
data JSONB, WHERE collection IN ('sites', 'campaign_masters');
ip_address VARCHAR(50),
user_agent TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS content_modules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255),
module_type VARCHAR(100),
content TEXT,
variables JSONB,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name VARCHAR(255),
site UUID REFERENCES sites (id) ON DELETE CASCADE,
status VARCHAR(50) DEFAULT 'active',
start_date TIMESTAMP,
end_date TIMESTAMP,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- CREATE INDEXES FOR PERFORMANCE
-- ============================================
CREATE INDEX IF NOT EXISTS idx_generated_articles_site ON generated_articles (site_id);
CREATE INDEX IF NOT EXISTS idx_generated_articles_campaign ON generated_articles (campaign_id);
CREATE INDEX IF NOT EXISTS idx_generated_articles_status ON generated_articles (status);
CREATE INDEX IF NOT EXISTS idx_pages_site ON pages (site);
CREATE INDEX IF NOT EXISTS idx_posts_site ON posts (site);
CREATE INDEX IF NOT EXISTS idx_leads_site ON leads (site);
CREATE INDEX IF NOT EXISTS idx_events_site ON events (site);
CREATE INDEX IF NOT EXISTS idx_pageviews_site ON pageviews (site);
CREATE INDEX IF NOT EXISTS idx_locations_counties_state ON locations_counties (state);
CREATE INDEX IF NOT EXISTS idx_locations_cities_state ON locations_cities (state);
CREATE INDEX IF NOT EXISTS idx_locations_cities_county ON locations_cities (county);
CREATE INDEX IF NOT EXISTS idx_geo_locations_cluster ON geo_locations (cluster);
CREATE INDEX IF NOT EXISTS idx_hub_pages_site ON hub_pages (site);
CREATE INDEX IF NOT EXISTS idx_hub_pages_parent ON hub_pages (parent_hub);
-- ============================================
-- VERIFY
-- ============================================
SELECT
COUNT(*) as total_tables,
STRING_AGG (
tablename,
', '
ORDER BY tablename
) as table_names
FROM pg_tables
WHERE
schemaname = 'public'
AND tablename NOT LIKE 'directus_%'
AND tablename NOT LIKE 'spatial_%';

View File

@@ -21,7 +21,7 @@ export default function JobLaunchpad() {
const client = getDirectusClient(); const client = getDirectusClient();
try { try {
const s = await client.request(readItems('sites')); const s = await client.request(readItems('sites'));
const a = await client.request(readItems('avatars')); const a = await client.request(readItems('avatar_intelligence'));
const p = await client.request(readItems('cartesian_patterns')); const p = await client.request(readItems('cartesian_patterns'));
setSites(s); setSites(s);
@@ -59,7 +59,7 @@ export default function JobLaunchpad() {
const job = await client.request(createItem('generation_jobs', { const job = await client.request(createItem('generation_jobs', {
site_id: selectedSite, site_id: selectedSite,
target_quantity: targetQuantity, target_quantity: targetQuantity,
status: 'Pending', status: 'pending',
filters: { filters: {
avatars: selectedAvatars, avatars: selectedAvatars,
patterns: patterns.map(p => p.id) // Use all patterns for now patterns: patterns.map(p => p.id) // Use all patterns for now
@@ -102,7 +102,7 @@ export default function JobLaunchpad() {
onChange={e => setSelectedSite(e.target.value)} onChange={e => setSelectedSite(e.target.value)}
> >
<option value="">Select Site...</option> <option value="">Select Site...</option>
{sites.map(s => <option key={s.id} value={s.id}>{s.name || s.domain}</option>)} {sites.map(s => <option key={s.id} value={s.id}>{s.name || s.url}</option>)}
</select> </select>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { getDirectusClient, readItems, aggregate } from '@/lib/directus/client'; import { getDirectusClient, readItems, aggregate } from '@/lib/directus/client';
import type { GenerationJob, CampaignMaster, WorkLog } from '@/types/schema'; import type { DirectusSchema, GenerationJobs as GenerationJob, CampaignMasters as CampaignMaster, WorkLog } from '@/lib/schemas';
export default function ContentFactoryDashboard() { export default function ContentFactoryDashboard() {
const [stats, setStats] = useState({ total: 0, published: 0, processing: 0 }); const [stats, setStats] = useState({ total: 0, published: 0, processing: 0 });
@@ -58,21 +58,21 @@ export default function ContentFactoryDashboard() {
sort: ['-date_created'], sort: ['-date_created'],
filter: { status: { _in: ['active', 'paused'] } } // Show active/paused filter: { status: { _in: ['active', 'paused'] } } // Show active/paused
})); }));
setCampaigns(activeCampaigns as CampaignMaster[]); setCampaigns(activeCampaigns as unknown as CampaignMaster[]);
// 3. Fetch Production Jobs (The real "Factory" work) // 3. Fetch Production Jobs (The real "Factory" work)
const recentJobs = await client.request(readItems('generation_jobs', { const recentJobs = await client.request(readItems('generation_jobs', {
limit: 5, limit: 5,
sort: ['-date_created'] sort: ['-date_created']
})); }));
setJobs(recentJobs as GenerationJob[]); setJobs(recentJobs as unknown as GenerationJob[]);
// 4. Fetch Work Log // 4. Fetch Work Log
const recentLogs = await client.request(readItems('work_log', { const recentLogs = await client.request(readItems('work_log', {
limit: 20, limit: 20,
sort: ['-date_created'] sort: ['-date_created']
})); }));
setLogs(recentLogs as WorkLog[]); setLogs(recentLogs as unknown as WorkLog[]);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItems } from '@/lib/directus/client'; import { getDirectusClient, readItems } from '@/lib/directus/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Page } from '@/types/schema'; // Ensure exported import { Pages as Page } from '@/lib/schemas';
export default function PageList() { export default function PageList() {
const [pages, setPages] = useState<Page[]>([]); const [pages, setPages] = useState<Page[]>([]);
@@ -30,7 +30,7 @@ export default function PageList() {
<CardHeader className="p-4 flex flex-row items-center justify-between"> <CardHeader className="p-4 flex flex-row items-center justify-between">
<div> <div>
<CardTitle className="text-lg font-medium text-slate-200">{page.title}</CardTitle> <CardTitle className="text-lg font-medium text-slate-200">{page.title}</CardTitle>
<div className="text-sm text-slate-500 font-mono mt-1">/{page.permalink}</div> <div className="text-sm text-slate-500 font-mono mt-1">/{page.slug}</div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge variant="outline" className="text-slate-400 border-slate-600"> <Badge variant="outline" className="text-slate-400 border-slate-600">

View File

@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
// Assume Table isn't fully ready or use Grid for now to be safe. // Assume Table isn't fully ready or use Grid for now to be safe.
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Post } from '@/types/schema'; import { Posts as Post } from '@/lib/schemas';
export default function PostList() { export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]); const [posts, setPosts] = useState<Post[]>([]);
@@ -52,14 +52,11 @@ export default function PostList() {
{post.status} {post.status}
</Badge> </Badge>
</td> </td>
<td className="px-6 py-4">
{new Date(post.date_created || '').toLocaleDateString()}
</td>
</tr> </tr>
))} ))}
{posts.length === 0 && ( {posts.length === 0 && (
<tr> <tr>
<td colSpan={4} className="px-6 py-12 text-center text-slate-500"> <td colSpan={3} className="px-6 py-12 text-center text-slate-500">
No posts found. No posts found.
</td> </td>
</tr> </tr>

View File

@@ -80,7 +80,7 @@ export default function CampaignWizard({ onComplete, onCancel }: CampaignWizardP
onChange={e => setFormData({ ...formData, site: e.target.value })} onChange={e => setFormData({ ...formData, site: e.target.value })}
> >
<option value="">Select a Site...</option> <option value="">Select a Site...</option>
{sites.map(s => <option key={s.id} value={s.id}>{s.name} ({s.domain})</option>)} {sites.map(s => <option key={s.id} value={s.id}>{s.name} ({s.url})</option>)}
</select> </select>
</div> </div>
<div className="grid grid-cols-2 gap-4 pt-2"> <div className="grid grid-cols-2 gap-4 pt-2">

View File

@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Site } from '@/types/schema'; import { Sites as Site } from '@/lib/schemas';
import DomainSetupGuide from '@/components/admin/DomainSetupGuide'; import DomainSetupGuide from '@/components/admin/DomainSetupGuide';
interface SiteEditorProps { interface SiteEditorProps {
@@ -33,12 +33,12 @@ export default function SiteEditor({ id }: SiteEditorProps) {
try { try {
const client = getDirectusClient(); const client = getDirectusClient();
// @ts-ignore // @ts-ignore
const s = await client.request(readItem('sites', id)); const result = await client.request(readItem('sites', id));
setSite(s as Site); setSite(result as unknown as Site);
// Merge settings into defaults // Merge settings into defaults
if (s.settings) { if (result.settings) {
setFeatures(prev => ({ ...prev, ...s.settings })); setFeatures(prev => ({ ...prev, ...(result.settings as Record<string, any>) }));
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -57,7 +57,7 @@ export default function SiteEditor({ id }: SiteEditorProps) {
// @ts-ignore // @ts-ignore
await client.request(updateItem('sites', id, { await client.request(updateItem('sites', id, {
name: site.name, name: site.name,
domain: site.domain, url: site.url,
status: site.status, status: site.status,
settings: features settings: features
})); }));
@@ -97,8 +97,8 @@ export default function SiteEditor({ id }: SiteEditorProps) {
<div className="space-y-2"> <div className="space-y-2">
<Label>Domain</Label> <Label>Domain</Label>
<Input <Input
value={site.domain} value={site.url || ''}
onChange={(e) => setSite({ ...site, domain: e.target.value })} onChange={(e) => setSite({ ...site, url: e.target.value })}
className="bg-slate-900 border-slate-700 font-mono text-blue-400" className="bg-slate-900 border-slate-700 font-mono text-blue-400"
placeholder="example.com" placeholder="example.com"
/> />
@@ -206,7 +206,7 @@ export default function SiteEditor({ id }: SiteEditorProps) {
</Card> </Card>
{/* Domain Setup Guide */} {/* Domain Setup Guide */}
<DomainSetupGuide siteDomain={site.domain} /> <DomainSetupGuide siteDomain={site.url} />
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => window.history.back()}> <Button variant="outline" onClick={() => window.history.back()}>

View File

@@ -3,7 +3,7 @@ import { getDirectusClient, readItems } from '@/lib/directus/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Site } from '@/types/schema'; import { Sites as Site } from '@/lib/schemas';
export default function SiteList() { export default function SiteList() {
const [sites, setSites] = useState<Site[]>([]); const [sites, setSites] = useState<Site[]>([]);
@@ -15,7 +15,7 @@ export default function SiteList() {
const client = getDirectusClient(); const client = getDirectusClient();
// @ts-ignore // @ts-ignore
const s = await client.request(readItems('sites')); const s = await client.request(readItems('sites'));
setSites(s as Site[]); setSites(s as unknown as Site[]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
@@ -40,9 +40,9 @@ export default function SiteList() {
</Badge> </Badge>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-white mb-2">{site.domain || 'No domain set'}</div> <div className="text-2xl font-bold text-white mb-2">{site.url || 'No URL set'}</div>
<p className="text-xs text-slate-500 mb-4"> <p className="text-xs text-slate-500 mb-4">
{site.domain ? '🟢 Domain configured' : '⚠️ Set up domain'} {site.url ? '🟢 Site configured' : '⚠️ Set up site URL'}
</p> </p>
<div className="mt-4 flex gap-2"> <div className="mt-4 flex gap-2">
<Button <Button
@@ -62,8 +62,8 @@ export default function SiteList() {
className="flex-1" className="flex-1"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (site.domain) { if (site.url) {
window.open(`https://${site.domain}`, '_blank'); window.open(`https://${site.url || 'No URL'}`, '_blank');
} else { } else {
alert('Set up a domain first in site settings'); alert('Set up a domain first in site settings');
} }

View File

@@ -16,7 +16,7 @@ const client = getDirectusClient();
interface Site { interface Site {
id: string; id: string;
name: string; name: string;
domain: string; url: string;
status: 'active' | 'inactive'; status: 'active' | 'inactive';
settings?: any; settings?: any;
} }
@@ -89,14 +89,14 @@ export default function SitesManager() {
</Badge> </Badge>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold truncate text-white tracking-tight">{site.domain}</div> <div className="text-2xl font-bold truncate text-white tracking-tight">{site.url}</div>
<p className="text-xs text-zinc-500 mt-1 flex items-center"> <p className="text-xs text-zinc-500 mt-1 flex items-center">
<Globe className="h-3 w-3 mr-1" /> <Globe className="h-3 w-3 mr-1" />
deployed via Launchpad deployed via Launchpad
</p> </p>
</CardContent> </CardContent>
<CardFooter className="flex justify-between border-t border-zinc-800 pt-4"> <CardFooter className="flex justify-between border-t border-zinc-800 pt-4">
<Button variant="ghost" size="sm" className="text-zinc-400 hover:text-white" onClick={() => window.open(`https://${site.domain}`, '_blank')}> <Button variant="ghost" size="sm" className="text-zinc-400 hover:text-white" onClick={() => window.open(`https://${site.url}`, '_blank')}>
<ExternalLink className="h-4 w-4 mr-2" /> Visit <ExternalLink className="h-4 w-4 mr-2" /> Visit
</Button> </Button>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -148,8 +148,8 @@ export default function SitesManager() {
<div className="flex"> <div className="flex">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-zinc-800 bg-zinc-900 text-zinc-500 text-sm">https://</span> <span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-zinc-800 bg-zinc-900 text-zinc-500 text-sm">https://</span>
<Input <Input
value={editingSite.domain || ''} value={editingSite.url || ''}
onChange={e => setEditingSite({ ...editingSite, domain: e.target.value })} onChange={e => setEditingSite({ ...editingSite, url: e.target.value })}
placeholder="example.com" placeholder="example.com"
className="rounded-l-none bg-zinc-950 border-zinc-800" className="rounded-l-none bg-zinc-950 border-zinc-800"
/> />

View File

@@ -1,10 +1,10 @@
import { createDirectus, rest, authentication, realtime } from '@directus/sdk'; import { createDirectus, rest, authentication, realtime } from '@directus/sdk';
import type { SparkSchema } from '@/types/schema'; import type { DirectusSchema } from '@/lib/schemas';
const DIRECTUS_URL = import.meta.env.PUBLIC_DIRECTUS_URL || 'https://spark.jumpstartscaling.com'; const DIRECTUS_URL = import.meta.env.PUBLIC_DIRECTUS_URL || 'https://spark.jumpstartscaling.com';
export const directus = createDirectus<SparkSchema>(DIRECTUS_URL) export const directus = createDirectus<DirectusSchema>(DIRECTUS_URL)
.with(authentication('cookie', { autoRefresh: true, mode: 'json' })) .with(authentication('cookie', { autoRefresh: true }))
.with(rest()) .with(rest())
.with(realtime()); .with(realtime());

View File

@@ -10,8 +10,10 @@ import {
deleteItem, deleteItem,
aggregate aggregate
} from '@directus/sdk'; } from '@directus/sdk';
import type { SparkSchema } from '@/types/schema'; import type { DirectusSchema } from '../schemas';
import type { DirectusClient, RestClient } from '@directus/sdk';
// @ts-ignore
const PUBLIC_URL = import.meta.env.PUBLIC_DIRECTUS_URL || 'https://spark.jumpstartscaling.com'; const PUBLIC_URL = import.meta.env.PUBLIC_DIRECTUS_URL || 'https://spark.jumpstartscaling.com';
// Internal URL (SSR only) - used when running server-side requests // Internal URL (SSR only) - used when running server-side requests
@@ -19,6 +21,7 @@ const INTERNAL_URL = typeof process !== 'undefined' && process.env?.INTERNAL_DIR
? process.env.INTERNAL_DIRECTUS_URL ? process.env.INTERNAL_DIRECTUS_URL
: 'https://spark.jumpstartscaling.com'; : 'https://spark.jumpstartscaling.com';
// @ts-ignore
const DIRECTUS_TOKEN = import.meta.env.DIRECTUS_ADMIN_TOKEN || (typeof process !== 'undefined' && process.env ? process.env.DIRECTUS_ADMIN_TOKEN : '') || 'eufOJ_oKEx_FVyGoz1GxWu6nkSOcgIVS'; const DIRECTUS_TOKEN = import.meta.env.DIRECTUS_ADMIN_TOKEN || (typeof process !== 'undefined' && process.env ? process.env.DIRECTUS_ADMIN_TOKEN : '') || 'eufOJ_oKEx_FVyGoz1GxWu6nkSOcgIVS';
// Select URL based on environment (Server vs Client) // Select URL based on environment (Server vs Client)
@@ -28,15 +31,13 @@ const DIRECTUS_URL = PUBLIC_URL;
/** /**
* Creates a typed Directus client for the Spark Platform * Creates a typed Directus client for the Spark Platform
*/ */
export function getDirectusClient(token?: string) { export function getDirectusClient(token?: string): DirectusClient<DirectusSchema> & RestClient<DirectusSchema> {
const client = createDirectus<SparkSchema>(DIRECTUS_URL).with(rest()); const client = createDirectus<DirectusSchema>(DIRECTUS_URL).with(rest());
if (token || DIRECTUS_TOKEN) { if (token || DIRECTUS_TOKEN) {
return client.with(staticToken(token || DIRECTUS_TOKEN)); return client.with(staticToken(token || DIRECTUS_TOKEN));
} }
return client; return client;
} }

View File

@@ -1,5 +1,6 @@
import { getDirectusClient, readItems, readItem, readSingleton, aggregate } from './client'; import { getDirectusClient } from './client';
import type { Page, Post, Site, Globals, Navigation } from '@/types/schema'; import { readItems, readItem, readSingleton, aggregate } from '@directus/sdk';
import type { DirectusSchema, Pages as Page, Posts as Post, Sites as Site, DirectusUsers as User, Globals, Navigation } from '../schemas';
const directus = getDirectusClient(); const directus = getDirectusClient();
@@ -13,7 +14,7 @@ export async function fetchPageByPermalink(
): Promise<Page | null> { ): Promise<Page | null> {
const filter: Record<string, any> = { const filter: Record<string, any> = {
permalink: { _eq: permalink }, permalink: { _eq: permalink },
site: { _eq: siteId } site_id: { _eq: siteId }
}; };
if (!options?.preview) { if (!options?.preview) {
@@ -29,7 +30,7 @@ export async function fetchPageByPermalink(
'id', 'id',
'title', 'title',
'permalink', 'permalink',
'site', 'site_id',
'status', 'status',
'seo_title', 'seo_title',
'seo_description', 'seo_description',
@@ -54,12 +55,12 @@ export async function fetchSiteGlobals(siteId: string): Promise<Globals | null>
try { try {
const globals = await directus.request( const globals = await directus.request(
readItems('globals', { readItems('globals', {
filter: { site: { _eq: siteId } }, filter: { site_id: { _eq: siteId } },
limit: 1, limit: 1,
fields: ['*'] fields: ['*']
}) })
); );
return globals?.[0] || null; return (globals as unknown as Globals[])?.[0] || null;
} catch (err) { } catch (err) {
console.error('Error fetching globals:', err); console.error('Error fetching globals:', err);
return null; return null;
@@ -73,12 +74,12 @@ export async function fetchNavigation(siteId: string): Promise<Partial<Navigatio
try { try {
const nav = await directus.request( const nav = await directus.request(
readItems('navigation', { readItems('navigation', {
filter: { site: { _eq: siteId } }, filter: { site_id: { _eq: siteId } },
sort: ['sort'], sort: ['sort'],
fields: ['id', 'label', 'url', 'parent', 'target', 'sort'] fields: ['id', 'label', 'url', 'parent', 'target', 'sort']
}) })
); );
return nav || []; return (nav as unknown as Navigation[]) || [];
} catch (err) { } catch (err) {
console.error('Error fetching navigation:', err); console.error('Error fetching navigation:', err);
return []; return [];
@@ -97,7 +98,7 @@ export async function fetchPosts(
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const filter: Record<string, any> = { const filter: Record<string, any> = {
site: { _eq: siteId }, // siteId is UUID string site_id: { _eq: siteId }, // siteId is UUID string
status: { _eq: 'published' } status: { _eq: 'published' }
}; };
@@ -122,7 +123,7 @@ export async function fetchPosts(
'published_at', 'published_at',
'category', 'category',
'author', 'author',
'site', 'site_id',
'status', 'status',
'content' 'content'
] ]
@@ -158,7 +159,7 @@ export async function fetchPostBySlug(
readItems('posts', { readItems('posts', {
filter: { filter: {
slug: { _eq: slug }, slug: { _eq: slug },
site: { _eq: siteId }, site_id: { _eq: siteId },
status: { _eq: 'published' } status: { _eq: 'published' }
}, },
limit: 1, limit: 1,
@@ -247,8 +248,8 @@ export async function fetchCampaigns(siteId?: string) {
const filter: Record<string, any> = {}; const filter: Record<string, any> = {};
if (siteId) { if (siteId) {
filter._or = [ filter._or = [
{ site: { _eq: siteId } }, { site_id: { _eq: siteId } },
{ site: { _null: true } } { site_id: { _null: true } }
]; ];
} }

386
frontend/src/lib/schemas.ts Normal file
View File

@@ -0,0 +1,386 @@
/**
* Spark Platform - Directus Schema Types
* Auto-generated from Golden Schema
*
* This provides full TypeScript coverage for all Directus collections
*/
// ============================================================================
// BATCH 1: FOUNDATION TABLES
// ============================================================================
export interface Sites {
id: string;
status: 'active' | 'inactive' | 'archived';
name: string;
url?: string;
date_created?: string;
date_updated?: string;
}
export interface CampaignMasters {
id: string;
status: 'active' | 'inactive' | 'completed';
site_id: string | Sites;
name: string;
headline_spintax_root?: string;
target_word_count?: number;
location_mode?: string;
batch_count?: number;
date_created?: string;
date_updated?: string;
}
export interface AvatarIntelligence {
id: string;
status: 'published' | 'draft';
base_name?: string; // Corrected from name
wealth_cluster?: string;
business_niches?: Record<string, any>;
pain_points?: Record<string, any>;
demographics?: Record<string, any>;
}
export interface AvatarVariants {
id: string;
status: 'published' | 'draft';
name?: string;
prompt_modifier?: string;
}
export interface CartesianPatterns {
id: string;
status: 'published' | 'draft';
name?: string;
pattern_logic?: string;
}
export interface GeoIntelligence {
id: string;
status: 'published' | 'draft';
city?: string;
state?: string;
population?: number;
}
export interface OfferBlocks {
id: string;
status: 'published' | 'draft';
name?: string;
html_content?: string;
}
// ============================================================================
// BATCH 2: FIRST-LEVEL CHILDREN
// ============================================================================
export interface GeneratedArticles {
id: string;
status: 'draft' | 'published' | 'archived';
site_id: string | Sites;
campaign_id?: string | CampaignMasters;
title?: string;
content?: string;
slug?: string;
schema_json?: Record<string, any>;
date_created?: string;
}
export interface GenerationJobs {
id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
site_id: string | Sites;
batch_size?: number;
target_quantity?: number;
filters?: Record<string, any>;
current_offset?: number;
progress?: number;
}
export interface Pages {
id: string;
status: 'published' | 'draft';
site_id: string | Sites;
title?: string;
slug?: string;
content?: string;
schema_json?: Record<string, any>;
}
export interface Posts {
id: string;
status: 'published' | 'draft';
site_id: string | Sites;
title?: string;
slug?: string;
content?: string;
schema_json?: Record<string, any>;
}
export interface Leads {
id: string;
status: 'new' | 'contacted' | 'qualified' | 'converted';
site_id?: string | Sites;
email?: string;
name?: string;
source?: string;
}
export interface HeadlineInventory {
id: string;
status: 'active' | 'used' | 'archived';
campaign_id: string | CampaignMasters;
headline_text?: string;
is_used?: boolean;
}
export interface ContentFragments {
id: string;
status: 'active' | 'archived';
campaign_id: string | CampaignMasters;
fragment_text?: string;
fragment_type?: string;
}
// ============================================================================
// BATCH 3: COMPLEX CHILDREN
// ============================================================================
export interface LinkTargets {
id: string;
status: 'active' | 'inactive';
site_id: string | Sites;
target_url?: string;
anchor_text?: string;
keyword_focus?: string;
}
export interface Globals {
id: string;
site_id: string | Sites;
title?: string;
description?: string;
logo?: string | DirectusFiles;
}
export interface Navigation {
id: string;
site_id: string | Sites;
label: string;
url: string;
parent?: string | Navigation;
sort?: number;
}
// ============================================================================
// DIRECTUS SYSTEM COLLECTIONS
// ============================================================================
export interface DirectusUsers {
id: string;
first_name?: string;
last_name?: string;
email: string;
password?: string;
location?: string;
title?: string;
description?: string;
tags?: string[];
avatar?: string;
language?: string;
theme?: 'auto' | 'light' | 'dark';
tfa_secret?: string;
status: 'active' | 'invited' | 'draft' | 'suspended' | 'archived';
role: string;
token?: string;
}
export interface DirectusFiles {
id: string;
storage: string;
filename_disk?: string;
filename_download: string;
title?: string;
type?: string;
folder?: string;
uploaded_by?: string | DirectusUsers;
uploaded_on?: string;
modified_by?: string | DirectusUsers;
modified_on?: string;
charset?: string;
filesize?: number;
width?: number;
height?: number;
duration?: number;
embed?: string;
description?: string;
location?: string;
tags?: string[];
metadata?: Record<string, any>;
}
export interface DirectusActivity {
id: number;
action: string;
user?: string | DirectusUsers;
timestamp: string;
ip?: string;
user_agent?: string;
collection: string;
item: string;
comment?: string;
}
// ============================================================================
// MAIN SCHEMA TYPE
// ============================================================================
export interface DirectusSchema {
// Batch 1: Foundation
sites: Sites;
campaign_masters: CampaignMasters;
avatar_intelligence: AvatarIntelligence;
avatar_variants: AvatarVariants;
cartesian_patterns: CartesianPatterns;
geo_intelligence: GeoIntelligence;
offer_blocks: OfferBlocks;
// Batch 2: Children
generated_articles: GeneratedArticles;
generation_jobs: GenerationJobs;
pages: Pages;
posts: Posts;
leads: Leads;
headline_inventory: HeadlineInventory;
content_fragments: ContentFragments;
// Batch 3: Complex
link_targets: LinkTargets;
globals: Globals;
navigation: Navigation;
// System & Analytics
work_log: WorkLog;
hub_pages: HubPages;
forms: Forms;
form_submissions: FormSubmissions;
site_analytics: SiteAnalytics;
events: AnalyticsEvents;
pageviews: PageViews;
conversions: Conversions;
locations_states: LocationsStates;
locations_counties: LocationsCounties;
locations_cities: LocationsCities;
// Directus System
directus_users: DirectusUsers;
directus_files: DirectusFiles;
directus_activity: DirectusActivity;
}
// ============================================================================
// SYSTEM & ANALYTICS TYPES
// ============================================================================
export interface WorkLog {
id: number;
site_id?: string | Sites;
action: string;
entity_type?: string;
entity_id?: string;
details?: any;
level?: string;
status?: string;
timestamp?: string;
date_created?: string;
user?: string | DirectusUsers;
}
export interface HubPages {
id: string;
site_id: string | Sites;
title: string;
slug: string;
parent_hub?: string | HubPages;
level?: number;
articles_count?: number;
schema_json?: Record<string, any>;
}
export interface Forms {
id: string;
site_id: string | Sites;
name: string;
fields: any[];
submit_action?: string;
success_message?: string;
redirect_url?: string;
}
export interface FormSubmissions {
id: string;
form: string | Forms;
data: Record<string, any>;
date_created?: string;
}
export interface SiteAnalytics {
id: string;
site_id: string | Sites;
google_ads_id?: string;
fb_pixel_id?: string;
}
export interface AnalyticsEvents {
id: string;
site_id: string | Sites;
event_name: string;
page_path: string;
timestamp?: string;
}
export interface PageViews {
id: string;
site_id: string | Sites;
page_path: string;
session_id?: string;
timestamp?: string;
}
export interface Conversions {
id: string;
site_id: string | Sites;
lead?: string | Leads;
conversion_type: string;
value?: number;
}
export interface LocationsStates {
id: string;
name: string;
code: string;
}
export interface LocationsCities {
id: string;
name: string;
state: string | LocationsStates;
population?: number;
}
export interface LocationsCounties {
id: string;
name: string;
state: string | LocationsStates;
population?: number;
}
// ============================================================================
// HELPER TYPES
// ============================================================================
export type Collections = keyof DirectusSchema;
export type Item<Collection extends Collections> = DirectusSchema[Collection];
export type QueryFilter<Collection extends Collections> = Partial<Item<Collection>>;

View File

@@ -1,491 +0,0 @@
/**
* Spark Platform - Directus Schema Types
*/
export interface Site {
id: string;
name: string;
domain: string;
domain_aliases?: string[];
settings?: Record<string, any>;
status: 'active' | 'inactive';
date_created?: string;
date_updated?: string;
}
export interface Page {
id: string;
site: string | Site;
title: string;
permalink: string;
status: 'draft' | 'published' | 'archived';
seo_title?: string;
seo_description?: string;
seo_image?: string;
blocks?: PageBlock[];
content?: string; // legacy fallback
schema_json?: Record<string, any>;
date_created?: string;
date_updated?: string;
}
export interface PageBlock {
id: string;
block_type: 'hero' | 'content' | 'features' | 'cta';
block_config: Record<string, any>;
}
export interface Post {
id: string;
site: string | Site;
title: string;
slug: string;
excerpt?: string;
content: string;
featured_image?: string;
status: 'draft' | 'published' | 'archived';
published_at?: string;
category?: string;
author?: string;
meta_title?: string;
seo_title?: string;
seo_description?: string;
date_created?: string;
date_updated?: string;
}
export interface Globals {
id: string;
site: string | Site;
site_name?: string;
site_tagline?: string;
logo?: string;
favicon?: string;
primary_color?: string;
secondary_color?: string;
footer_text?: string;
social_links?: SocialLink[];
scripts_head?: string;
scripts_body?: string;
}
export interface SocialLink {
platform: string;
url: string;
}
export interface Navigation {
id: string;
site: string | Site;
label: string;
url: string;
target?: '_self' | '_blank';
parent?: string | Navigation;
sort: number;
}
export interface Author {
id: string;
name: string;
bio?: string;
avatar?: string;
email?: string;
}
// SEO Engine Types
export interface CampaignMaster {
id: string;
site?: string | Site;
name: string;
headline_spintax_root: string;
niche_variables?: Record<string, string>;
location_mode: 'none' | 'state' | 'county' | 'city';
location_target?: string;
batch_count?: number;
status: 'active' | 'paused' | 'completed';
target_word_count?: number;
article_template?: string; // UUID of the template
date_created?: string;
}
export interface HeadlineInventory {
id: string;
campaign: string | CampaignMaster;
final_title_text: string;
status: 'available' | 'used';
used_on_article?: string;
location_data?: any; // JSON location data
date_created?: string;
}
export interface ContentFragment {
id: string;
campaign: string | CampaignMaster;
fragment_type: FragmentType;
content_body: string;
word_count?: number;
date_created?: string;
}
export type FragmentType = string;
export interface ImageTemplate {
id: string;
name: string;
svg_template: string;
svg_source?: string;
is_default?: boolean;
preview?: string;
}
export interface LocationState {
id: string;
name: string;
code: string;
}
export interface LocationCounty {
id: string;
name: string;
state: string | LocationState;
}
export interface LocationCity {
id: string;
name: string;
state: string | LocationState;
county: string | LocationCounty;
population?: number;
}
// ... (Existing types preserved above)
// Cartesian Engine Types
// Cartesian Engine Types
export interface GenerationJob {
id: string;
site_id: string | Site;
target_quantity: number;
status: 'queued' | 'processing' | 'completed' | 'failed' | 'Pending' | 'Complete'; // allowing legacy for safety
type?: string;
progress?: number;
priority?: 'high' | 'medium' | 'low';
config: Record<string, any>;
current_offset: number;
date_created?: string;
}
export interface ArticleTemplate {
id: string;
name: string;
structure_json: string[];
}
export interface Avatar {
id: string; // key
base_name: string;
business_niches: string[];
wealth_cluster: string;
}
export interface AvatarVariant {
id: string;
avatar_id: string;
variants_json: Record<string, string>;
}
export interface GeoCluster {
id: string;
cluster_name: string;
}
export interface GeoLocation {
id: string;
cluster: string | GeoCluster;
city: string;
state: string;
zip_focus?: string;
}
export interface SpintaxDictionary {
id: string;
category: string;
data: string[];
base_word?: string;
variations?: string; // legacy
}
export interface CartesianPattern {
id: string;
pattern_key: string;
pattern_type: string;
formula: string;
example_output?: string;
description?: string;
date_created?: string;
}
export interface OfferBlockUniversal {
id: string;
block_id: string;
title: string;
hook_generator: string;
universal_pains: string[];
universal_solutions: string[];
universal_value_points: string[];
cta_spintax: string;
}
export interface OfferBlockPersonalized {
id: string;
block_related_id: string;
avatar_related_id: string;
pains: string[];
solutions: string[];
value_points: string[];
}
// Updated GeneratedArticle to match Init Schema
export interface GeneratedArticle {
id: string;
site_id: number | string;
title: string;
slug: string;
html_content: string;
status: 'queued' | 'processing' | 'qc' | 'approved' | 'published' | 'draft' | 'archived';
priority?: 'high' | 'medium' | 'low';
assignee?: string;
due_date?: string;
seo_score?: number;
generation_hash: string;
meta_title?: string;
meta_desc?: string;
is_published?: boolean;
sync_status?: string;
schema_json?: Record<string, any>;
is_test_batch?: boolean;
date_created?: string;
date_updated?: string;
date_published?: string;
}
/**
* CRM & Forms
*/
export interface Lead {
id: string;
site: string | Site;
first_name: string;
last_name?: string;
email: string;
phone?: string;
message?: string;
source?: string;
status: 'new' | 'contacted' | 'qualified' | 'lost';
date_created?: string;
}
export interface NewsletterSubscriber {
id: string;
site: string | Site;
email: string;
status: 'subscribed' | 'unsubscribed';
date_created?: string;
}
export interface Form {
id: string;
site: string | Site;
name: string;
fields: any[];
submit_action: 'message' | 'redirect' | 'both';
success_message?: string;
redirect_url?: string;
}
export interface FormSubmission {
id: string;
form: string | Form;
data: Record<string, any>;
date_created?: string;
}
/**
* Full Spark Platform Schema for Directus SDK
*/
/**
* Full Spark Platform Schema for Directus SDK
*/
export interface SparkSchema {
sites: Site[];
pages: Page[];
posts: Post[];
globals: Globals[];
navigation: Navigation[];
authors: Author[];
// SEO Engine
campaign_masters: CampaignMaster[];
headline_inventory: HeadlineInventory[];
content_fragments: ContentFragment[];
image_templates: ImageTemplate[];
locations_states: LocationState[];
locations_counties: LocationCounty[];
locations_cities: LocationCity[];
production_queue: ProductionQueueItem[];
quality_flags: QualityFlag[];
// Cartesian Engine
generation_jobs: GenerationJob[];
article_templates: ArticleTemplate[];
avatars: Avatar[];
avatar_variants: AvatarVariant[];
geo_clusters: GeoCluster[];
geo_locations: GeoLocation[];
spintax_dictionaries: SpintaxDictionary[];
cartesian_patterns: CartesianPattern[];
offer_blocks_universal: OfferBlockUniversal[];
offer_blocks_personalized: OfferBlockPersonalized[];
generated_articles: GeneratedArticle[];
// CRM & Forms
leads: Lead[];
newsletter_subscribers: NewsletterSubscriber[];
forms: Form[];
form_submissions: FormSubmission[];
// Infrastructure & Analytics
link_targets: LinkTarget[];
hub_pages: HubPage[];
work_log: WorkLog[];
events: AnalyticsEvent[];
pageviews: PageView[];
conversions: Conversion[];
site_analytics: SiteAnalyticsConfig[];
}
export interface ProductionQueueItem {
id: string;
site: string | Site;
campaign: string | CampaignMaster;
status: 'test_batch' | 'pending' | 'active' | 'completed' | 'paused';
total_requested: number;
completed_count: number;
velocity_mode: string;
schedule_data: any[]; // JSON
date_created?: string;
}
export interface QualityFlag {
id: string;
site: string | Site;
batch_id?: string;
article_a: string;
article_b: string;
collision_text: string;
similarity_score: number;
status: 'pending' | 'resolved' | 'ignored';
date_created?: string;
}
export interface HubPage {
id: string;
site: string | Site;
title: string;
slug: string;
parent_hub?: string | HubPage;
level: number;
articles_count: number;
schema_json?: Record<string, any>;
date_created?: string;
}
export interface AnalyticsEvent {
id: string;
site: string | Site;
event_name: string;
event_category?: string;
event_label?: string;
event_value?: number;
page_path: string;
session_id?: string;
visitor_id?: string;
metadata?: Record<string, any>;
timestamp?: string;
}
export interface PageView {
id: string;
site: string | Site;
page_path: string;
page_title?: string | null;
referrer?: string | null;
user_agent?: string | null;
device_type?: string | null;
browser?: string | null;
os?: string | null;
utm_source?: string | null;
utm_medium?: string | null;
utm_campaign?: string | null;
utm_content?: string | null;
utm_term?: string | null;
is_bot?: boolean;
bot_name?: string | null;
session_id?: string | null;
visitor_id?: string | null;
timestamp?: string;
}
export interface Conversion {
id: string;
site: string | Site;
lead?: string | Lead;
conversion_type: string;
value?: number;
currency?: string;
source?: string;
campaign?: string;
gclid?: string;
fbclid?: string;
sent_to_google?: boolean;
sent_to_facebook?: boolean;
date_created?: string;
}
export interface SiteAnalyticsConfig {
id: string;
site: string | Site;
google_ads_id?: string;
google_ads_conversion_label?: string;
fb_pixel_id?: string;
fb_access_token?: string;
}
export interface WorkLog {
id: number;
site?: number | string; // Relaxed type
action: string;
entity_type?: string;
entity_id?: string | number;
details?: string | Record<string, any>; // Relaxed to allow JSON object
level?: string;
status?: string;
timestamp?: string;
date_created?: string;
user?: string;
}
export interface LinkTarget {
id: string;
site: string;
target_url?: string;
target_post?: string;
anchor_text: string;
anchor_variations?: string[];
priority?: number;
is_active?: boolean;
is_hub?: boolean;
max_per_article?: number;
}