modcomms/frontend/services/apiService.ts
michael 99af0164e6 Add PostgreSQL database support with Alembic migrations
Backend:
- Add PostgreSQL service to docker-compose with health checks
- Add SQLAlchemy async models for all entities (Agency, User, Campaign,
  Proof, ProofVersion, FlaggedItem, ResolvedItem, ErrorItem)
- Add Alembic migration framework with initial schema migration
- Add repository layer for CRUD operations
- Add REST API endpoints for campaigns, proofs, and audit items
- Add file storage service for proof uploads
- Update WebSocket handler to optionally persist analysis results

Frontend:
- Add apiService.ts for REST API communication
- Update geminiService.ts to support database persistence options

Deployment:
- Update deploy.sh to handle database migrations (6-step process)
- Update Dockerfile to include alembic configuration
- Add PostgreSQL environment variables to .env templates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 12:27:18 -06:00

317 lines
9.6 KiB
TypeScript

import { IPublicClientApplication } from '@azure/msal-browser';
import { getAccessToken } from './authService';
import type { AgentReview, FlaggedItem, ResolvedItem, ErrorItem } from '../types';
const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000';
// Types for API responses
export interface CampaignResponse {
id: string;
name: string;
workfront_id: string | null;
client_lead: string | null;
agency_lead: string | null;
brand_guidelines: string | null;
status: string;
agency: string | null;
created_at: string;
updated_at: string;
proofs: number;
}
export interface ProofVersionResponse {
id: string;
version: number;
file_storage_key: string | null;
thumbnail_url: string | null;
agent_review: AgentReview | null;
overall_status: string | null;
workfront_id: string | null;
created_at: string;
}
export interface ProofResponse {
id: string;
proof_name: string;
channel: string | null;
sub_channel: string | null;
proof_type: string | null;
workfront_id: string | null;
created_at: string;
versions: ProofVersionResponse[];
}
export interface AnalyticsResponse {
total_reviews: number;
passed: number;
failed: number;
errors: number;
legal_review: number;
}
export interface FlaggedItemResponse {
id: string;
proof_version_id: string;
agent_flagged: string;
comments: string | null;
submitter_name: string | null;
submitter_agency: string | null;
campaign_name: string | null;
proof_name: string | null;
version: number | null;
created_at: string;
}
export interface ResolvedItemResponse {
id: string;
proof_version_id: string;
agent: string;
issue: string | null;
resolution: string | null;
submitter_name: string | null;
submitter_agency: string | null;
campaign_name: string | null;
proof_name: string | null;
version: number | null;
created_at: string;
}
export interface ErrorItemResponse {
id: string;
proof_version_id: string;
error_summary: string | null;
campaign_name: string | null;
proof_name: string | null;
version: number | null;
created_at: string;
}
class ApiService {
private msalInstance: IPublicClientApplication | null = null;
setMsalInstance(instance: IPublicClientApplication) {
this.msalInstance = instance;
}
private async getHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
if (this.msalInstance) {
const token = await getAccessToken(this.msalInstance);
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
private async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const headers = await this.getHeaders();
const response = await fetch(`${API_URL}/api${endpoint}`, {
...options,
headers: {
...headers,
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T;
}
return response.json();
}
// Campaign endpoints
async getCampaigns(): Promise<CampaignResponse[]> {
return this.fetch<CampaignResponse[]>('/campaigns');
}
async getCampaign(id: string): Promise<CampaignResponse> {
return this.fetch<CampaignResponse>(`/campaigns/${id}`);
}
async createCampaign(data: {
name: string;
workfront_id?: string;
client_lead?: string;
agency_lead?: string;
brand_guidelines?: string;
}): Promise<CampaignResponse> {
return this.fetch<CampaignResponse>('/campaigns', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateCampaign(id: string, data: {
name?: string;
workfront_id?: string;
client_lead?: string;
agency_lead?: string;
brand_guidelines?: string;
status?: string;
}): Promise<CampaignResponse> {
return this.fetch<CampaignResponse>(`/campaigns/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteCampaign(id: string): Promise<void> {
return this.fetch<void>(`/campaigns/${id}`, {
method: 'DELETE',
});
}
// Proof endpoints
async getProofs(campaignId: string): Promise<ProofResponse[]> {
return this.fetch<ProofResponse[]>(`/campaigns/${campaignId}/proofs`);
}
async getProof(id: string): Promise<ProofResponse> {
return this.fetch<ProofResponse>(`/proofs/${id}`);
}
async deleteProof(id: string): Promise<void> {
return this.fetch<void>(`/proofs/${id}`, {
method: 'DELETE',
});
}
// Audit endpoints
async flagProofVersion(proofId: string, version: number, data: {
agent_flagged: string;
comments?: string;
}): Promise<FlaggedItemResponse> {
return this.fetch<FlaggedItemResponse>(`/proofs/${proofId}/versions/${version}/flag`, {
method: 'POST',
body: JSON.stringify({
proof_version_id: '', // Will be set by backend
...data,
}),
});
}
async resolveProofVersion(proofId: string, version: number, data: {
agent: string;
issue?: string;
resolution?: string;
}): Promise<ResolvedItemResponse> {
return this.fetch<ResolvedItemResponse>(`/proofs/${proofId}/versions/${version}/resolve`, {
method: 'POST',
body: JSON.stringify({
proof_version_id: '', // Will be set by backend
...data,
}),
});
}
async getFlaggedItems(): Promise<FlaggedItemResponse[]> {
return this.fetch<FlaggedItemResponse[]>('/audit/flagged');
}
async getResolvedItems(): Promise<ResolvedItemResponse[]> {
return this.fetch<ResolvedItemResponse[]>('/audit/resolved');
}
async getErrorItems(): Promise<ErrorItemResponse[]> {
return this.fetch<ErrorItemResponse[]>('/audit/errors');
}
// Analytics endpoint
async getAnalytics(): Promise<AnalyticsResponse> {
return this.fetch<AnalyticsResponse>('/analytics');
}
// Helper to convert API response to frontend format
convertCampaignToFrontend(campaign: CampaignResponse) {
return {
name: campaign.name,
workfrontId: campaign.workfront_id || '',
clientLead: campaign.client_lead || '',
agency: campaign.agency || '',
agencyLead: campaign.agency_lead || '',
proofs: campaign.proofs,
status: campaign.status as 'In Progress' | 'Completed',
lastModified: campaign.updated_at,
brandGuidelines: campaign.brand_guidelines || 'Barclays',
_id: campaign.id,
};
}
convertProofToFrontend(proof: ProofResponse) {
const latestVersion = proof.versions[0];
return {
proofName: proof.proof_name,
channel: proof.channel || '',
subChannel: proof.sub_channel || '',
proofType: proof.proof_type || '',
status: latestVersion?.overall_status === 'Analysis Error' ? 'error' : 'completed' as 'completed' | 'analyzing' | 'error' | 'loading',
overallStatus: latestVersion?.overall_status as any,
versions: proof.versions.map(v => ({
version: v.version,
timestamp: v.created_at.split('T')[0],
workfrontId: v.workfront_id || '',
proofPreviewUrl: v.thumbnail_url || '',
feedback: v.agent_review || {} as AgentReview,
overallStatus: v.overall_status as any,
})),
_id: proof.id,
};
}
convertFlaggedItemToFrontend(item: FlaggedItemResponse): FlaggedItem {
return {
id: item.id,
campaignName: item.campaign_name || '',
proofName: item.proof_name || '',
version: item.version || 1,
submitter: item.submitter_name || '',
submitAgency: item.submitter_agency || '',
agentFlagged: item.agent_flagged,
comments: item.comments || '',
timestamp: item.created_at,
};
}
convertResolvedItemToFrontend(item: ResolvedItemResponse): ResolvedItem {
return {
id: item.id,
campaignName: item.campaign_name || '',
proofName: item.proof_name || '',
version: item.version || 1,
submitter: item.submitter_name || '',
submitAgency: item.submitter_agency || '',
agent: item.agent,
issue: item.issue || '',
resolution: item.resolution || '',
timestamp: item.created_at,
};
}
convertErrorItemToFrontend(item: ErrorItemResponse): ErrorItem {
return {
id: item.id,
campaignName: item.campaign_name || '',
proofName: item.proof_name || '',
version: item.version || 1,
submitter: '',
submitAgency: '',
errorSummary: item.error_summary || '',
timestamp: item.created_at,
};
}
}
export const apiService = new ApiService();
export default apiService;