php-content/changelog/2026/jan/TASK-content-api-backend.md
Snider 6ede1b1a20 refactor: rename namespace Core\Content to Core\Mod\Content
Aligns content module namespace with the standard module structure
convention (Core\Mod\{Name}) for consistency across the monorepo.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:24:53 +00:00

1012 lines
32 KiB
Markdown

# TASK: Content API Backend for AI Pipeline
**Status:** Ready for Implementation
**Priority:** P1
**Estimated Effort:** 2-3 weeks
**Dependencies:** Mod/Content (native CMS), external HTTP clients
---
## Overview
Build Laravel API endpoints at `host.uk.com` to orchestrate the AI content pipeline. This serves as the bridge between external requests, AI services, and the native CMS (Mod/Content).
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ External Requests │
│ (Webhooks, API Calls, Scheduled Jobs) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ host.uk.com Laravel API │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Content │ │ AI Gateway │ │ Mod/Content │ │ Mod/Social │ │
│ │ Briefs │ │ (Gemini/ │ │ Native CMS │ │ Scheduler │ │
│ │ Queue │ │ Claude) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database │ │ Gemini/ │ │ Mod/Content │ │ Mod/Social │
│ (Briefs, │ │ Claude │ │ ContentItem │ │ Posts & │
│ Queue) │ │ APIs │ │ Model │ │ Accounts │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
```
---
## Database Schema
### Migration 1: Content Briefs
```php
// database/migrations/xxxx_create_content_briefs_table.php
Schema::create('content_briefs', function (Blueprint $table) {
$table->id();
$table->string('service'); // social, link, analytics, trust, notify
$table->string('content_type'); // help_article, blog_post, landing_page
$table->string('title');
$table->string('slug')->nullable();
$table->text('description')->nullable();
$table->json('keywords')->nullable();
$table->string('category')->nullable();
$table->string('difficulty')->nullable(); // beginner, intermediate, advanced
$table->integer('target_word_count')->default(1000);
$table->json('prompt_variables')->nullable(); // Additional context
$table->string('status')->default('pending'); // pending, queued, generating, review, published
$table->integer('priority')->default(50); // 1-100, higher = more urgent
$table->timestamp('scheduled_for')->nullable();
$table->timestamps();
$table->index(['service', 'status']);
$table->index(['status', 'priority']);
});
```
### Migration 2: Content Queue
```php
// database/migrations/xxxx_create_content_queue_table.php
Schema::create('content_queue', function (Blueprint $table) {
$table->id();
$table->foreignId('brief_id')->constrained('content_briefs')->onDelete('cascade');
$table->string('stage'); // draft, refine, review, publish
$table->text('gemini_output')->nullable();
$table->text('claude_output')->nullable();
$table->text('final_content')->nullable();
$table->json('metadata')->nullable(); // frontmatter, seo data
$table->integer('content_item_id')->nullable();
$table->string('content_status')->nullable();
$table->json('generation_log')->nullable(); // Track AI calls, costs
$table->timestamp('generated_at')->nullable();
$table->timestamp('refined_at')->nullable();
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->index('stage');
});
```
### Migration 3: Social Queue
```php
// database/migrations/xxxx_create_social_queue_table.php
Schema::create('social_queue', function (Blueprint $table) {
$table->id();
$table->foreignId('content_id')->nullable()->constrained('content_queue')->onDelete('set null');
$table->string('platform'); // twitter, linkedin, instagram, facebook
$table->text('content');
$table->string('media_url')->nullable();
$table->json('metadata')->nullable();
$table->timestamp('scheduled_for');
$table->string('status')->default('pending'); // pending, scheduled, published, failed
$table->string('social_post_id')->nullable();
$table->timestamps();
$table->index(['platform', 'status']);
$table->index('scheduled_for');
});
```
### Migration 4: AI Usage Tracking
```php
// database/migrations/xxxx_create_ai_usage_table.php
Schema::create('ai_usage', function (Blueprint $table) {
$table->id();
$table->string('provider'); // gemini, claude, openai
$table->string('model');
$table->string('purpose'); // draft, refine, social, image
$table->integer('input_tokens')->default(0);
$table->integer('output_tokens')->default(0);
$table->decimal('cost_estimate', 10, 6)->default(0);
$table->foreignId('brief_id')->nullable()->constrained('content_briefs')->onDelete('set null');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['provider', 'created_at']);
});
```
---
## API Endpoints
### Content Briefs API
```php
// routes/api.php
Route::prefix('content')->middleware(['auth:sanctum'])->group(function () {
// Briefs
Route::get('briefs', [ContentBriefController::class, 'index']);
Route::post('briefs', [ContentBriefController::class, 'store']);
Route::get('briefs/{brief}', [ContentBriefController::class, 'show']);
Route::put('briefs/{brief}', [ContentBriefController::class, 'update']);
Route::delete('briefs/{brief}', [ContentBriefController::class, 'destroy']);
Route::post('briefs/bulk', [ContentBriefController::class, 'bulkStore']);
Route::get('briefs/next', [ContentBriefController::class, 'next']); // For external callers
// Queue
Route::get('queue', [ContentQueueController::class, 'index']);
Route::get('queue/{item}', [ContentQueueController::class, 'show']);
Route::post('queue/{item}/generate', [ContentQueueController::class, 'generate']);
Route::post('queue/{item}/refine', [ContentQueueController::class, 'refine']);
Route::post('queue/{item}/publish', [ContentQueueController::class, 'publish']);
Route::post('queue/{item}/approve', [ContentQueueController::class, 'approve']);
Route::post('queue/{item}/reject', [ContentQueueController::class, 'reject']);
// Generation (for external callers)
Route::post('generate/draft', [GenerationController::class, 'draft']);
Route::post('generate/refine', [GenerationController::class, 'refine']);
Route::post('generate/social', [GenerationController::class, 'socialPosts']);
});
```
### Content Integration API (Mod/Content)
```php
Route::prefix('content-items')->middleware(['auth:sanctum'])->group(function () {
Route::get('sites', [ContentController::class, 'sites']);
Route::get('items/{site}', [ContentController::class, 'items']);
Route::post('items/{site}', [ContentController::class, 'createItem']);
Route::put('items/{site}/{id}', [ContentController::class, 'updateItem']);
Route::post('items/{site}/{id}/publish', [ContentController::class, 'publishItem']);
Route::delete('items/{site}/{id}', [ContentController::class, 'deleteItem']);
Route::get('categories/{site}', [ContentController::class, 'categories']);
Route::get('media/{site}', [ContentController::class, 'media']);
});
```
### Social Scheduling API
```php
Route::prefix('social')->middleware(['auth:sanctum'])->group(function () {
Route::get('queue', [SocialQueueController::class, 'index']);
Route::post('queue', [SocialQueueController::class, 'store']);
Route::post('queue/bulk', [SocialQueueController::class, 'bulkStore']);
Route::put('queue/{item}', [SocialQueueController::class, 'update']);
Route::delete('queue/{item}', [SocialQueueController::class, 'destroy']);
Route::post('queue/{item}/schedule', [SocialQueueController::class, 'schedule']);
});
```
### Analytics/Stats API
```php
Route::prefix('stats')->middleware(['auth:sanctum'])->group(function () {
Route::get('content', [StatsController::class, 'contentStats']);
Route::get('ai-usage', [StatsController::class, 'aiUsage']);
Route::get('pipeline', [StatsController::class, 'pipelineStatus']);
});
```
---
## Services
### AI Gateway Service
```php
// app/Services/AIGatewayService.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use App\Models\AIUsage;
class AIGatewayService
{
public function __construct(
protected string $geminiApiKey,
protected string $claudeApiKey
) {}
/**
* Generate content draft using Gemini
*/
public function generateDraft(string $prompt, array $options = []): array
{
$startTime = microtime(true);
$response = Http::withHeaders([
'Content-Type' => 'application/json',
])->post("https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key={$this->geminiApiKey}", [
'contents' => [
['parts' => [['text' => $prompt]]]
],
'generationConfig' => [
'temperature' => $options['temperature'] ?? 0.7,
'maxOutputTokens' => $options['max_tokens'] ?? 4096,
]
]);
$result = $response->json();
// Log usage
$this->logUsage('gemini', 'gemini-pro', 'draft', $prompt, $result, $options['brief_id'] ?? null);
return [
'success' => $response->successful(),
'content' => $result['candidates'][0]['content']['parts'][0]['text'] ?? null,
'raw' => $result,
'duration' => microtime(true) - $startTime,
];
}
/**
* Refine content using Claude
*/
public function refineContent(string $content, string $refinementPrompt, array $options = []): array
{
$startTime = microtime(true);
$response = Http::withHeaders([
'x-api-key' => $this->claudeApiKey,
'anthropic-version' => '2024-01-01',
'Content-Type' => 'application/json',
])->post('https://api.anthropic.com/v1/messages', [
'model' => $options['model'] ?? 'claude-3-opus-20240229',
'max_tokens' => $options['max_tokens'] ?? 4096,
'messages' => [
[
'role' => 'user',
'content' => $refinementPrompt . "\n\n---\n\n" . $content
]
]
]);
$result = $response->json();
// Log usage
$this->logUsage('claude', $options['model'] ?? 'claude-3-opus', 'refine', $refinementPrompt, $result, $options['brief_id'] ?? null);
return [
'success' => $response->successful(),
'content' => $result['content'][0]['text'] ?? null,
'raw' => $result,
'duration' => microtime(true) - $startTime,
];
}
/**
* Generate social media posts from content
*/
public function generateSocialPosts(string $content, string $title, string $url): array
{
$prompt = <<<PROMPT
Generate social media posts for this content:
Title: {$title}
URL: {$url}
Content summary:
{$content}
Generate posts for:
1. Twitter (280 chars max, include URL)
2. LinkedIn (300 words, professional tone)
3. Facebook (150 words, conversational)
Return as JSON: {"twitter": "...", "linkedin": "...", "facebook": "..."}
PROMPT;
return $this->generateDraft($prompt, ['temperature' => 0.8]);
}
protected function logUsage(string $provider, string $model, string $purpose, string $input, array $result, ?int $briefId): void
{
// Estimate tokens (rough calculation)
$inputTokens = (int) (strlen($input) / 4);
$outputTokens = isset($result['content'][0]['text'])
? (int) (strlen($result['content'][0]['text']) / 4)
: (isset($result['candidates'][0]['content']['parts'][0]['text'])
? (int) (strlen($result['candidates'][0]['content']['parts'][0]['text']) / 4)
: 0);
// Cost estimates (approximate)
$costPerInputToken = match($provider) {
'gemini' => 0.000001,
'claude' => 0.000015,
default => 0.00001,
};
$costPerOutputToken = match($provider) {
'gemini' => 0.000002,
'claude' => 0.000075,
default => 0.00003,
};
AIUsage::create([
'provider' => $provider,
'model' => $model,
'purpose' => $purpose,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'cost_estimate' => ($inputTokens * $costPerInputToken) + ($outputTokens * $costPerOutputToken),
'brief_id' => $briefId,
'metadata' => [
'usage' => $result['usage'] ?? null,
],
]);
}
}
```
### Content Client Service (Mod/Content)
```php
// app/Mod/Content/Services/ContentClientService.php
<?php
namespace Mod\Content\Services;
use Mod\Content\Models\ContentItem;
use Mod\Content\Models\ContentSite;
use Illuminate\Support\Facades\Cache;
class ContentClientService
{
protected array $sites = [
'social' => 'social.host.uk.com',
'link' => 'link.host.uk.com',
'analytics' => 'analytics.host.uk.com',
'trust' => 'trust.host.uk.com',
'notify' => 'notify.host.uk.com',
];
public function getItems(string $site, array $params = []): array
{
$cacheKey = "content_items_{$site}_" . md5(serialize($params));
return Cache::remember($cacheKey, 300, function () use ($site, $params) {
return ContentItem::query()
->where('site', $site)
->when($params['status'] ?? null, fn($q, $s) => $q->where('status', $s))
->limit($params['per_page'] ?? 20)
->get()
->toArray();
});
}
public function createItem(string $site, array $data): array
{
$item = ContentItem::create([
'site' => $site,
'title' => $data['title'],
'content' => $data['content'],
'excerpt' => $data['excerpt'] ?? '',
'status' => $data['status'] ?? 'draft',
'categories' => $data['categories'] ?? [],
'tags' => $data['tags'] ?? [],
'meta' => $data['meta'] ?? [],
]);
// Clear cache
Cache::forget("content_items_{$site}_*");
return [
'success' => true,
'item' => $item->toArray(),
'id' => $item->id,
];
}
public function updateItem(string $site, int $itemId, array $data): array
{
$item = ContentItem::where('site', $site)->findOrFail($itemId);
$item->update($data);
Cache::forget("content_items_{$site}_*");
return [
'success' => true,
'item' => $item->fresh()->toArray(),
];
}
public function publishItem(string $site, int $itemId): array
{
return $this->updateItem($site, $itemId, ['status' => 'published']);
}
public function getCategories(string $site): array
{
return Cache::remember("content_categories_{$site}", 3600, function () use ($site) {
return ContentItem::where('site', $site)
->distinct('category')
->pluck('category')
->toArray();
});
}
}
```
### Social Scheduler Service (Mod/Social)
```php
// app/Mod/Social/Services/SocialSchedulerService.php
<?php
namespace Mod\Social\Services;
use Mod\Social\Models\Post;
use Mod\Social\Models\Account;
class SocialSchedulerService
{
public function schedulePost(array $data): array
{
$post = Post::create([
'content' => $data['content'],
'media' => $data['media'] ?? [],
'accounts' => $data['accounts'], // Platform account IDs
'scheduled_at' => $data['scheduled_at'],
'status' => 'scheduled',
]);
return [
'success' => true,
'post' => $post->toArray(),
'id' => $post->id,
];
}
public function getAccounts(): array
{
return Account::all()->toArray();
}
public function getScheduledPosts(array $params = []): array
{
return Post::query()
->where('status', 'scheduled')
->when($params['account_id'] ?? null, fn($q, $id) => $q->where('account_id', $id))
->orderBy('scheduled_at')
->get()
->toArray();
}
}
```
---
## Controllers
### Content Brief Controller
```php
// app/Http/Controllers/Api/ContentBriefController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\ContentBrief;
use App\Http\Requests\ContentBriefRequest;
use Illuminate\Http\Request;
class ContentBriefController extends Controller
{
public function index(Request $request)
{
$briefs = ContentBrief::query()
->when($request->service, fn($q, $s) => $q->where('service', $s))
->when($request->status, fn($q, $s) => $q->where('status', $s))
->when($request->type, fn($q, $t) => $q->where('content_type', $t))
->orderBy('priority', 'desc')
->orderBy('created_at', 'asc')
->paginate($request->per_page ?? 20);
return response()->json($briefs);
}
public function store(ContentBriefRequest $request)
{
$brief = ContentBrief::create($request->validated());
return response()->json($brief, 201);
}
public function bulkStore(Request $request)
{
$request->validate([
'briefs' => 'required|array',
'briefs.*.service' => 'required|string',
'briefs.*.content_type' => 'required|string',
'briefs.*.title' => 'required|string',
]);
$briefs = collect($request->briefs)->map(function ($data) {
return ContentBrief::create($data);
});
return response()->json([
'created' => $briefs->count(),
'briefs' => $briefs,
], 201);
}
/**
* Get next brief to process (for external callers)
*/
public function next(Request $request)
{
$brief = ContentBrief::query()
->where('status', 'pending')
->when($request->service, fn($q, $s) => $q->where('service', $s))
->orderBy('priority', 'desc')
->orderBy('scheduled_for', 'asc')
->orderBy('created_at', 'asc')
->first();
if (!$brief) {
return response()->json(['message' => 'No briefs pending'], 404);
}
// Mark as queued
$brief->update(['status' => 'queued']);
return response()->json($brief);
}
public function show(ContentBrief $brief)
{
return response()->json($brief->load('queueItem'));
}
public function update(ContentBriefRequest $request, ContentBrief $brief)
{
$brief->update($request->validated());
return response()->json($brief);
}
public function destroy(ContentBrief $brief)
{
$brief->delete();
return response()->json(null, 204);
}
}
```
### Generation Controller (for external callers)
```php
// app/Http/Controllers/Api/GenerationController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\AIGatewayService;
use Mod\Content\Services\ContentClientService;
use App\Models\ContentBrief;
use App\Models\ContentQueue;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class GenerationController extends Controller
{
public function __construct(
protected AIGatewayService $ai,
protected ContentClientService $content
) {}
/**
* Generate draft content (Gemini)
*/
public function draft(Request $request)
{
$request->validate([
'brief_id' => 'required|exists:content_briefs,id',
]);
$brief = ContentBrief::findOrFail($request->brief_id);
// Load prompt template
$promptTemplate = $this->getPromptTemplate($brief->content_type);
$prompt = $this->buildPrompt($promptTemplate, $brief);
// Generate with Gemini
$result = $this->ai->generateDraft($prompt, [
'brief_id' => $brief->id,
]);
if (!$result['success']) {
return response()->json(['error' => 'Generation failed', 'details' => $result], 500);
}
// Create or update queue item
$queueItem = ContentQueue::updateOrCreate(
['brief_id' => $brief->id],
[
'stage' => 'draft',
'gemini_output' => $result['content'],
'generated_at' => now(),
'generation_log' => [
'gemini' => [
'duration' => $result['duration'],
'timestamp' => now()->toIso8601String(),
]
],
]
);
$brief->update(['status' => 'generating']);
return response()->json([
'success' => true,
'queue_item' => $queueItem,
'content' => $result['content'],
]);
}
/**
* Refine content (Claude)
*/
public function refine(Request $request)
{
$request->validate([
'queue_id' => 'required|exists:content_queue,id',
]);
$queueItem = ContentQueue::with('brief')->findOrFail($request->queue_id);
if (!$queueItem->gemini_output) {
return response()->json(['error' => 'No draft to refine'], 400);
}
// Load refinement prompt
$refinementPrompt = $this->getRefinementPrompt($queueItem->brief->content_type);
// Refine with Claude
$result = $this->ai->refineContent(
$queueItem->gemini_output,
$refinementPrompt,
['brief_id' => $queueItem->brief_id]
);
if (!$result['success']) {
return response()->json(['error' => 'Refinement failed', 'details' => $result], 500);
}
// Update queue item
$queueItem->update([
'stage' => 'review',
'claude_output' => $result['content'],
'final_content' => $result['content'],
'refined_at' => now(),
'generation_log' => array_merge($queueItem->generation_log ?? [], [
'claude' => [
'duration' => $result['duration'],
'timestamp' => now()->toIso8601String(),
]
]),
]);
$queueItem->brief->update(['status' => 'review']);
return response()->json([
'success' => true,
'queue_item' => $queueItem->fresh(),
'content' => $result['content'],
]);
}
/**
* Generate social posts for content
*/
public function socialPosts(Request $request)
{
$request->validate([
'queue_id' => 'required|exists:content_queue,id',
'url' => 'required|url',
]);
$queueItem = ContentQueue::with('brief')->findOrFail($request->queue_id);
$result = $this->ai->generateSocialPosts(
substr($queueItem->final_content, 0, 2000), // Summary
$queueItem->brief->title,
$request->url
);
if (!$result['success']) {
return response()->json(['error' => 'Social generation failed'], 500);
}
$posts = json_decode($result['content'], true);
return response()->json([
'success' => true,
'posts' => $posts,
]);
}
protected function getPromptTemplate(string $type): string
{
$templates = [
'help_article' => Storage::disk('local')->get('prompts/help-article.txt'),
'blog_post' => Storage::disk('local')->get('prompts/blog-post.txt'),
'landing_page' => Storage::disk('local')->get('prompts/landing-page.txt'),
];
return $templates[$type] ?? $templates['help_article'];
}
protected function getRefinementPrompt(string $type): string
{
return Storage::disk('local')->get('prompts/refinement.txt');
}
protected function buildPrompt(string $template, ContentBrief $brief): string
{
$variables = array_merge([
'SERVICE_NAME' => $this->getServiceName($brief->service),
'SERVICE_URL' => $this->getServiceUrl($brief->service),
'TITLE' => $brief->title,
'DESCRIPTION' => $brief->description ?? '',
'KEYWORDS' => implode(', ', $brief->keywords ?? []),
'CATEGORY' => $brief->category ?? '',
'DIFFICULTY' => $brief->difficulty ?? 'beginner',
'WORD_COUNT' => $brief->target_word_count,
], $brief->prompt_variables ?? []);
foreach ($variables as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value, $template);
}
return $template;
}
protected function getServiceName(string $service): string
{
return match($service) {
'social' => 'Host Social',
'link' => 'Host Link',
'analytics' => 'Host Analytics',
'trust' => 'Host Trust',
'notify' => 'Host Notify',
default => 'Host UK',
};
}
protected function getServiceUrl(string $service): string
{
return match($service) {
'social' => 'social.host.uk.com',
'link' => 'link.host.uk.com',
'analytics' => 'analytics.host.uk.com',
'trust' => 'trust.host.uk.com',
'notify' => 'notify.host.uk.com',
default => 'host.uk.com',
};
}
}
```
---
## Configuration
### Environment Variables
```env
# AI Services
GEMINI_API_KEY=your_gemini_api_key
ANTHROPIC_API_KEY=your_claude_api_key
# External Webhook Secret (for validation)
CONTENT_WEBHOOK_SECRET=your_webhook_secret
```
### Config File
```php
// config/content.php
return [
'ai' => [
'gemini' => [
'api_key' => env('GEMINI_API_KEY'),
'model' => 'gemini-pro',
'max_tokens' => 4096,
],
'claude' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => 'claude-3-opus-20240229',
'max_tokens' => 4096,
],
],
'sites' => [
'social' => 'social.host.uk.com',
'link' => 'link.host.uk.com',
'analytics' => 'analytics.host.uk.com',
'trust' => 'trust.host.uk.com',
'notify' => 'notify.host.uk.com',
],
'webhook' => [
'secret' => env('CONTENT_WEBHOOK_SECRET'),
],
'defaults' => [
'word_count' => 1000,
'priority' => 50,
],
];
```
---
## Implementation Checklist
### Phase 1: Core Infrastructure (Week 1)
- [ ] **Database**
- [ ] Create migrations
- [ ] Create models with relationships
- [ ] Create factories for testing
- [ ] **Services**
- [ ] AIGatewayService (Gemini + Claude)
- [ ] ContentClientService (Mod/Content)
- [ ] SocialSchedulerService (Mod/Social)
- [ ] **Basic API**
- [ ] ContentBrief CRUD
- [ ] ContentQueue management
- [ ] Authentication (Sanctum)
### Phase 2: Generation Pipeline (Week 2)
- [ ] **Generation Controller**
- [ ] Draft endpoint (Gemini)
- [ ] Refine endpoint (Claude)
- [ ] Social posts endpoint
- [ ] **Prompt Management**
- [ ] Store prompt templates
- [ ] Variable substitution
- [ ] Service-specific prompts
- [ ] **Content Publishing (Mod/Content)**
- [ ] Create draft items
- [ ] Update existing items
- [ ] Publish workflow
### Phase 3: Integration (Week 3)
- [ ] **External Webhooks**
- [ ] Webhook endpoints for external callers
- [ ] Secret validation
- [ ] Response formatting
- [ ] **Social Integration (Mod/Social)**
- [ ] Schedule social posts
- [ ] Account management
- [ ] Post status tracking
- [ ] **Monitoring**
- [ ] AI usage tracking
- [ ] Cost reporting
- [ ] Error logging
### Phase 4: Polish
- [ ] **Admin Dashboard** (optional)
- [ ] Brief management UI
- [ ] Queue status view
- [ ] Analytics dashboard
- [ ] **Testing**
- [ ] Unit tests for services
- [ ] Feature tests for API
- [ ] Integration tests
- [ ] **Documentation**
- [ ] API documentation
- [ ] External integration examples
- [ ] Deployment guide
---
## External Integration Endpoints Summary
| Endpoint | Method | Purpose |
|----------------------------------|--------|----------------------------|
| `/api/content/briefs/next` | GET | Get next brief to process |
| `/api/content/generate/draft` | POST | Generate draft with Gemini |
| `/api/content/generate/refine` | POST | Refine with Claude |
| `/api/content/generate/social` | POST | Generate social posts |
| `/api/content/queue/{id}/publish`| POST | Publish to Mod/Content |
| `/api/social/queue` | POST | Schedule via Mod/Social |
---
## File Structure
```
app/
├── Http/
│ ├── Controllers/
│ │ └── Api/
│ │ ├── ContentBriefController.php
│ │ ├── ContentQueueController.php
│ │ ├── GenerationController.php
│ │ ├── SocialQueueController.php
│ │ └── StatsController.php
│ └── Requests/
│ └── ContentBriefRequest.php
├── Models/
│ ├── ContentBrief.php
│ ├── ContentQueue.php
│ ├── SocialQueue.php
│ └── AIUsage.php
├── Mod/
│ ├── Content/
│ │ └── Services/
│ │ └── ContentClientService.php
│ └── Social/
│ └── Services/
│ └── SocialSchedulerService.php
└── Services/
└── AIGatewayService.php
database/
└── migrations/
├── xxxx_create_content_briefs_table.php
├── xxxx_create_content_queue_table.php
├── xxxx_create_social_queue_table.php
└── xxxx_create_ai_usage_table.php
storage/
└── app/
└── prompts/
├── help-article.txt
├── blog-post.txt
├── landing-page.txt
└── refinement.txt
config/
└── content.php
```