feat(ai): add AI trace tracking and admin panel
- Add AiTrace model to Prisma schema with relations to User - Implement AiTraceService with CRUD operations for AI traces - Add new admin panel for AI traces with filtering and detail views - Integrate trace persistence in receipt import flow - Add API endpoints for listing and retrieving AI traces - Update Flutter admin UI with new AI tab and navigation - Add new domain models for AI traces and details - Add migration for AiTrace table creation BREAKING CHANGE: None
This commit is contained in:
@@ -2,7 +2,7 @@ class AuthApiPaths {
|
||||
static const login = '/auth/login';
|
||||
}
|
||||
|
||||
class ProductApiPaths {
|
||||
class ProductApiPaths {
|
||||
static const list = '/products';
|
||||
static const mine = '/products/mine';
|
||||
static const createPrivate = '/products/private';
|
||||
@@ -13,14 +13,15 @@ class ProductApiPaths {
|
||||
static const deleted = '/products/deleted';
|
||||
static const merge = '/products/merge';
|
||||
static const mergePrivate = '/products/private/merge';
|
||||
static String updateMineCategory(int id) => '/products/mine/$id/category';
|
||||
static const backfillMineCategories = '/products/mine/backfill-categories';
|
||||
static String updateMineCategory(int id) => '/products/mine/$id/category';
|
||||
static const backfillMineCategories = '/products/mine/backfill-categories';
|
||||
static String mergePreview(int sourceProductId, int targetProductId) =>
|
||||
'/products/merge-preview?sourceProductId=$sourceProductId&targetProductId=$targetProductId';
|
||||
static String setStatus(int id) => '/products/$id/status';
|
||||
static String update(int id) => '/products/$id';
|
||||
static String canonicalName(int id) => '/products/$id/canonical-name';
|
||||
static String canonicalNamePrivate(int id) => '/products/private/$id/canonical-name';
|
||||
static String canonicalNamePrivate(int id) =>
|
||||
'/products/private/$id/canonical-name';
|
||||
static String remove(int id) => '/products/$id';
|
||||
static String restore(int id) => '/products/$id/restore';
|
||||
static const bulkUpdate = '/products/bulk-update';
|
||||
@@ -28,37 +29,63 @@ class ProductApiPaths {
|
||||
|
||||
class AiApiPaths {
|
||||
static const models = '/ai/models';
|
||||
static String traces({
|
||||
required String source,
|
||||
int? limit,
|
||||
String? cursor,
|
||||
String? period,
|
||||
bool onlyErrors = false,
|
||||
}) {
|
||||
final params = <String, String>{'source': source};
|
||||
if (limit != null) params['limit'] = '$limit';
|
||||
if (cursor != null && cursor.isNotEmpty) params['cursor'] = cursor;
|
||||
if (period != null && period.isNotEmpty) params['period'] = period;
|
||||
if (onlyErrors) params['onlyErrors'] = 'true';
|
||||
final query = params.entries
|
||||
.map((e) =>
|
||||
'${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}')
|
||||
.join('&');
|
||||
return '/ai/traces?$query';
|
||||
}
|
||||
|
||||
static String traceById(String traceId) =>
|
||||
'/ai/traces/${Uri.encodeComponent(traceId)}';
|
||||
}
|
||||
|
||||
class CategoryApiPaths {
|
||||
static const tree = '/categories/tree';
|
||||
}
|
||||
|
||||
class ReceiptImportApiPaths {
|
||||
static const refreshCategories = '/receipt-import/refresh-categories';
|
||||
static const unitMappings = '/receipt-import/unit-mappings';
|
||||
}
|
||||
|
||||
class FlyerImportApiPaths {
|
||||
static const parse = '/flyer-import/parse';
|
||||
static const latestSession = '/flyer-import/sessions/latest';
|
||||
static String bySession(int sessionId) => '/flyer-import/sessions/$sessionId';
|
||||
static String sourceBySession(int sessionId) => '/flyer-import/sessions/$sessionId/source';
|
||||
static String patchItem(int sessionId, int itemId) => '/flyer-import/sessions/$sessionId/items/$itemId';
|
||||
}
|
||||
|
||||
class FlyerSelectionApiPaths {
|
||||
static String bySession(int sessionId) => '/flyer-sessions/$sessionId/selections';
|
||||
static String bulkBySession(int sessionId) => '/flyer-sessions/$sessionId/selections/bulk';
|
||||
static String planToShoppingListBySession(int sessionId) =>
|
||||
'/flyer-sessions/$sessionId/selections/plan-to-shopping-list';
|
||||
static const open = '/flyer-selections/open';
|
||||
}
|
||||
|
||||
class ShoppingListApiPaths {
|
||||
static const items = '/shopping-list/items';
|
||||
static String updateStatus(int itemId) => '/shopping-list/items/$itemId/status';
|
||||
}
|
||||
class ReceiptImportApiPaths {
|
||||
static const refreshCategories = '/receipt-import/refresh-categories';
|
||||
static const unitMappings = '/receipt-import/unit-mappings';
|
||||
}
|
||||
|
||||
class FlyerImportApiPaths {
|
||||
static const parse = '/flyer-import/parse';
|
||||
static const latestSession = '/flyer-import/sessions/latest';
|
||||
static String bySession(int sessionId) => '/flyer-import/sessions/$sessionId';
|
||||
static String sourceBySession(int sessionId) =>
|
||||
'/flyer-import/sessions/$sessionId/source';
|
||||
static String patchItem(int sessionId, int itemId) =>
|
||||
'/flyer-import/sessions/$sessionId/items/$itemId';
|
||||
}
|
||||
|
||||
class FlyerSelectionApiPaths {
|
||||
static String bySession(int sessionId) =>
|
||||
'/flyer-sessions/$sessionId/selections';
|
||||
static String bulkBySession(int sessionId) =>
|
||||
'/flyer-sessions/$sessionId/selections/bulk';
|
||||
static String planToShoppingListBySession(int sessionId) =>
|
||||
'/flyer-sessions/$sessionId/selections/plan-to-shopping-list';
|
||||
static const open = '/flyer-selections/open';
|
||||
}
|
||||
|
||||
class ShoppingListApiPaths {
|
||||
static const items = '/shopping-list/items';
|
||||
static String updateStatus(int itemId) =>
|
||||
'/shopping-list/items/$itemId/status';
|
||||
}
|
||||
|
||||
class HelpTextApiPaths {
|
||||
static String byKey(String key) => '/help-texts/${Uri.encodeComponent(key)}';
|
||||
@@ -77,7 +104,8 @@ class RecipeApiPaths {
|
||||
static String remove(int id) => '/recipes/$id';
|
||||
static String setVisibility(int id) => '/recipes/$id/visibility';
|
||||
static String share(int id) => '/recipes/$id/share';
|
||||
static String unshare(int id, String username) => '/recipes/$id/share/${Uri.encodeComponent(username)}';
|
||||
static String unshare(int id, String username) =>
|
||||
'/recipes/$id/share/${Uri.encodeComponent(username)}';
|
||||
static String inventoryPreview(int id) => '/recipes/$id/inventory-preview';
|
||||
static String analysis(int id) => '/recipes/$id/analysis';
|
||||
static String rematch(int id) => '/recipes/$id/rematch';
|
||||
@@ -92,9 +120,11 @@ class InventoryApiPaths {
|
||||
static String update(int id) => '/inventory/$id';
|
||||
static String remove(int id) => '/inventory/$id';
|
||||
static String moveToPantry(int id) => '/inventory/$id/move-to-pantry';
|
||||
static String moveToPantryAdmin(int id) => '/inventory/admin/$id/move-to-pantry';
|
||||
static String moveToPantryAdmin(int id) =>
|
||||
'/inventory/admin/$id/move-to-pantry';
|
||||
static String consume(int id) => '/inventory/$id/consume';
|
||||
static String consumptionHistory(int id) => '/inventory/$id/consumption-history';
|
||||
static String consumptionHistory(int id) =>
|
||||
'/inventory/$id/consumption-history';
|
||||
}
|
||||
|
||||
class AdminInventoryApiPaths {
|
||||
@@ -105,10 +135,12 @@ class AdminInventoryApiPaths {
|
||||
if (sort != null && sort.isNotEmpty) params['sort'] = sort;
|
||||
if (params.isEmpty) return list;
|
||||
final query = params.entries
|
||||
.map((e) => '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}')
|
||||
.map((e) =>
|
||||
'${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}')
|
||||
.join('&');
|
||||
return '$list?$query';
|
||||
}
|
||||
|
||||
static String update(int id) => '/inventory/admin/$id';
|
||||
static String remove(int id) => '/inventory/admin/$id';
|
||||
static String moveToPantry(int id) => '/inventory/admin/$id/move-to-pantry';
|
||||
@@ -121,7 +153,8 @@ class PantryApiPaths {
|
||||
static const list = '/pantry';
|
||||
static String remove(int id) => '/pantry/$id';
|
||||
static String moveToInventory(int id) => '/pantry/$id/move-to-inventory';
|
||||
static String moveToInventoryAdmin(int id) => '/pantry/admin/$id/move-to-inventory';
|
||||
static String moveToInventoryAdmin(int id) =>
|
||||
'/pantry/admin/$id/move-to-inventory';
|
||||
static const adminList = '/pantry/admin';
|
||||
static const adminCreate = '/pantry/admin';
|
||||
static String adminUpdate(int id) => '/pantry/admin/$id';
|
||||
@@ -143,14 +176,14 @@ class MealPlanApiPaths {
|
||||
static const list = '/meal-plan';
|
||||
|
||||
static String listByRange(String from, String to) =>
|
||||
'$list?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
'$list?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
|
||||
static String shoppingList(String from, String to) =>
|
||||
'$list/shopping-list?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
'$list/shopping-list?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
|
||||
static String inventoryCompare(String from, String to) =>
|
||||
'$list/inventory-compare?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
'$list/inventory-compare?from=${Uri.encodeQueryComponent(from)}&to=${Uri.encodeQueryComponent(to)}';
|
||||
|
||||
static String removeByDate(String date) =>
|
||||
'$list/${Uri.encodeComponent(date)}';
|
||||
}
|
||||
'$list/${Uri.encodeComponent(date)}';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user