Enhance ApiStatusIndicator to conditionally render for aws-admin accounts only. Improve error handling in API response management across various components, ensuring expected 400 responses are logged appropriately. Update Credits and ApiMonitor components to handle null values gracefully and implement CRUD operations for planner and writer endpoints.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { API_BASE_URL } from "../../services/api";
|
import { API_BASE_URL } from "../../services/api";
|
||||||
|
import { useAuthStore } from "../../store/authStore";
|
||||||
|
|
||||||
interface GroupStatus {
|
interface GroupStatus {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -77,10 +78,19 @@ const endpointGroups = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function ApiStatusIndicator() {
|
export default function ApiStatusIndicator() {
|
||||||
|
const { user } = useAuthStore();
|
||||||
const [groupStatuses, setGroupStatuses] = useState<GroupStatus[]>([]);
|
const [groupStatuses, setGroupStatuses] = useState<GroupStatus[]>([]);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const intervalRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Only show and run for aws-admin accounts
|
||||||
|
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||||
|
|
||||||
|
// Return null if not aws-admin account
|
||||||
|
if (!isAwsAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const checkEndpoint = useCallback(async (path: string, method: string): Promise<'healthy' | 'warning' | 'error'> => {
|
const checkEndpoint = useCallback(async (path: string, method: string): Promise<'healthy' | 'warning' | 'error'> => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token') ||
|
const token = localStorage.getItem('auth_token') ||
|
||||||
@@ -135,8 +145,22 @@ export default function ApiStatusIndicator() {
|
|||||||
fetchOptions.body = JSON.stringify(body);
|
fetchOptions.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||||
|
// These are expected and indicate the endpoint is working
|
||||||
|
const isExpected400 = method === 'POST' && (
|
||||||
|
path.includes('/login/') ||
|
||||||
|
path.includes('/register/') ||
|
||||||
|
path.includes('/bulk_') ||
|
||||||
|
path.includes('/test/')
|
||||||
|
);
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||||
|
|
||||||
|
// Suppress console errors for expected 400 responses
|
||||||
|
if (!isExpected400 || response.status !== 400) {
|
||||||
|
// Only log if it's not an expected 400
|
||||||
|
}
|
||||||
|
|
||||||
if (actualMethod === 'OPTIONS') {
|
if (actualMethod === 'OPTIONS') {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return 'healthy';
|
return 'healthy';
|
||||||
@@ -158,7 +182,18 @@ export default function ApiStatusIndicator() {
|
|||||||
}
|
}
|
||||||
return 'warning';
|
return 'warning';
|
||||||
} else if (method === 'POST') {
|
} else if (method === 'POST') {
|
||||||
|
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||||
|
const isExpected400 = path.includes('/login/') ||
|
||||||
|
path.includes('/register/') ||
|
||||||
|
path.includes('/bulk_') ||
|
||||||
|
path.includes('/test/');
|
||||||
|
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
|
// 400 is expected for test requests - endpoint is working
|
||||||
|
if (!isExpected400) {
|
||||||
|
// Only log if it's unexpected
|
||||||
|
console.warn(`[ApiStatusIndicator] ${method} ${path}: 400 (unexpected)`);
|
||||||
|
}
|
||||||
return 'healthy';
|
return 'healthy';
|
||||||
} else if (response.status >= 200 && response.status < 300) {
|
} else if (response.status >= 200 && response.status < 300) {
|
||||||
return 'healthy';
|
return 'healthy';
|
||||||
@@ -170,6 +205,19 @@ export default function ApiStatusIndicator() {
|
|||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
return 'warning';
|
return 'warning';
|
||||||
|
} else if (method === 'PUT' || method === 'DELETE') {
|
||||||
|
// UPDATE/DELETE operations
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
// 400/404 expected for test requests - endpoint is working
|
||||||
|
return 'healthy';
|
||||||
|
} else if (response.status === 204 || (response.status >= 200 && response.status < 300)) {
|
||||||
|
return 'healthy';
|
||||||
|
} else if (response.status === 401 || response.status === 403) {
|
||||||
|
return 'warning';
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
return 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'warning';
|
return 'warning';
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function Credits() {
|
|||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Current Balance</h3>
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Current Balance</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
{balance.credits.toLocaleString()}
|
{(balance.credits ?? 0).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Available credits</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Available credits</p>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -62,7 +62,7 @@ export default function Credits() {
|
|||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Monthly Allocation</h3>
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Monthly Allocation</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
{balance.plan_credits_per_month.toLocaleString()}
|
{(balance.plan_credits_per_month ?? 0).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits per month</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits per month</p>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -72,7 +72,7 @@ export default function Credits() {
|
|||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Used This Month</h3>
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Used This Month</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
{balance.credits_used_this_month.toLocaleString()}
|
{(balance.credits_used_this_month ?? 0).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits consumed</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits consumed</p>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -82,7 +82,7 @@ export default function Credits() {
|
|||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Remaining</h3>
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Remaining</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
{balance.credits_remaining.toLocaleString()}
|
{(balance.credits_remaining ?? 0).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits remaining</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Credits remaining</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const endpointGroups: EndpointGroup[] = [
|
|||||||
{ path: "/v1/system/prompts/save/", method: "POST", description: "Save prompt" },
|
{ path: "/v1/system/prompts/save/", method: "POST", description: "Save prompt" },
|
||||||
{ path: "/v1/system/author-profiles/", method: "GET", description: "List author profiles" },
|
{ path: "/v1/system/author-profiles/", method: "GET", description: "List author profiles" },
|
||||||
{ path: "/v1/system/strategies/", method: "GET", description: "List strategies" },
|
{ path: "/v1/system/strategies/", method: "GET", description: "List strategies" },
|
||||||
{ path: "/v1/system/settings/integrations/1/test/", method: "POST", description: "Test integration" },
|
{ path: "/v1/system/settings/integrations/openai/test/", method: "POST", description: "Test integration (OpenAI)" },
|
||||||
{ path: "/v1/system/settings/account/", method: "GET", description: "Account settings" },
|
{ path: "/v1/system/settings/account/", method: "GET", description: "Account settings" },
|
||||||
{ path: "/v1/billing/credits/balance/balance/", method: "GET", description: "Credit balance" },
|
{ path: "/v1/billing/credits/balance/balance/", method: "GET", description: "Credit balance" },
|
||||||
{ path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" },
|
{ path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" },
|
||||||
@@ -126,6 +126,46 @@ const endpointGroups: EndpointGroup[] = [
|
|||||||
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
|
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "CRUD Operations - Planner",
|
||||||
|
endpoints: [
|
||||||
|
{ path: "/v1/planner/keywords/", method: "GET", description: "List keywords (READ)" },
|
||||||
|
{ path: "/v1/planner/keywords/", method: "POST", description: "Create keyword (CREATE)" },
|
||||||
|
{ path: "/v1/planner/keywords/1/", method: "GET", description: "Get keyword (READ)" },
|
||||||
|
{ path: "/v1/planner/keywords/1/", method: "PUT", description: "Update keyword (UPDATE)" },
|
||||||
|
{ path: "/v1/planner/keywords/1/", method: "DELETE", description: "Delete keyword (DELETE)" },
|
||||||
|
{ path: "/v1/planner/clusters/", method: "GET", description: "List clusters (READ)" },
|
||||||
|
{ path: "/v1/planner/clusters/", method: "POST", description: "Create cluster (CREATE)" },
|
||||||
|
{ path: "/v1/planner/clusters/1/", method: "GET", description: "Get cluster (READ)" },
|
||||||
|
{ path: "/v1/planner/clusters/1/", method: "PUT", description: "Update cluster (UPDATE)" },
|
||||||
|
{ path: "/v1/planner/clusters/1/", method: "DELETE", description: "Delete cluster (DELETE)" },
|
||||||
|
{ path: "/v1/planner/ideas/", method: "GET", description: "List ideas (READ)" },
|
||||||
|
{ path: "/v1/planner/ideas/", method: "POST", description: "Create idea (CREATE)" },
|
||||||
|
{ path: "/v1/planner/ideas/1/", method: "GET", description: "Get idea (READ)" },
|
||||||
|
{ path: "/v1/planner/ideas/1/", method: "PUT", description: "Update idea (UPDATE)" },
|
||||||
|
{ path: "/v1/planner/ideas/1/", method: "DELETE", description: "Delete idea (DELETE)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CRUD Operations - Writer",
|
||||||
|
endpoints: [
|
||||||
|
{ path: "/v1/writer/tasks/", method: "GET", description: "List tasks (READ)" },
|
||||||
|
{ path: "/v1/writer/tasks/", method: "POST", description: "Create task (CREATE)" },
|
||||||
|
{ path: "/v1/writer/tasks/1/", method: "GET", description: "Get task (READ)" },
|
||||||
|
{ path: "/v1/writer/tasks/1/", method: "PUT", description: "Update task (UPDATE)" },
|
||||||
|
{ path: "/v1/writer/tasks/1/", method: "DELETE", description: "Delete task (DELETE)" },
|
||||||
|
{ path: "/v1/writer/content/", method: "GET", description: "List content (READ)" },
|
||||||
|
{ path: "/v1/writer/content/", method: "POST", description: "Create content (CREATE)" },
|
||||||
|
{ path: "/v1/writer/content/1/", method: "GET", description: "Get content (READ)" },
|
||||||
|
{ path: "/v1/writer/content/1/", method: "PUT", description: "Update content (UPDATE)" },
|
||||||
|
{ path: "/v1/writer/content/1/", method: "DELETE", description: "Delete content (DELETE)" },
|
||||||
|
{ path: "/v1/writer/images/", method: "GET", description: "List images (READ)" },
|
||||||
|
{ path: "/v1/writer/images/", method: "POST", description: "Create image (CREATE)" },
|
||||||
|
{ path: "/v1/writer/images/1/", method: "GET", description: "Get image (READ)" },
|
||||||
|
{ path: "/v1/writer/images/1/", method: "PUT", description: "Update image (UPDATE)" },
|
||||||
|
{ path: "/v1/writer/images/1/", method: "DELETE", description: "Delete image (DELETE)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
@@ -245,9 +285,18 @@ export default function ApiMonitor() {
|
|||||||
body = { username: 'test', password: 'test' }; // Will fail validation but endpoint exists
|
body = { username: 'test', password: 'test' }; // Will fail validation but endpoint exists
|
||||||
} else if (path.includes('/register/')) {
|
} else if (path.includes('/register/')) {
|
||||||
body = { username: 'test', email: 'test@test.com', password: 'test' }; // Will fail validation but endpoint exists
|
body = { username: 'test', email: 'test@test.com', password: 'test' }; // Will fail validation but endpoint exists
|
||||||
|
} else if (path.includes('/bulk_')) {
|
||||||
|
// Bulk operations need ids array
|
||||||
|
body = { ids: [] };
|
||||||
|
} else {
|
||||||
|
// CRUD CREATE operations - minimal valid body
|
||||||
|
body = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchOptions.body = JSON.stringify(body);
|
fetchOptions.body = JSON.stringify(body);
|
||||||
|
} else if (method === 'PUT') {
|
||||||
|
// CRUD UPDATE operations - minimal valid body
|
||||||
|
fetchOptions.body = JSON.stringify({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||||
@@ -380,6 +429,39 @@ export default function ApiMonitor() {
|
|||||||
} else {
|
} else {
|
||||||
status = 'warning';
|
status = 'warning';
|
||||||
}
|
}
|
||||||
|
} else if (method === 'PUT') {
|
||||||
|
// UPDATE operations
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
// 400/404 expected for test requests (validation/not found) - endpoint is working
|
||||||
|
status = 'healthy';
|
||||||
|
} else if (response.status >= 200 && response.status < 300) {
|
||||||
|
status = 'healthy';
|
||||||
|
} else if (response.status === 429) {
|
||||||
|
status = 'warning'; // Rate limited
|
||||||
|
} else if (response.status === 401 || response.status === 403) {
|
||||||
|
status = 'warning'; // Needs authentication
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
status = 'error';
|
||||||
|
} else {
|
||||||
|
status = 'warning';
|
||||||
|
}
|
||||||
|
} else if (method === 'DELETE') {
|
||||||
|
// DELETE operations
|
||||||
|
if (response.status === 204 || response.status === 200) {
|
||||||
|
// 204 No Content or 200 OK - successful delete
|
||||||
|
status = 'healthy';
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
// 404 expected for test requests (resource not found) - endpoint is working
|
||||||
|
status = 'healthy';
|
||||||
|
} else if (response.status === 429) {
|
||||||
|
status = 'warning'; // Rate limited
|
||||||
|
} else if (response.status === 401 || response.status === 403) {
|
||||||
|
status = 'warning'; // Needs authentication
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
status = 'error';
|
||||||
|
} else {
|
||||||
|
status = 'warning';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store API status
|
// Store API status
|
||||||
|
|||||||
@@ -236,14 +236,15 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
|||||||
if (contentType.includes('application/json')) {
|
if (contentType.includes('application/json')) {
|
||||||
try {
|
try {
|
||||||
errorData = JSON.parse(text);
|
errorData = JSON.parse(text);
|
||||||
errorMessage = errorData.error || errorData.message || errorData.detail || errorMessage;
|
|
||||||
|
|
||||||
// If the response has a success field set to false, return it as-is
|
// Handle unified error format: {success: false, error: "...", errors: {...}}
|
||||||
// This allows callers to handle structured error responses
|
if (errorData.success === false) {
|
||||||
if (errorData.success === false && errorData.error) {
|
// Extract error message from unified format
|
||||||
// Return the error response object instead of throwing
|
errorMessage = errorData.error || errorData.message || errorData.detail || errorMessage;
|
||||||
// This is a special case for structured error responses
|
// Keep errorData for structured error handling
|
||||||
return errorData;
|
} else {
|
||||||
|
// Old format or other error structure
|
||||||
|
errorMessage = errorData.error || errorData.message || errorData.detail || errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classify error type
|
// Classify error type
|
||||||
@@ -314,12 +315,47 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON response
|
// Parse JSON response
|
||||||
|
let parsedResponse;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text);
|
parsedResponse = JSON.parse(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If JSON parsing fails, return text
|
// If JSON parsing fails, return text
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle unified API response format
|
||||||
|
// Paginated responses: {success: true, count: X, results: [...], next: ..., previous: ...}
|
||||||
|
// Single object/list responses: {success: true, data: {...}}
|
||||||
|
// Error responses: {success: false, error: "...", errors: {...}}
|
||||||
|
|
||||||
|
// If it's a unified format response with success field
|
||||||
|
if (parsedResponse && typeof parsedResponse === 'object' && 'success' in parsedResponse) {
|
||||||
|
// For paginated responses, return as-is (results is at top level)
|
||||||
|
if ('results' in parsedResponse && 'count' in parsedResponse) {
|
||||||
|
return parsedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single object/list responses, extract data field
|
||||||
|
if ('data' in parsedResponse) {
|
||||||
|
return parsedResponse.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error responses should have been thrown already in !response.ok block above
|
||||||
|
// If we somehow get here with an error response (shouldn't happen), throw it
|
||||||
|
if (parsedResponse.success === false) {
|
||||||
|
const errorMsg = parsedResponse.error || parsedResponse.message || 'Request failed';
|
||||||
|
const apiError = new Error(`API Error: ${errorMsg}`);
|
||||||
|
(apiError as any).response = parsedResponse;
|
||||||
|
(apiError as any).status = 400;
|
||||||
|
throw apiError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If success is true but no data/results, return the whole response
|
||||||
|
return parsedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a unified format response, return as-is (backward compatibility)
|
||||||
|
return parsedResponse;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
@@ -1114,10 +1150,8 @@ export async function fetchContentImages(filters: ContentImagesFilters = {}): Pr
|
|||||||
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const response = await fetchAPI(`/v1/writer/images/content_images/${queryString ? `?${queryString}` : ''}`);
|
// fetchAPI automatically extracts data field from unified format
|
||||||
// Extract data field from unified API response format
|
return fetchAPI(`/v1/writer/images/content_images/${queryString ? `?${queryString}` : ''}`);
|
||||||
// Response format: { success: true, data: { count: ..., results: [...] }, request_id: "..." }
|
|
||||||
return response?.data || response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkUpdateImagesStatus(contentId: number, status: string): Promise<{ updated_count: number }> {
|
export async function bulkUpdateImagesStatus(contentId: number, status: string): Promise<{ updated_count: number }> {
|
||||||
@@ -1264,7 +1298,9 @@ export async function selectSectorsForSite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSiteSectors(siteId: number): Promise<any[]> {
|
export async function fetchSiteSectors(siteId: number): Promise<any[]> {
|
||||||
return fetchAPI(`/v1/auth/sites/${siteId}/sectors/`);
|
const response = await fetchAPI(`/v1/auth/sites/${siteId}/sectors/`);
|
||||||
|
// fetchAPI automatically extracts data field from unified format
|
||||||
|
return Array.isArray(response) ? response : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Industries API functions
|
// Industries API functions
|
||||||
@@ -1291,7 +1327,20 @@ export interface IndustriesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchIndustries(): Promise<IndustriesResponse> {
|
export async function fetchIndustries(): Promise<IndustriesResponse> {
|
||||||
return fetchAPI('/v1/auth/industries/');
|
const response = await fetchAPI('/v1/auth/industries/');
|
||||||
|
// fetchAPI automatically extracts data field, but industries endpoint returns {industries: [...]}
|
||||||
|
// So we need to handle the nested structure
|
||||||
|
if (response && typeof response === 'object' && 'industries' in response) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
industries: response.industries || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// If response is already an array or different format
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
industries: Array.isArray(response) ? response : []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sectors API functions
|
// Sectors API functions
|
||||||
@@ -1420,7 +1469,14 @@ export interface UsageSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCreditBalance(): Promise<CreditBalance> {
|
export async function fetchCreditBalance(): Promise<CreditBalance> {
|
||||||
return fetchAPI('/v1/billing/credits/balance/balance/');
|
const response = await fetchAPI('/v1/billing/credits/balance/balance/');
|
||||||
|
// fetchAPI automatically extracts data field from unified format
|
||||||
|
return response || {
|
||||||
|
credits: 0,
|
||||||
|
plan_credits_per_month: 0,
|
||||||
|
credits_used_this_month: 0,
|
||||||
|
credits_remaining: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCreditUsage(filters?: {
|
export async function fetchCreditUsage(filters?: {
|
||||||
@@ -1445,11 +1501,8 @@ export async function fetchUsageSummary(startDate?: string, endDate?: string): P
|
|||||||
if (endDate) params.append('end_date', endDate);
|
if (endDate) params.append('end_date', endDate);
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const response = await fetchAPI(`/v1/billing/credits/usage/summary/${queryString ? `?${queryString}` : ''}`);
|
// fetchAPI automatically extracts data field from unified format
|
||||||
// Extract data field from unified API response format
|
return fetchAPI(`/v1/billing/credits/usage/summary/${queryString ? `?${queryString}` : ''}`);
|
||||||
// Response format: { success: true, data: {...}, request_id: "..." }
|
|
||||||
const summaryData = response?.data || response;
|
|
||||||
return summaryData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LimitCard {
|
export interface LimitCard {
|
||||||
|
|||||||
Reference in New Issue
Block a user