Files
cannaiq/frontend/src/lib/api.ts
Kelly 3861a31a3b Add crawler scheduler, orchestrator, and multi-category intelligence
- Add scheduler UI with store schedules, job queue, and global settings
- Add store crawl orchestrator for intelligent crawl workflow
- Add multi-category intelligence detection (product, specials, brands, metadata)
- Add CrawlerLogger for structured JSON logging
- Add migrations for scheduler tables and dispensary linking
- Add dispensary → scheduler navigation link
- Support production/sandbox crawler modes per provider

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 09:29:15 -07:00

490 lines
14 KiB
TypeScript
Executable File

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3010';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private getHeaders(): HeadersInit {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...this.getHeaders(),
...options?.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Request failed' }));
throw new Error(error.error || `HTTP ${response.status}`);
}
return response.json();
}
// Auth
async login(email: string, password: string) {
return this.request<{ token: string; user: any }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
async getMe() {
return this.request<{ user: any }>('/api/auth/me');
}
// Dashboard
async getDashboardStats() {
return this.request<any>('/api/dashboard/stats');
}
async getDashboardActivity() {
return this.request<any>('/api/dashboard/activity');
}
// Stores
async getStores() {
return this.request<{ stores: any[] }>('/api/stores');
}
async getStore(id: number) {
return this.request<any>(`/api/stores/${id}`);
}
async createStore(data: any) {
return this.request<any>('/api/stores', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateStore(id: number, data: any) {
return this.request<any>(`/api/stores/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async getDispensaries() {
return this.request<{ dispensaries: any[] }>('/api/dispensaries');
}
async getDispensary(slug: string) {
return this.request<any>(`/api/dispensaries/${slug}`);
}
async updateDispensary(id: number, data: any) {
return this.request<any>(`/api/dispensaries/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteStore(id: number) {
return this.request<any>(`/api/stores/${id}`, {
method: 'DELETE',
});
}
async scrapeStore(id: number, parallel?: number, userAgent?: string) {
return this.request<any>(`/api/stores/${id}/scrape`, {
method: 'POST',
body: JSON.stringify({ parallel, userAgent }),
});
}
async downloadStoreImages(id: number) {
return this.request<any>(`/api/stores/${id}/download-images`, {
method: 'POST',
});
}
async discoverStoreCategories(id: number) {
return this.request<any>(`/api/stores/${id}/discover-categories`, {
method: 'POST',
});
}
async debugScrapeStore(id: number) {
return this.request<any>(`/api/stores/${id}/debug-scrape`, {
method: 'POST',
});
}
async getStoreBrands(id: number) {
return this.request<{ brands: string[] }>(`/api/stores/${id}/brands`);
}
async getStoreSpecials(id: number, date?: string) {
const params = date ? `?date=${date}` : '';
return this.request<{ specials: any[]; date: string }>(`/api/stores/${id}/specials${params}`);
}
// Categories
async getCategories(storeId?: number) {
const params = storeId ? `?store_id=${storeId}` : '';
return this.request<{ categories: any[] }>(`/api/categories${params}`);
}
async getCategoryTree(storeId: number) {
return this.request<any>(`/api/categories/tree?store_id=${storeId}`);
}
// Products
async getProducts(params?: any) {
const query = new URLSearchParams(params).toString();
return this.request<any>(`/api/products${query ? `?${query}` : ''}`);
}
async getProduct(id: number) {
return this.request<any>(`/api/products/${id}`);
}
// Campaigns
async getCampaigns() {
return this.request<{ campaigns: any[] }>('/api/campaigns');
}
async getCampaign(id: number) {
return this.request<any>(`/api/campaigns/${id}`);
}
async createCampaign(data: any) {
return this.request<any>('/api/campaigns', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateCampaign(id: number, data: any) {
return this.request<any>(`/api/campaigns/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteCampaign(id: number) {
return this.request<any>(`/api/campaigns/${id}`, {
method: 'DELETE',
});
}
async addProductToCampaign(campaignId: number, productId: number, displayOrder?: number) {
return this.request<any>(`/api/campaigns/${campaignId}/products`, {
method: 'POST',
body: JSON.stringify({ product_id: productId, display_order: displayOrder }),
});
}
async removeProductFromCampaign(campaignId: number, productId: number) {
return this.request<any>(`/api/campaigns/${campaignId}/products/${productId}`, {
method: 'DELETE',
});
}
// Analytics
async getAnalyticsOverview(days?: number) {
return this.request<any>(`/api/analytics/overview${days ? `?days=${days}` : ''}`);
}
async getProductAnalytics(id: number, days?: number) {
return this.request<any>(`/api/analytics/products/${id}${days ? `?days=${days}` : ''}`);
}
async getCampaignAnalytics(id: number, days?: number) {
return this.request<any>(`/api/analytics/campaigns/${id}${days ? `?days=${days}` : ''}`);
}
// Settings
async getSettings() {
return this.request<{ settings: any[] }>('/api/settings');
}
async updateSetting(key: string, value: string) {
return this.request<any>(`/api/settings/${key}`, {
method: 'PUT',
body: JSON.stringify({ value }),
});
}
async updateSettings(settings: Array<{ key: string; value: string }>) {
return this.request<any>('/api/settings', {
method: 'PUT',
body: JSON.stringify({ settings }),
});
}
// Proxies
async getProxies() {
return this.request<{ proxies: any[] }>('/api/proxies');
}
async getProxy(id: number) {
return this.request<any>(`/api/proxies/${id}`);
}
async addProxy(data: any) {
return this.request<any>('/api/proxies', {
method: 'POST',
body: JSON.stringify(data),
});
}
async addProxiesBulk(proxies: any[]) {
return this.request<any>('/api/proxies/bulk', {
method: 'POST',
body: JSON.stringify({ proxies }),
});
}
async testProxy(id: number) {
return this.request<any>(`/api/proxies/${id}/test`, {
method: 'POST',
});
}
async testAllProxies() {
return this.request<{ jobId: number; message: string }>('/api/proxies/test-all', {
method: 'POST',
});
}
async getProxyTestJob(jobId: number) {
return this.request<{ job: any }>(`/api/proxies/test-job/${jobId}`);
}
async getActiveProxyTestJob() {
return this.request<{ job: any }>('/api/proxies/test-job');
}
async cancelProxyTestJob(jobId: number) {
return this.request<{ message: string }>(`/api/proxies/test-job/${jobId}/cancel`, {
method: 'POST',
});
}
async updateProxy(id: number, data: any) {
return this.request<any>(`/api/proxies/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteProxy(id: number) {
return this.request<any>(`/api/proxies/${id}`, {
method: 'DELETE',
});
}
async updateProxyLocations() {
return this.request<{ message: string }>('/api/proxies/update-locations', {
method: 'POST',
});
}
// Logs
async getLogs(limit?: number, level?: string, category?: string) {
const params = new URLSearchParams();
if (limit) params.append('limit', limit.toString());
if (level) params.append('level', level);
if (category) params.append('category', category);
return this.request<{ logs: any[] }>(`/api/logs?${params.toString()}`);
}
async clearLogs() {
return this.request<any>('/api/logs', {
method: 'DELETE',
});
}
// Scraper Monitor
async getActiveScrapers() {
return this.request<any>('/api/scraper-monitor/active');
}
async getScraperHistory(storeId?: number) {
const params = storeId ? `?store_id=${storeId}` : '';
return this.request<any>(`/api/scraper-monitor/history${params}`);
}
async getJobStats(dispensaryId?: number) {
const params = dispensaryId ? `?dispensary_id=${dispensaryId}` : '';
return this.request<any>(`/api/scraper-monitor/jobs/stats${params}`);
}
async getActiveJobs(dispensaryId?: number) {
const params = dispensaryId ? `?dispensary_id=${dispensaryId}` : '';
return this.request<any>(`/api/scraper-monitor/jobs/active${params}`);
}
async getRecentJobs(options?: { limit?: number; dispensaryId?: number; status?: string }) {
const params = new URLSearchParams();
if (options?.limit) params.append('limit', options.limit.toString());
if (options?.dispensaryId) params.append('dispensary_id', options.dispensaryId.toString());
if (options?.status) params.append('status', options.status);
const queryString = params.toString() ? `?${params.toString()}` : '';
return this.request<any>(`/api/scraper-monitor/jobs/recent${queryString}`);
}
async getWorkerStats(dispensaryId?: number) {
const params = dispensaryId ? `?dispensary_id=${dispensaryId}` : '';
return this.request<any>(`/api/scraper-monitor/jobs/workers${params}`);
}
// Change Approval
async getChanges(status?: 'pending' | 'approved' | 'rejected') {
const params = status ? `?status=${status}` : '';
return this.request<{ changes: any[] }>(`/api/changes${params}`);
}
async getChangeStats() {
return this.request<{
pending_count: number;
pending_recrawl_count: number;
approved_count: number;
rejected_count: number;
}>('/api/changes/stats');
}
async approveChange(changeId: number) {
return this.request<{
message: string;
dispensary: any;
requires_recrawl: boolean;
}>(`/api/changes/${changeId}/approve`, {
method: 'POST',
});
}
async rejectChange(changeId: number, reason?: string) {
return this.request<{
message: string;
change: any;
}>(`/api/changes/${changeId}/reject`, {
method: 'POST',
body: JSON.stringify({ reason }),
});
}
// Dispensary Products, Brands, Specials
async getDispensaryProducts(slug: string, category?: number) {
const params = category ? `?category=${category}` : '';
return this.request<{ products: any[] }>(`/api/dispensaries/${slug}/products${params}`);
}
async getDispensaryBrands(slug: string) {
return this.request<{ brands: Array<{ brand: string; product_count: number }> }>(`/api/dispensaries/${slug}/brands`);
}
async getDispensarySpecials(slug: string) {
return this.request<{ specials: any[] }>(`/api/dispensaries/${slug}/specials`);
}
// API Permissions
async getApiPermissions() {
return this.request<{ permissions: any[] }>('/api/api-permissions');
}
async createApiPermission(data: { user_name: string; allowed_ips?: string; allowed_domains?: string }) {
return this.request<{ permission: any; message: string }>('/api/api-permissions', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateApiPermission(id: number, data: { user_name?: string; allowed_ips?: string; allowed_domains?: string; is_active?: number }) {
return this.request<{ permission: any }>(`/api/api-permissions/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async toggleApiPermission(id: number) {
return this.request<{ permission: any }>(`/api/api-permissions/${id}/toggle`, {
method: 'PATCH',
});
}
async deleteApiPermission(id: number) {
return this.request<{ message: string }>(`/api/api-permissions/${id}`, {
method: 'DELETE',
});
}
// Crawler Schedule
async getGlobalSchedule() {
return this.request<{ schedules: any[] }>('/api/schedule/global');
}
async updateGlobalSchedule(type: string, data: { enabled?: boolean; interval_hours?: number; run_time?: string }) {
return this.request<{ schedule: any; message: string }>(`/api/schedule/global/${type}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async getStoreSchedules() {
return this.request<{ stores: any[] }>('/api/schedule/stores');
}
async getStoreSchedule(storeId: number) {
return this.request<{ schedule: any }>(`/api/schedule/stores/${storeId}`);
}
async updateStoreSchedule(storeId: number, data: any) {
return this.request<{ schedule: any }>(`/api/schedule/stores/${storeId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async getCrawlJobs(limit?: number) {
const params = limit ? `?limit=${limit}` : '';
return this.request<{ jobs: any[] }>(`/api/schedule/jobs${params}`);
}
async getStoreCrawlJobs(storeId: number, limit?: number) {
const params = limit ? `?limit=${limit}` : '';
return this.request<{ jobs: any[] }>(`/api/schedule/jobs/store/${storeId}${params}`);
}
async cancelCrawlJob(jobId: number) {
return this.request<{ success: boolean; message: string }>(`/api/schedule/jobs/${jobId}/cancel`, {
method: 'POST',
});
}
async triggerStoreCrawl(storeId: number) {
return this.request<{ job: any; message: string }>(`/api/schedule/trigger/store/${storeId}`, {
method: 'POST',
});
}
async triggerAllCrawls() {
return this.request<{ jobs_created: number; message: string }>('/api/schedule/trigger/all', {
method: 'POST',
});
}
async restartScheduler() {
return this.request<{ message: string }>('/api/schedule/restart', {
method: 'POST',
});
}
}
export const api = new ApiClient(API_URL);