Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* and improve performance while managing cache invalidation.
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import { apiClient, aiApiClient } from './client';
|
||||
import analyticsCache from '../services/analyticsCache';
|
||||
|
||||
interface PlatformAnalytics {
|
||||
@@ -73,16 +73,16 @@ class CachedAnalyticsAPI {
|
||||
/**
|
||||
* Get analytics data with caching
|
||||
*/
|
||||
async getAnalyticsData(platforms?: string[], bypassCache: boolean = false): Promise<AnalyticsResponse> {
|
||||
async getAnalyticsData(platforms?: string[], bypassCache: boolean = false, opts?: { start_date?: string; end_date?: string }): Promise<AnalyticsResponse> {
|
||||
const baseParams: any = platforms ? { platforms: platforms.join(',') } : {};
|
||||
const endpoint = '/api/analytics/data';
|
||||
|
||||
// If bypassing cache, add timestamp to force fresh request
|
||||
const requestParams = bypassCache ? { ...baseParams, _t: Date.now() } : baseParams;
|
||||
const requestParams = bypassCache ? { ...baseParams, _t: Date.now(), ...(opts || {}) } : { ...baseParams, ...(opts || {}) };
|
||||
|
||||
// Try to get from cache first (unless bypassing)
|
||||
if (!bypassCache) {
|
||||
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, baseParams);
|
||||
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, requestParams);
|
||||
if (cached) {
|
||||
console.log('📦 Analytics Cache HIT: Analytics data (cached for 60 minutes)');
|
||||
return cached;
|
||||
@@ -95,12 +95,19 @@ class CachedAnalyticsAPI {
|
||||
|
||||
// Cache the result with extended TTL (unless bypassing)
|
||||
if (!bypassCache) {
|
||||
analyticsCache.set(endpoint, baseParams, response.data, this.CACHE_TTL.ANALYTICS_DATA);
|
||||
analyticsCache.set(endpoint, requestParams, response.data, this.CACHE_TTL.ANALYTICS_DATA);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getAIInsights(opts: { start_date: string; end_date: string }): Promise<{ success: boolean; insights?: any; error?: string }> {
|
||||
const endpoint = '/api/analytics/ai-insights';
|
||||
const params = { start_date: opts.start_date, end_date: opts.end_date, _t: Date.now() };
|
||||
const response = await aiApiClient.get(endpoint, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate platform status cache
|
||||
*/
|
||||
@@ -128,7 +135,7 @@ class CachedAnalyticsAPI {
|
||||
/**
|
||||
* Force refresh analytics data (bypass cache)
|
||||
*/
|
||||
async forceRefreshAnalyticsData(platforms?: string[]): Promise<AnalyticsResponse> {
|
||||
async forceRefreshAnalyticsData(platforms?: string[], opts?: { start_date?: string; end_date?: string }): Promise<AnalyticsResponse> {
|
||||
// Try to clear backend cache first (but don't fail if it doesn't work)
|
||||
try {
|
||||
await this.clearBackendCache(platforms);
|
||||
@@ -140,7 +147,7 @@ class CachedAnalyticsAPI {
|
||||
this.invalidateAnalyticsData();
|
||||
|
||||
// Finally get fresh data with cache bypass
|
||||
return this.getAnalyticsData(platforms, true);
|
||||
return this.getAnalyticsData(platforms, true, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const sanitizeUrlForLogging = (url: string | undefined): string => {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const [base, query] = url.split('?');
|
||||
if (!query) return url;
|
||||
const params = new URLSearchParams(query);
|
||||
if (params.has('token')) {
|
||||
params.set('token', '***');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `${base}?${queryString}` : base;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
// Global subscription error handler - will be set by the app
|
||||
// Can be async to support subscription status refresh
|
||||
let globalSubscriptionErrorHandler: ((error: any) => boolean | Promise<boolean>) | null = null;
|
||||
@@ -108,7 +124,8 @@ export const pollingApiClient = axios.create({
|
||||
// Add request interceptor for logging and authentication
|
||||
apiClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
console.log(`Making ${config.method?.toUpperCase()} request to ${config.url}`);
|
||||
const safeUrl = sanitizeUrlForLogging(config.url);
|
||||
console.log(`Making ${config.method?.toUpperCase()} request to ${safeUrl}`);
|
||||
try {
|
||||
if (!authTokenGetter) {
|
||||
// If authTokenGetter is not set, reject the request to prevent 401 errors
|
||||
@@ -123,7 +140,8 @@ apiClient.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
(config.headers as any)['Authorization'] = `Bearer ${token}`;
|
||||
console.log(`[apiClient] ✅ Added auth token to request: ${config.url}`);
|
||||
const safeUrlWithToken = sanitizeUrlForLogging(config.url);
|
||||
console.log(`[apiClient] ✅ Auth token attached for request to ${safeUrlWithToken}`);
|
||||
} else {
|
||||
// Token getter returned null - reject request to prevent 401 errors
|
||||
// ProtectedRoute should ensure user is authenticated before components render
|
||||
@@ -299,7 +317,8 @@ apiClient.interceptors.response.use(
|
||||
// Add interceptors for AI client
|
||||
aiApiClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
console.log(`Making AI ${config.method?.toUpperCase()} request to ${config.url}`);
|
||||
const safeUrl = sanitizeUrlForLogging(config.url);
|
||||
console.log(`Making AI ${config.method?.toUpperCase()} request to ${safeUrl}`);
|
||||
try {
|
||||
if (!authTokenGetter) {
|
||||
console.warn(`[aiApiClient] ⚠️ authTokenGetter not set for ${config.url} - request may fail authentication`);
|
||||
@@ -309,7 +328,8 @@ aiApiClient.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
(config.headers as any)['Authorization'] = `Bearer ${token}`;
|
||||
console.log(`[aiApiClient] ✅ Added auth token to request: ${config.url}`);
|
||||
const safeUrlWithToken = sanitizeUrlForLogging(config.url);
|
||||
console.log(`[aiApiClient] ✅ Auth token attached for request to ${safeUrlWithToken}`);
|
||||
} else {
|
||||
console.warn(`[aiApiClient] ⚠️ authTokenGetter returned null for ${config.url} - user may not be signed in`);
|
||||
}
|
||||
@@ -579,4 +599,4 @@ pollingApiClient.interceptors.response.use(
|
||||
console.error('Polling API Error:', error.message || error, error.response?.status, error.response?.data);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
@@ -35,6 +35,27 @@ export interface AIInsight {
|
||||
tool_path?: string;
|
||||
}
|
||||
|
||||
export interface SIFIndexingHealth {
|
||||
has_task: boolean;
|
||||
status: 'healthy' | 'warning' | 'critical' | 'not_scheduled';
|
||||
message?: string;
|
||||
task?: {
|
||||
id: number;
|
||||
website_url: string;
|
||||
raw_status: string;
|
||||
next_execution: string | null;
|
||||
last_success: string | null;
|
||||
last_failure: string | null;
|
||||
consecutive_failures: number;
|
||||
failure_pattern?: any;
|
||||
};
|
||||
last_run?: {
|
||||
status: string | null;
|
||||
time: string | null;
|
||||
error_message: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SEODashboardData {
|
||||
health_score: SEOHealthScore;
|
||||
key_insight: string;
|
||||
@@ -141,5 +162,15 @@ export const seoDashboardAPI = {
|
||||
console.error('Error checking SEO dashboard health:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getSIFHealth(): Promise<SIFIndexingHealth> {
|
||||
try {
|
||||
const response = await apiClient.get('/api/seo-dashboard/sif-health');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching SIF indexing health:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user