Scheduled research persona generation
This commit is contained in:
@@ -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);
|
||||
|
||||
181
frontend/src/api/oauthTokenMonitoring.ts
Normal file
181
frontend/src/api/oauthTokenMonitoring.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
157
frontend/src/api/researchConfig.ts
Normal file
157
frontend/src/api/researchConfig.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
249
frontend/src/api/schedulerDashboard.ts
Normal file
249
frontend/src/api/schedulerDashboard.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user