Scheduled research persona generation

This commit is contained in:
ajaysi
2025-11-05 08:51:00 +05:30
parent 55087c4f37
commit d99c7c83a7
98 changed files with 14518 additions and 828 deletions

View File

@@ -1,14 +1,15 @@
import axios from 'axios';
// Global subscription error handler - will be set by the app
let globalSubscriptionErrorHandler: ((error: any) => boolean) | null = null;
// Can be async to support subscription status refresh
let globalSubscriptionErrorHandler: ((error: any) => boolean | Promise<boolean>) | null = null;
export const setGlobalSubscriptionErrorHandler = (handler: (error: any) => boolean) => {
export const setGlobalSubscriptionErrorHandler = (handler: (error: any) => boolean | Promise<boolean>) => {
globalSubscriptionErrorHandler = handler;
};
// Export a function to trigger subscription error handler from outside axios interceptors
export const triggerSubscriptionError = (error: any) => {
export const triggerSubscriptionError = async (error: any) => {
const status = error?.response?.status;
console.log('triggerSubscriptionError: Received error', {
hasHandler: !!globalSubscriptionErrorHandler,
@@ -18,7 +19,9 @@ export const triggerSubscriptionError = (error: any) => {
if (globalSubscriptionErrorHandler) {
console.log('triggerSubscriptionError: Calling global subscription error handler');
return globalSubscriptionErrorHandler(error);
const result = globalSubscriptionErrorHandler(error);
// Handle both sync and async handlers
return result instanceof Promise ? await result : result;
}
console.warn('triggerSubscriptionError: No global subscription error handler registered');
@@ -28,6 +31,13 @@ export const triggerSubscriptionError = (error: any) => {
// Optional token getter installed from within the app after Clerk is available
let authTokenGetter: (() => Promise<string | null>) | null = null;
// Optional Clerk sign-out function - set by App.tsx when Clerk is available
let clerkSignOut: (() => Promise<void>) | null = null;
export const setClerkSignOut = (signOutFn: () => Promise<void>) => {
clerkSignOut = signOutFn;
};
export const setAuthTokenGetter = (getter: () => Promise<string | null>) => {
authTokenGetter = getter;
};
@@ -170,25 +180,67 @@ apiClient.interceptors.response.use(
console.error('Token refresh failed:', retryError);
}
// If retry failed, don't redirect during app initialization (root route)
// Only redirect if we're on a protected route and definitely authenticated
// If retry failed, token is expired - sign out user and redirect to sign in
const isOnboardingRoute = window.location.pathname.includes('/onboarding');
const isRootRoute = window.location.pathname === '/';
// Don't redirect from root route during app initialization - allow InitialRouteHandler to work
if (!isRootRoute && !isOnboardingRoute) {
// Only redirect if we're definitely not just initializing
try { window.location.assign('/'); } catch {}
// Token expired - sign out user and redirect to landing/sign-in
console.warn('401 Unauthorized - token expired, signing out user');
// Clear any cached auth data
localStorage.removeItem('user_id');
localStorage.removeItem('auth_token');
// Use Clerk signOut if available, otherwise just redirect
if (clerkSignOut) {
clerkSignOut()
.then(() => {
// Redirect to landing page after sign out
window.location.assign('/');
})
.catch((err) => {
console.error('Error during Clerk sign out:', err);
// Fallback: redirect anyway
window.location.assign('/');
});
} else {
// Fallback: redirect to landing (will show sign-in if Clerk handles it)
window.location.assign('/');
}
} else {
console.warn('401 Unauthorized - token refresh failed (during initialization, not redirecting)');
}
}
// Handle 401 errors that weren't retried (e.g., no authTokenGetter, already retried, etc.)
if (error?.response?.status === 401 && (originalRequest._retry || !authTokenGetter)) {
const isOnboardingRoute = window.location.pathname.includes('/onboarding');
const isRootRoute = window.location.pathname === '/';
if (!isRootRoute && !isOnboardingRoute) {
// Token expired - sign out user and redirect
console.warn('401 Unauthorized - token expired (not retried), signing out user');
localStorage.removeItem('user_id');
localStorage.removeItem('auth_token');
if (clerkSignOut) {
clerkSignOut()
.then(() => window.location.assign('/'))
.catch(() => window.location.assign('/'));
} else {
window.location.assign('/');
}
}
}
// Check if it's a subscription-related error and handle it globally
if (error.response?.status === 429 || error.response?.status === 402) {
console.log('API Client: Detected subscription error, triggering global handler');
if (globalSubscriptionErrorHandler) {
const wasHandled = globalSubscriptionErrorHandler(error);
const result = globalSubscriptionErrorHandler(error);
const wasHandled = result instanceof Promise ? await result : result;
if (wasHandled) {
console.log('API Client: Subscription error handled by global handler');
return Promise.reject(error);
@@ -245,7 +297,18 @@ aiApiClient.interceptors.response.use(
// Don't redirect from root route during app initialization
if (!isRootRoute && !isOnboardingRoute) {
try { window.location.assign('/'); } catch {}
// Token expired - sign out user and redirect
console.warn('401 Unauthorized - token expired, signing out user');
localStorage.removeItem('user_id');
localStorage.removeItem('auth_token');
if (clerkSignOut) {
clerkSignOut()
.then(() => window.location.assign('/'))
.catch(() => window.location.assign('/'));
} else {
window.location.assign('/');
}
} else {
console.warn('401 Unauthorized - token refresh failed (during initialization, not redirecting)');
}
@@ -255,7 +318,8 @@ aiApiClient.interceptors.response.use(
if (error.response?.status === 429 || error.response?.status === 402) {
console.log('AI API Client: Detected subscription error, triggering global handler');
if (globalSubscriptionErrorHandler) {
const wasHandled = globalSubscriptionErrorHandler(error);
const result = globalSubscriptionErrorHandler(error);
const wasHandled = result instanceof Promise ? await result : result;
if (wasHandled) {
console.log('AI API Client: Subscription error handled by global handler');
return Promise.reject(error);
@@ -290,7 +354,7 @@ longRunningApiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
async (error) => {
if (error?.response?.status === 401) {
// Only redirect on 401 if we're not in onboarding flow or root route
const isOnboardingRoute = window.location.pathname.includes('/onboarding');
@@ -307,7 +371,8 @@ longRunningApiClient.interceptors.response.use(
if (error.response?.status === 429 || error.response?.status === 402) {
console.log('Long-running API Client: Detected subscription error, triggering global handler');
if (globalSubscriptionErrorHandler) {
const wasHandled = globalSubscriptionErrorHandler(error);
const result = globalSubscriptionErrorHandler(error);
const wasHandled = result instanceof Promise ? await result : result;
if (wasHandled) {
console.log('Long-running API Client: Subscription error handled by global handler');
return Promise.reject(error);
@@ -342,7 +407,7 @@ pollingApiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
async (error) => {
if (error?.response?.status === 401) {
// Only redirect on 401 if we're not in onboarding flow or root route
const isOnboardingRoute = window.location.pathname.includes('/onboarding');
@@ -357,18 +422,11 @@ pollingApiClient.interceptors.response.use(
}
// Check if it's a subscription-related error and handle it globally
if (error.response?.status === 429 || error.response?.status === 402) {
console.log('Polling API Client: Detected subscription error, triggering global handler', {
status: error.response?.status,
data: error.response?.data,
hasHandler: !!globalSubscriptionErrorHandler
});
if (globalSubscriptionErrorHandler) {
const wasHandled = globalSubscriptionErrorHandler(error);
console.log('Polling API Client: Global handler returned', wasHandled);
if (wasHandled) {
console.log('Polling API Client: Subscription error handled by global handler - modal should be showing');
} else {
console.warn('Polling API Client: Global handler did not handle subscription error');
const result = globalSubscriptionErrorHandler(error);
const wasHandled = result instanceof Promise ? await result : result;
if (!wasHandled) {
console.warn('Polling API Client: Subscription error not handled by global handler');
}
// Always reject so the polling hook can also handle it
return Promise.reject(error);

View File

@@ -0,0 +1,181 @@
/**
* OAuth Token Monitoring API Client
* Functions for interacting with OAuth token monitoring endpoints
*/
import { apiClient } from './client';
export interface OAuthTokenStatus {
connected: boolean;
monitoring_task: {
id: number | null;
status: string;
last_check: string | null;
last_success: string | null;
last_failure: string | null;
failure_reason: string | null;
next_check: string | null;
} | null;
}
export interface PlatformStatus {
[platform: string]: OAuthTokenStatus;
}
export interface OAuthTokenStatusResponse {
success: boolean;
data: {
user_id: string;
platform_status: PlatformStatus;
connected_platforms: string[];
};
}
export interface ManualRefreshResponse {
success: boolean;
message: string;
data: {
platform: string;
status: string;
last_check: string | null;
last_success: string | null;
last_failure: string | null;
failure_reason: string | null;
next_check: string | null;
execution_result: {
success: boolean;
error_message: string | null;
execution_time_ms: number | null;
result_data: any;
};
};
}
export interface ExecutionLog {
id: number;
task_id: number;
platform: string;
execution_date: string;
status: string;
result_data: any;
error_message: string | null;
execution_time_ms: number | null;
created_at: string;
}
export interface ExecutionLogsResponse {
success: boolean;
data: {
logs: ExecutionLog[];
total_count: number;
limit: number;
offset: number;
};
}
export interface CreateTasksResponse {
success: boolean;
message: string;
data: {
tasks_created: number;
tasks: Array<{
id: number;
platform: string;
status: string;
next_check: string | null;
}>;
};
}
/**
* Get OAuth token monitoring status for all platforms
*/
export const getOAuthTokenStatus = async (userId: string): Promise<OAuthTokenStatusResponse> => {
try {
const response = await apiClient.get<OAuthTokenStatusResponse>(`/api/oauth-tokens/status/${userId}`);
return response.data;
} catch (error: any) {
console.error('Error fetching OAuth token status:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch OAuth token status'
);
}
};
/**
* Manually trigger token refresh for a specific platform
*/
export const manualRefreshToken = async (
userId: string,
platform: string
): Promise<ManualRefreshResponse> => {
try {
const response = await apiClient.post<ManualRefreshResponse>(
`/api/oauth-tokens/refresh/${userId}/${platform}`
);
return response.data;
} catch (error: any) {
console.error('Error manually refreshing token:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to refresh token'
);
}
};
/**
* Get execution logs for OAuth token monitoring
*/
export const getOAuthTokenExecutionLogs = async (
userId: string,
platform?: string,
limit: number = 50,
offset: number = 0
): Promise<ExecutionLogsResponse> => {
try {
const params: any = { limit, offset };
if (platform) {
params.platform = platform;
}
const response = await apiClient.get<ExecutionLogsResponse>(
`/api/oauth-tokens/execution-logs/${userId}`,
{ params }
);
return response.data;
} catch (error: any) {
console.error('Error fetching execution logs:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch execution logs'
);
}
};
/**
* Create OAuth token monitoring tasks
*/
export const createOAuthMonitoringTasks = async (
userId: string,
platforms?: string[]
): Promise<CreateTasksResponse> => {
try {
const response = await apiClient.post<CreateTasksResponse>(
`/api/oauth-tokens/create-tasks/${userId}`,
platforms ? { platforms } : {}
);
return response.data;
} catch (error: any) {
console.error('Error creating monitoring tasks:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to create monitoring tasks'
);
}
};

View File

@@ -216,6 +216,42 @@ export const generatePlatformPersona = async (platform: string): Promise<any> =>
}
};
/**
* Check if Facebook persona exists for user
* Note: user_id is extracted from Clerk JWT token or passed as parameter
*/
export const checkFacebookPersona = async (userId?: string): Promise<{
has_persona: boolean;
has_core_persona: boolean;
persona: any;
onboarding_completed: boolean;
}> => {
try {
// Get user_id from parameter or localStorage
const user_id = userId || localStorage.getItem('user_id');
if (!user_id) {
return {
has_persona: false,
has_core_persona: false,
persona: null,
onboarding_completed: false
};
}
const response = await apiClient.get(`/api/personas/facebook-persona/check/${user_id}`);
return response.data;
} catch (error: any) {
console.error('Error checking Facebook persona:', error);
// Return safe defaults on error
return {
has_persona: false,
has_core_persona: false,
persona: null,
onboarding_completed: false
};
}
};
/**
* Delete a persona
*/

View File

@@ -0,0 +1,157 @@
/**
* Research Configuration API
* Fetches provider availability and persona-aware defaults
*/
import { ResearchMode, ResearchProvider } from '../services/blogWriterApi';
import { apiClient } from './client';
export interface ProviderAvailability {
google_available: boolean;
exa_available: boolean;
gemini_key_status: 'configured' | 'missing';
exa_key_status: 'configured' | 'missing';
}
export interface PersonaDefaults {
industry?: string;
target_audience?: string;
suggested_domains: string[];
suggested_exa_category?: string;
}
export interface ResearchPreset {
name: string;
keywords: string;
industry: string;
target_audience: string;
research_mode: ResearchMode;
config: any; // ResearchConfig
description?: string;
icon?: string;
}
export interface ResearchPersona {
default_industry: string;
default_target_audience: string;
default_research_mode: ResearchMode;
default_provider: ResearchProvider;
suggested_keywords: string[];
keyword_expansion_patterns: Record<string, string[]>;
suggested_exa_domains: string[];
suggested_exa_category?: string;
research_angles: string[];
query_enhancement_rules: Record<string, string>;
recommended_presets: ResearchPreset[];
research_preferences: Record<string, any>;
generated_at?: string;
confidence_score?: number;
version?: string;
}
export interface ResearchConfigResponse {
provider_availability: ProviderAvailability;
persona_defaults: PersonaDefaults;
research_persona?: ResearchPersona;
onboarding_completed?: boolean;
persona_scheduled?: boolean;
}
/**
* Get provider availability status
*/
export const getProviderAvailability = async (): Promise<ProviderAvailability> => {
try {
const response = await apiClient.get('/api/research/provider-availability');
return response.data;
} catch (error: any) {
console.error('[researchConfig] Error getting provider availability:', error);
throw new Error(`Failed to get provider availability: ${error?.response?.statusText || error.message}`);
}
};
/**
* Get persona-aware research defaults
*/
export const getPersonaDefaults = async (): Promise<PersonaDefaults> => {
try {
const response = await apiClient.get('/api/research/persona-defaults');
return response.data;
} catch (error: any) {
console.error('[researchConfig] Error getting persona defaults:', error);
throw new Error(`Failed to get persona defaults: ${error?.response?.statusText || error.message}`);
}
};
// Request deduplication: cache in-flight requests to prevent duplicate API calls
let pendingConfigRequest: Promise<ResearchConfigResponse> | null = null;
/**
* Get complete research configuration
*
* Uses request deduplication: if multiple components call this simultaneously,
* they will share the same promise to prevent duplicate API calls.
*/
export const getResearchConfig = async (): Promise<ResearchConfigResponse> => {
// If a request is already in flight, return the same promise
if (pendingConfigRequest) {
console.log('[researchConfig] Reusing pending request to avoid duplicate API call');
return pendingConfigRequest;
}
// Create new request and cache it
pendingConfigRequest = (async () => {
try {
const response = await apiClient.get('/api/research/config');
return response.data;
} catch (error: any) {
const statusCode = error?.response?.status;
const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error';
console.error('[researchConfig] Error getting research config:', {
status: statusCode,
message: errorMessage,
fullError: error
});
// Provide more specific error messages based on status code
if (statusCode === 500) {
throw new Error(`Backend server error: ${errorMessage}. Please check backend logs or try again later.`);
} else if (statusCode === 401) {
throw new Error('Authentication required. Please sign in again.');
} else if (statusCode === 403) {
throw new Error('Access denied. Please check your permissions.');
} else if (statusCode === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
} else if (!statusCode && error?.message) {
// Network error or other connection issue
throw new Error(`Failed to connect to server: ${error.message}`);
} else {
throw new Error(`Failed to get research config: ${errorMessage}`);
}
} finally {
// Clear the cached request after completion (success or error)
pendingConfigRequest = null;
}
})();
return pendingConfigRequest;
};
/**
* Get or refresh research persona
* @param forceRefresh - If true, regenerate persona even if cache is valid
*/
export const refreshResearchPersona = async (forceRefresh: boolean = false): Promise<ResearchPersona> => {
try {
const url = `/api/research/research-persona${forceRefresh ? '?force_refresh=true' : ''}`;
const response = await apiClient.get(url);
return response.data;
} catch (error: any) {
console.error('[researchConfig] Error refreshing research persona:', error?.response?.status || error?.message);
// Preserve the original error so subscription errors can be detected
// The apiClient interceptor should handle 429 errors, but we preserve the error structure
throw error;
}
};

View File

@@ -0,0 +1,249 @@
/**
* Scheduler Dashboard API Client
* Provides typed functions for fetching scheduler dashboard data.
*/
import { apiClient } from './client';
// TypeScript interfaces for scheduler dashboard data
export interface SchedulerStats {
total_checks: number;
tasks_found: number;
tasks_executed: number;
tasks_failed: number;
tasks_skipped: number;
last_check: string | null;
last_update: string | null;
active_executions: number;
running: boolean;
check_interval_minutes: number;
min_check_interval_minutes: number;
max_check_interval_minutes: number;
intelligent_scheduling: boolean;
active_strategies_count: number;
last_interval_adjustment: string | null;
registered_types: string[];
// Cumulative/historical values from database
cumulative_total_check_cycles: number;
cumulative_tasks_found: number;
cumulative_tasks_executed: number;
cumulative_tasks_failed: number;
}
export interface SchedulerJob {
id: string;
trigger_type: string;
next_run_time: string | null;
user_id: string | null;
job_store: string;
user_job_store: string;
function_name?: string | null;
platform?: string; // For OAuth token monitoring tasks
task_id?: number; // For OAuth token monitoring tasks
is_database_task?: boolean; // Flag to indicate DB task vs APScheduler job
frequency?: string; // For OAuth tasks (e.g., 'Weekly')
}
export interface UserIsolation {
enabled: boolean;
current_user_id: string | null;
}
export interface SchedulerDashboardData {
stats: SchedulerStats;
jobs: SchedulerJob[];
job_count: number;
recurring_jobs: number;
one_time_jobs: number;
user_isolation: UserIsolation;
last_updated: string;
}
export interface TaskInfo {
id: number;
task_title: string;
component_name: string;
metric: string;
frequency: string;
}
export interface ExecutionLog {
id: number;
task_id: number | null;
user_id: number | string | null;
execution_date: string;
status: 'success' | 'failed' | 'running' | 'skipped';
error_message: string | null;
execution_time_ms: number | null;
result_data: any;
created_at: string;
task?: TaskInfo;
is_scheduler_log?: boolean; // Flag for scheduler logs vs execution logs
event_type?: string;
job_id?: string | null;
}
export interface ExecutionLogsResponse {
logs: ExecutionLog[];
total_count: number;
limit: number;
offset: number;
has_more: boolean;
is_scheduler_logs?: boolean; // Flag to indicate if these are scheduler logs
}
export interface SchedulerJobsResponse {
jobs: SchedulerJob[];
total_jobs: number;
recurring_jobs: number;
one_time_jobs: number;
}
export interface SchedulerEvent {
id: number;
event_type: 'check_cycle' | 'interval_adjustment' | 'start' | 'stop' | 'job_scheduled' | 'job_cancelled' | 'job_completed' | 'job_failed';
event_date: string | null;
check_cycle_number: number | null;
check_interval_minutes: number | null;
previous_interval_minutes: number | null;
new_interval_minutes: number | null;
tasks_found: number | null;
tasks_executed: number | null;
tasks_failed: number | null;
tasks_by_type: Record<string, number> | null;
check_duration_seconds: number | null;
active_strategies_count: number | null;
active_executions: number | null;
job_id: string | null;
job_type: string | null;
user_id: string | null;
event_data: any;
error_message: string | null;
created_at: string | null;
}
export interface SchedulerEventHistoryResponse {
events: SchedulerEvent[];
total_count: number;
limit: number;
offset: number;
has_more: boolean;
}
/**
* Get scheduler dashboard statistics and current state.
*/
export const getSchedulerDashboard = async (): Promise<SchedulerDashboardData> => {
try {
const response = await apiClient.get<SchedulerDashboardData>('/api/scheduler/dashboard');
return response.data;
} catch (error: any) {
console.error('Error fetching scheduler dashboard:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch scheduler dashboard'
);
}
};
/**
* Get task execution logs from database.
*
* @param limit - Number of logs to return (1-500, default: 50)
* @param offset - Pagination offset (default: 0)
* @param status - Filter by status (success, failed, running, skipped)
*/
export const getExecutionLogs = async (
limit: number = 50,
offset: number = 0,
status?: 'success' | 'failed' | 'running' | 'skipped'
): Promise<ExecutionLogsResponse> => {
try {
const params: any = { limit, offset };
if (status) {
params.status = status;
}
const response = await apiClient.get<ExecutionLogsResponse>('/api/scheduler/execution-logs', {
params
});
return response.data;
} catch (error: any) {
console.error('Error fetching execution logs:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch execution logs'
);
}
};
/**
* Get detailed information about all scheduled jobs.
*/
export const getSchedulerJobs = async (): Promise<SchedulerJobsResponse> => {
try {
const response = await apiClient.get<SchedulerJobsResponse>('/api/scheduler/jobs');
return response.data;
} catch (error: any) {
console.error('Error fetching scheduler jobs:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch scheduler jobs'
);
}
};
/**
* Get scheduler event history from database.
*
* @param limit - Number of events to return (1-1000, default: 100)
* @param offset - Pagination offset (default: 0)
* @param eventType - Filter by event type (check_cycle, interval_adjustment, start, stop, etc.)
*/
export const getSchedulerEventHistory = async (
limit: number = 100,
offset: number = 0,
eventType?: 'check_cycle' | 'interval_adjustment' | 'start' | 'stop' | 'job_scheduled' | 'job_cancelled' | 'job_completed' | 'job_failed'
): Promise<SchedulerEventHistoryResponse> => {
try {
const params: any = { limit, offset };
if (eventType) {
params.event_type = eventType;
}
const response = await apiClient.get<SchedulerEventHistoryResponse>('/api/scheduler/event-history', {
params
});
return response.data;
} catch (error: any) {
console.error('Error fetching scheduler event history:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch scheduler event history'
);
}
};
/**
* Get recent scheduler logs (restoration, job scheduling, etc.) formatted as execution logs.
* These are shown in Execution Logs section when actual execution logs are not available.
* Returns only the latest 5 logs (rolling window).
*/
export const getRecentSchedulerLogs = async (): Promise<ExecutionLogsResponse> => {
try {
const response = await apiClient.get<ExecutionLogsResponse>('/api/scheduler/recent-scheduler-logs');
return response.data;
} catch (error: any) {
console.error('Error fetching recent scheduler logs:', error);
throw new Error(
error.response?.data?.detail ||
error.message ||
'Failed to fetch recent scheduler logs'
);
}
};

View File

@@ -51,7 +51,8 @@ export interface StyleDetectionResponse {
timestamp: string;
}
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
// Consistent API URL pattern - no hardcoded localhost fallback
const API_BASE_URL = process.env.REACT_APP_API_URL || process.env.REACT_APP_BACKEND_URL || '';
/**
* Analyze content style using AI

View File

@@ -66,7 +66,7 @@ export interface WordPressHealthResponse {
}
class WordPressAPI {
private baseUrl = '/wordpress';
private baseUrl = '/api/wordpress';
private getAuthToken: (() => Promise<string | null>) | null = null;
/**
@@ -102,7 +102,17 @@ class WordPressAPI {
const client = await this.getAuthenticatedClient();
const response = await client.get(`${this.baseUrl}/status`);
return response.data;
} catch (error) {
} catch (error: any) {
// Handle 404 gracefully - endpoint may not exist yet
if (error?.response?.status === 404) {
// Return empty status instead of throwing
return {
connected: false,
sites: [],
total_sites: 0
};
}
// Only log non-404 errors
console.error('WordPress API: Error getting status:', error);
throw error;
}