Expose AZ data under neutral /api/az and update frontend routes

This commit is contained in:
Kelly
2025-12-02 10:19:44 -07:00
parent 62ecbed076
commit 53a0918233
6 changed files with 43 additions and 36 deletions

View File

@@ -49,9 +49,9 @@ export default function App() {
<Route path="/scraper-tools" element={<PrivateRoute><ScraperTools /></PrivateRoute>} /> <Route path="/scraper-tools" element={<PrivateRoute><ScraperTools /></PrivateRoute>} />
<Route path="/scraper-monitor" element={<PrivateRoute><ScraperMonitor /></PrivateRoute>} /> <Route path="/scraper-monitor" element={<PrivateRoute><ScraperMonitor /></PrivateRoute>} />
<Route path="/scraper-schedule" element={<PrivateRoute><ScraperSchedule /></PrivateRoute>} /> <Route path="/scraper-schedule" element={<PrivateRoute><ScraperSchedule /></PrivateRoute>} />
<Route path="/dutchie-az-schedule" element={<PrivateRoute><DutchieAZSchedule /></PrivateRoute>} /> <Route path="/az-schedule" element={<PrivateRoute><DutchieAZSchedule /></PrivateRoute>} />
<Route path="/dutchie-az" element={<PrivateRoute><DutchieAZStores /></PrivateRoute>} /> <Route path="/az" element={<PrivateRoute><DutchieAZStores /></PrivateRoute>} />
<Route path="/dutchie-az/stores/:id" element={<PrivateRoute><DutchieAZStoreDetail /></PrivateRoute>} /> <Route path="/az/stores/:id" element={<PrivateRoute><DutchieAZStoreDetail /></PrivateRoute>} />
<Route path="/api-permissions" element={<PrivateRoute><ApiPermissions /></PrivateRoute>} /> <Route path="/api-permissions" element={<PrivateRoute><ApiPermissions /></PrivateRoute>} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>

View File

@@ -163,18 +163,18 @@ export function Layout({ children }: LayoutProps) {
/> />
</NavSection> </NavSection>
<NavSection title="Dutchie AZ"> <NavSection title="AZ Data">
<NavLink <NavLink
to="/dutchie-az" to="/az"
icon={<Store className="w-4 h-4" />} icon={<Store className="w-4 h-4" />}
label="AZ Stores" label="AZ Stores"
isActive={isActive('/dutchie-az', false)} isActive={isActive('/az', false)}
/> />
<NavLink <NavLink
to="/dutchie-az-schedule" to="/az-schedule"
icon={<Calendar className="w-4 h-4" />} icon={<Calendar className="w-4 h-4" />}
label="AZ Schedule" label="AZ Schedule"
isActive={isActive('/dutchie-az-schedule')} isActive={isActive('/az-schedule')}
/> />
</NavSection> </NavSection>

View File

@@ -527,7 +527,7 @@ class ApiClient {
} }
// ============================================================ // ============================================================
// DUTCHIE AZ API // AZ DATA API (formerly dutchie-az)
// ============================================================ // ============================================================
// Dutchie AZ Dashboard // Dutchie AZ Dashboard
@@ -540,7 +540,7 @@ class ApiClient {
failedJobCount: number; failedJobCount: number;
brandCount: number; brandCount: number;
categoryCount: number; categoryCount: number;
}>('/api/dutchie-az/dashboard'); }>('/api/az/dashboard');
} }
// Dutchie AZ Schedules (CRUD) // Dutchie AZ Schedules (CRUD)
@@ -562,7 +562,7 @@ class ApiClient {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}>; }>;
}>('/api/dutchie-az/admin/schedules'); }>('/api/az/admin/schedules');
} }
async getDutchieAZSchedule(id: number) { async getDutchieAZSchedule(id: number) {
@@ -581,7 +581,7 @@ class ApiClient {
jobConfig: Record<string, any> | null; jobConfig: Record<string, any> | null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}>(`/api/dutchie-az/admin/schedules/${id}`); }>(`/api/az/admin/schedules/${id}`);
} }
async createDutchieAZSchedule(data: { async createDutchieAZSchedule(data: {
@@ -593,7 +593,7 @@ class ApiClient {
jobConfig?: Record<string, any>; jobConfig?: Record<string, any>;
startImmediately?: boolean; startImmediately?: boolean;
}) { }) {
return this.request<any>('/api/dutchie-az/admin/schedules', { return this.request<any>('/api/az/admin/schedules', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@@ -606,26 +606,26 @@ class ApiClient {
jitterMinutes?: number; jitterMinutes?: number;
jobConfig?: Record<string, any>; jobConfig?: Record<string, any>;
}) { }) {
return this.request<any>(`/api/dutchie-az/admin/schedules/${id}`, { return this.request<any>(`/api/az/admin/schedules/${id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteDutchieAZSchedule(id: number) { async deleteDutchieAZSchedule(id: number) {
return this.request<{ success: boolean; message: string }>(`/api/dutchie-az/admin/schedules/${id}`, { return this.request<{ success: boolean; message: string }>(`/api/az/admin/schedules/${id}`, {
method: 'DELETE', method: 'DELETE',
}); });
} }
async triggerDutchieAZSchedule(id: number) { async triggerDutchieAZSchedule(id: number) {
return this.request<{ success: boolean; message: string }>(`/api/dutchie-az/admin/schedules/${id}/trigger`, { return this.request<{ success: boolean; message: string }>(`/api/az/admin/schedules/${id}/trigger`, {
method: 'POST', method: 'POST',
}); });
} }
async initDutchieAZSchedules() { async initDutchieAZSchedules() {
return this.request<{ success: boolean; schedules: any[] }>('/api/dutchie-az/admin/schedules/init', { return this.request<{ success: boolean; schedules: any[] }>('/api/az/admin/schedules/init', {
method: 'POST', method: 'POST',
}); });
} }
@@ -636,7 +636,7 @@ class ApiClient {
if (limit) params.append('limit', limit.toString()); if (limit) params.append('limit', limit.toString());
if (offset) params.append('offset', offset.toString()); if (offset) params.append('offset', offset.toString());
const queryString = params.toString() ? `?${params.toString()}` : ''; const queryString = params.toString() ? `?${params.toString()}` : '';
return this.request<{ logs: any[]; total: number }>(`/api/dutchie-az/admin/schedules/${scheduleId}/logs${queryString}`); return this.request<{ logs: any[]; total: number }>(`/api/az/admin/schedules/${scheduleId}/logs${queryString}`);
} }
async getDutchieAZRunLogs(options?: { scheduleId?: number; jobName?: string; limit?: number; offset?: number }) { async getDutchieAZRunLogs(options?: { scheduleId?: number; jobName?: string; limit?: number; offset?: number }) {
@@ -646,28 +646,28 @@ class ApiClient {
if (options?.limit) params.append('limit', options.limit.toString()); if (options?.limit) params.append('limit', options.limit.toString());
if (options?.offset) params.append('offset', options.offset.toString()); if (options?.offset) params.append('offset', options.offset.toString());
const queryString = params.toString() ? `?${params.toString()}` : ''; const queryString = params.toString() ? `?${params.toString()}` : '';
return this.request<{ logs: any[]; total: number }>(`/api/dutchie-az/admin/run-logs${queryString}`); return this.request<{ logs: any[]; total: number }>(`/api/az/admin/run-logs${queryString}`);
} }
// Dutchie AZ Scheduler Control // Dutchie AZ Scheduler Control
async getDutchieAZSchedulerStatus() { async getDutchieAZSchedulerStatus() {
return this.request<{ running: boolean; pollIntervalMs: number }>('/api/dutchie-az/admin/scheduler/status'); return this.request<{ running: boolean; pollIntervalMs: number }>('/api/az/admin/scheduler/status');
} }
async startDutchieAZScheduler() { async startDutchieAZScheduler() {
return this.request<{ success: boolean; message: string }>('/api/dutchie-az/admin/scheduler/start', { return this.request<{ success: boolean; message: string }>('/api/az/admin/scheduler/start', {
method: 'POST', method: 'POST',
}); });
} }
async stopDutchieAZScheduler() { async stopDutchieAZScheduler() {
return this.request<{ success: boolean; message: string }>('/api/dutchie-az/admin/scheduler/stop', { return this.request<{ success: boolean; message: string }>('/api/az/admin/scheduler/stop', {
method: 'POST', method: 'POST',
}); });
} }
async triggerDutchieAZImmediateCrawl() { async triggerDutchieAZImmediateCrawl() {
return this.request<{ success: boolean; message: string }>('/api/dutchie-az/admin/scheduler/trigger', { return this.request<{ success: boolean; message: string }>('/api/az/admin/scheduler/trigger', {
method: 'POST', method: 'POST',
}); });
} }
@@ -680,11 +680,11 @@ class ApiClient {
if (params?.limit) searchParams.append('limit', params.limit.toString()); if (params?.limit) searchParams.append('limit', params.limit.toString());
if (params?.offset) searchParams.append('offset', params.offset.toString()); if (params?.offset) searchParams.append('offset', params.offset.toString());
const queryString = searchParams.toString() ? `?${searchParams.toString()}` : ''; const queryString = searchParams.toString() ? `?${searchParams.toString()}` : '';
return this.request<{ stores: any[]; total: number }>(`/api/dutchie-az/stores${queryString}`); return this.request<{ stores: any[]; total: number }>(`/api/az/stores${queryString}`);
} }
async getDutchieAZStore(id: number) { async getDutchieAZStore(id: number) {
return this.request<any>(`/api/dutchie-az/stores/${id}`); return this.request<any>(`/api/az/stores/${id}`);
} }
async getDutchieAZStoreSummary(id: number) { async getDutchieAZStoreSummary(id: number) {
@@ -700,7 +700,7 @@ class ApiClient {
brandCount: number; brandCount: number;
categoryCount: number; categoryCount: number;
lastCrawl: any | null; lastCrawl: any | null;
}>(`/api/dutchie-az/stores/${id}/summary`); }>(`/api/az/stores/${id}/summary`);
} }
async getDutchieAZStoreProducts(id: number, params?: { async getDutchieAZStoreProducts(id: number, params?: {
@@ -752,19 +752,19 @@ class ApiClient {
total: number; total: number;
limit: number; limit: number;
offset: number; offset: number;
}>(`/api/dutchie-az/stores/${id}/products${queryString}`); }>(`/api/az/stores/${id}/products${queryString}`);
} }
async getDutchieAZStoreBrands(id: number) { async getDutchieAZStoreBrands(id: number) {
return this.request<{ return this.request<{
brands: Array<{ brand: string; product_count: number }>; brands: Array<{ brand: string; product_count: number }>;
}>(`/api/dutchie-az/stores/${id}/brands`); }>(`/api/az/stores/${id}/brands`);
} }
async getDutchieAZStoreCategories(id: number) { async getDutchieAZStoreCategories(id: number) {
return this.request<{ return this.request<{
categories: Array<{ type: string; subcategory: string; product_count: number }>; categories: Array<{ type: string; subcategory: string; product_count: number }>;
}>(`/api/dutchie-az/stores/${id}/categories`); }>(`/api/az/stores/${id}/categories`);
} }
// Dutchie AZ Debug // Dutchie AZ Debug
@@ -795,7 +795,7 @@ class ApiClient {
dispensary_name: string; dispensary_name: string;
crawled_at: string; crawled_at: string;
}>; }>;
}>('/api/dutchie-az/debug/summary'); }>('/api/az/debug/summary');
} }
async getDutchieAZDebugStore(id: number) { async getDutchieAZDebugStore(id: number) {
@@ -823,11 +823,11 @@ class ApiClient {
outOfStock: any[]; outOfStock: any[];
}; };
categories: Array<{ type: string; subcategory: string; count: string }>; categories: Array<{ type: string; subcategory: string; count: string }>;
}>(`/api/dutchie-az/debug/store/${id}`); }>(`/api/az/debug/store/${id}`);
} }
async triggerDutchieAZCrawl(id: number, options?: { pricingType?: string; useBothModes?: boolean }) { async triggerDutchieAZCrawl(id: number, options?: { pricingType?: string; useBothModes?: boolean }) {
return this.request<any>(`/api/dutchie-az/admin/crawl/${id}`, { return this.request<any>(`/api/az/admin/crawl/${id}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(options || {}), body: JSON.stringify(options || {}),
}); });

View File

@@ -114,7 +114,14 @@ export function DutchieAZSchedule() {
const handleUpdateSchedule = async (id: number, updates: Partial<JobSchedule>) => { const handleUpdateSchedule = async (id: number, updates: Partial<JobSchedule>) => {
try { try {
await api.updateDutchieAZSchedule(id, updates); const payload = {
description: updates.description ?? undefined,
enabled: updates.enabled,
baseIntervalMinutes: updates.baseIntervalMinutes,
jitterMinutes: updates.jitterMinutes,
jobConfig: updates.jobConfig ?? undefined,
};
await api.updateDutchieAZSchedule(id, payload);
setEditingSchedule(null); setEditingSchedule(null);
await loadData(); await loadData();
} catch (error) { } catch (error) {

View File

@@ -140,11 +140,11 @@ export function DutchieAZStoreDetail() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<button <button
onClick={() => navigate('/dutchie-az')} onClick={() => navigate('/az')}
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900" className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
Back to Dutchie AZ Stores Back to AZ Stores
</button> </button>
{/* Update Button */} {/* Update Button */}

View File

@@ -175,7 +175,7 @@ export function DutchieAZStores() {
</td> </td>
<td> <td>
<button <button
onClick={() => navigate(`/dutchie-az/stores/${store.id}`)} onClick={() => navigate(`/az/stores/${store.id}`)}
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
disabled={!store.platform_dispensary_id} disabled={!store.platform_dispensary_id}
> >