diff --git a/.gitignore b/.gitignore index 5837d48..229f647 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ debug_page.txt .github/agents .github/schemas .github/skills + +prompts/ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8499d49 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,403 @@ +# Architecture Documentation + +**Last Updated:** 2026-02-15T00:00:00.000Z +**JIRA:** RECIPE-0001 + +--- + +## Overview + +**Project Name:** InstaRecipe +**Type:** Progressive Web Application (PWA) +**Primary Language:** TypeScript +**Framework:** SvelteKit 2.x with Svelte 5 +**Runtime:** Node.js 22+ + +### Purpose + +A modern web application that extracts recipes from Instagram posts and saves them to Tandoor Recipe Manager using an async queue-based processing system. + +--- + +## Project Structure + +``` +insta-recipe/ +├── src/ # Source code +│ ├── lib/ # Library code +│ │ ├── client/ # Client-side modules +│ │ │ ├── PushNotificationManager.ts +│ │ │ ├── PWAInstallManager.ts +│ │ │ └── ServiceWorkerMessageHandler.ts +│ │ ├── server/ # Server-side modules +│ │ │ ├── api/ # API utilities (errors, handlers) +│ │ │ ├── browser.ts # Playwright browser management +│ │ │ ├── extraction.ts # Instagram content extraction +│ │ │ ├── llm.ts # LLM integration (OpenAI) +│ │ │ ├── notifications/ # Push notification service +│ │ │ ├── parser.ts # Recipe parsing with LLM +│ │ │ ├── prompts/ # LLM prompts +│ │ │ ├── queue/ # Queue management system +│ │ │ │ ├── QueueManager.ts +│ │ │ │ ├── QueueProcessor.ts +│ │ │ │ ├── config.ts +│ │ │ │ └── types.ts +│ │ │ ├── scheduler.ts # Background task scheduler +│ │ │ ├── tandoor.ts # Tandoor API integration +│ │ │ ├── tandoor-config.ts +│ │ │ └── validation/ # Input validation +│ │ ├── assets/ # Static assets +│ │ └── index.ts +│ ├── routes/ # SvelteKit routes +│ │ ├── api/ # API endpoints +│ │ │ ├── extract/ # Legacy extraction endpoint (deprecated) +│ │ │ ├── health/ # Health check +│ │ │ ├── llm-health/ # LLM health check +│ │ │ ├── notifications/ # Push notification endpoints +│ │ │ ├── queue/ # Queue management API +│ │ │ │ ├── [id]/ # Individual queue item operations +│ │ │ │ └── stream/ # SSE for real-time updates +│ │ │ ├── tandoor/ # Tandoor integration +│ │ │ ├── tandoor-config/ +│ │ │ └── thumbnail/ +│ │ ├── components/ # Shared components +│ │ ├── share/ # Share target page +│ │ │ └── components/ # Share-specific components +│ │ ├── +layout.svelte # Root layout +│ │ └── +page.svelte # Queue dashboard (home) +│ ├── tests/ # Test files +│ ├── app.d.ts # Type definitions +│ ├── app.html # HTML template +│ ├── app.server.ts # Server initialization +│ ├── hooks.server.ts # SvelteKit server hooks +│ └── service-worker.ts # Service worker for PWA +├── build/ # Build output +├── docs/ # Documentation +│ ├── plans/ # Implementation plans +│ └── outcomes/ # Implementation outcomes +├── scripts/ # Utility scripts +├── static/ # Static files +├── .ssl/ # SSL certificates (local dev) +├── docker-compose.yml # Docker configuration +├── Dockerfile # Container image +├── package.json # Dependencies +├── svelte.config.js # SvelteKit configuration +├── tsconfig.json # TypeScript configuration +└── vite.config.ts # Vite configuration +``` + +--- + +## Key Directories + +### `/src/lib/server/` +Server-side business logic following Hexagonal Architecture principles. Contains domain logic, adapters for external systems (Instagram, Tandoor, LLM), and port definitions. + +### `/src/lib/client/` +Client-side utilities for PWA features (push notifications, install prompts, service worker messaging). + +### `/src/routes/api/` +RESTful API endpoints implemented as SvelteKit server routes. Each directory contains `+server.ts` files exporting HTTP verb handlers. + +### `/src/routes/share/` +Share target page allowing users to share Instagram URLs directly from their browser or mobile apps. + +### `/src/lib/server/queue/` +Queue management system with in-memory storage, processor workers, and type definitions. + +### `/docs/` +Comprehensive documentation including plans, outcomes, API specs, and migration guides. + +--- + +## Design Patterns + +### Singleton Pattern +Used for shared service instances: +- `QueueManager` (`queueManager` exported instance) +- `QueueProcessor` (`queueProcessor` exported instance) +- `PushNotificationService` (`pushNotificationService` exported instance) +- `ServiceWorkerMessageHandler` (`serviceWorkerMessageHandler` exported instance) + +### Factory Pattern +Used for creating configured instances: +- `createLLM()` - Creates OpenAI client with environment configuration +- `createBrowserContext()` - Creates Playwright browser context with options +- `initializeBrowser()` - Initializes Chromium browser instance + +### Observer Pattern +Implemented in QueueManager for real-time updates: +- Subscribers receive notifications on queue item changes +- Server-Sent Events (SSE) stream queue updates to clients +- Push notifications notify users of completion events + +### Adapter Pattern (Hexagonal Architecture) +External systems accessed via adapters: +- **Instagram Adapter**: `extraction.ts` - Extracts content via Playwright +- **LLM Adapter**: `llm.ts`, `parser.ts` - Recipe parsing via OpenAI +- **Tandoor Adapter**: `tandoor.ts` - Recipe management system integration +- **Browser Adapter**: `browser.ts` - Playwright browser automation + +### Strategy Pattern +Multiple extraction strategies with fallback: +1. Embedded JSON extraction +2. DOM selector extraction +3. GraphQL API extraction +4. Legacy extraction method + +--- + +## Key Components + +### Queue Management System +**Location**: `src/lib/server/queue/` + +Three-phase processing pipeline: +1. **Extraction Phase**: Extract text and thumbnail from Instagram +2. **Parsing Phase**: Parse recipe using LLM +3. **Uploading Phase**: Upload to Tandoor (if enabled) + +**Components**: +- `QueueManager`: In-memory FIFO queue with CRUD operations +- `QueueProcessor`: Worker that processes items with configurable concurrency +- `types.ts`: Comprehensive type definitions for queue items and updates + +### API Layer +**Location**: `src/routes/api/` + +RESTful endpoints for: +- Queue operations (`POST /api/queue`, `GET /api/queue`, `GET /api/queue/[id]`) +- Real-time updates (`GET /api/queue/stream` - SSE) +- Push notifications (`POST /api/notifications/subscribe`) +- Health checks (`GET /api/health`, `GET /api/llm-health`) + +### Client-Side Services +**Location**: `src/lib/client/` + +- **PushNotificationManager**: Manages Web Push API subscriptions +- **PWAInstallManager**: Handles PWA install prompts +- **ServiceWorkerMessageHandler**: Processes service worker messages + +### Instagram Extraction +**Location**: `src/lib/server/extraction.ts` + +Multi-method extraction with intelligent fallback: +- Progress callbacks for real-time feedback +- Retry logic with configurable attempts +- Thumbnail extraction and validation + +### LLM Integration +**Location**: `src/lib/server/parser.ts`, `src/lib/server/llm.ts` + +- Recipe detection endpoint +- Structured extraction using OpenAI with Zod schemas +- Configurable model and temperature settings + +--- + +## Dependencies + +### Production Dependencies +- **@types/uuid** (^10.0.0) - UUID type definitions +- **date-fns** (^4.1.0) - Date utility library +- **openai** (^4.20.0) - OpenAI API client +- **playwright** (^1.56.1) - Browser automation +- **uuid** (^13.0.0) - Unique ID generation +- **zod** (^3.23.0) - Schema validation + +### Development Dependencies +- **@sveltejs/kit** (^2.48.5) - SvelteKit framework +- **@sveltejs/adapter-node** (^5.4.0) - Node.js adapter +- **svelte** (^5.43.8) - Svelte 5 framework +- **typescript** (^5.9.3) - TypeScript compiler +- **vite** (^6.0.0) - Build tool +- **vitest** (^4.0.10) - Testing framework +- **@vitest/browser-playwright** (^4.0.10) - Browser testing +- **tailwindcss** (^4.1.17) - CSS framework +- **eslint** (^9.39.1) - Linting +- **prettier** (^3.6.2) - Code formatting +- **typescript-eslint** (^8.47.0) - TypeScript ESLint + +--- + +## Module Organization + +### SvelteKit Path Aliases +- `$lib` → `src/lib/` +- `$lib/*` → `src/lib/*` +- `$app/*` → SvelteKit app imports +- `$env/dynamic/private` → Environment variables (server-side) + +### Directory Structure Conventions +- **Server-only code**: `src/lib/server/` (not bundled to client) +- **Client-only code**: `src/lib/client/` (not executed on server) +- **Shared code**: `src/lib/` (available to both) +- **Routes**: `src/routes/` (file-based routing) +- **Tests**: Colocated with source files (`*.spec.ts`, `*.test.ts`) + +--- + +## Data Flow + +### Recipe Extraction Flow +``` +User submits URL + ↓ +POST /api/queue + ↓ +QueueManager.enqueue() + ↓ +QueueProcessor picks up item + ↓ +Phase 1: extractTextAndThumbnail() + ↓ +Phase 2: extractRecipe() (LLM) + ↓ +Phase 3: uploadRecipeWithIngredientsDTO() (Tandoor) + ↓ +Push notification sent + ↓ +SSE updates notify client +``` + +### Real-time Updates Flow +``` +Client connects to GET /api/queue/stream (SSE) + ↓ +QueueManager.subscribe(callback) + ↓ +Queue item changes trigger callback + ↓ +SSE sends event to client + ↓ +Client updates UI reactively +``` + +### Push Notification Flow +``` +Client requests permission + ↓ +POST /api/notifications/subscribe (with subscription) + ↓ +PushNotificationService stores subscription + ↓ +Queue item completes + ↓ +PushNotificationService.sendNotification() + ↓ +Service worker receives push event + ↓ +Notification displayed to user +``` + +--- + +## Build System + +### Build Command +```bash +npm run build +``` + +Generates production-ready build in `build/` directory using: +- Vite for bundling +- `@sveltejs/adapter-node` for Node.js deployment +- TypeScript compilation +- SvelteKit prerendering and optimization + +### Test Command +```bash +npm test +``` + +Runs test suite using Vitest with two projects: +1. **Server tests**: Node environment for server-side code +2. **Client tests**: Playwright browser for Svelte components + +### Development Server +```bash +npm run dev +``` + +Starts Vite dev server with: +- HTTPS enabled (certificates in `.ssl/`) +- Hot module replacement +- TypeScript checking +- File watching + +### Linting & Formatting +```bash +npm run lint # ESLint + Prettier check +npm run format # Prettier write +``` + +--- + +## Deployment + +### Docker Deployment +Dockerfile includes: +- Node.js 22 Alpine base image +- Playwright Chromium installation +- Production build +- Port 3000 exposure + +Run with: +```bash +docker-compose up +``` + +### Environment Variables +Required configuration: +- `OPENAI_API_KEY` - LLM API access +- `TANDOOR_URL` - Tandoor instance URL (optional) +- `TANDOOR_TOKEN` - Tandoor API token (optional) +- `QUEUE_CONCURRENCY` - Concurrent processing limit (default: 2) +- `QUEUE_MAX_RETRIES` - Failed item retry limit (default: 3) + +--- + +## Testing Architecture + +### Test Categories +1. **Unit Tests**: Individual function testing +2. **Integration Tests**: Multi-component workflows +3. **API Tests**: Endpoint behavior validation +4. **Browser Tests**: Svelte component rendering + +### Test Coverage +138 tests covering: +- Queue management operations +- Instagram URL validation +- SSE streaming +- API endpoints +- Scheduler functionality +- Notification service + +### Test Configuration +- **Server tests**: Node environment with mocked dependencies +- **Client tests**: Playwright Chromium browser with Svelte testing library + +--- + +## Security Considerations + +### SSL/TLS +- Development uses local SSL certificates signed by external Caddy CA +- Certificates stored in `.ssl/` (git-ignored) +- Required for PWA features (Service Worker, Push API) + +### Authentication +- Basic auth for scheduled tasks (username/password from environment) +- Tandoor integration uses bearer token authentication + +### Input Validation +- Instagram URL validation with regex patterns +- Zod schema validation for API payloads +- Error handling with custom error classes + +--- + +**Document Version:** 1.0 +**Generated by:** Initializer Agent +**Next Review:** As needed for architectural changes diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md new file mode 100644 index 0000000..c690c83 --- /dev/null +++ b/docs/CODE_STYLE.md @@ -0,0 +1,792 @@ +# Code Style Guide + +**Last Updated:** 2026-02-15T00:00:00.000Z +**JIRA:** RECIPE-0001 + +--- + +## Language & Version + +**Primary Language:** TypeScript +**Version:** 5.9.3 +**Framework:** SvelteKit 2.48.5 with Svelte 5.43.8 +**Node.js:** 22+ (LTS) + +--- + +## Naming Conventions + +### Files & Directories + +#### SvelteKit Route Files +- Route pages: `+page.svelte` +- Route servers: `+server.ts` +- Route layouts: `+layout.svelte` +- Type definitions: `$types.ts` (auto-generated) + +**Example:** +``` +src/routes/api/queue/ +├── [id]/ +│ ├── +server.ts +│ └── retry/ +│ └── +server.ts +├── stream/ +│ └── +server.ts +└── +server.ts +``` + +#### Library Files +- **PascalCase** for classes and managers: `QueueManager.ts`, `PushNotificationService.ts` +- **kebab-case** for utilities and configs: `tandoor-config.ts`, `instagram-url.ts` +- **lowercase** for general modules: `browser.ts`, `extraction.ts`, `parser.ts` + +**Examples from codebase:** +- `src/lib/server/queue/QueueManager.ts` +- `src/lib/server/tandoor-config.ts` +- `src/lib/client/PushNotificationManager.ts` + +#### Test Files +Pattern: `.spec.ts` or `.test.ts` + +**Examples:** +- `queue-manager.spec.ts` +- `instagram-url-validation.spec.ts` +- `page.svelte.spec.ts` + +### Variables & Functions + +#### Variables +- **camelCase** for local variables and parameters +- **SCREAMING_SNAKE_CASE** for constants + +**Examples:** +```typescript +// From QueueManager.ts +private items: Map = new Map(); +private subscribers: Set = new Set(); + +// From parser.ts +const RECIPE_DETECTION_PROMPT = "..."; +const RECIPE_EXTRACTION_PROMPT = "..."; + +// Local variables +const now = new Date().toISOString(); +const unsubscribe = queueManager.subscribe(callback); +``` + +#### Functions +- **camelCase** for function names +- **Descriptive action verbs**: `enqueue`, `extractRecipe`, `uploadRecipeImage` + +**Examples:** +```typescript +// From QueueManager.ts +enqueue(url: string): QueueItem { ... } +dequeue(): QueueItem | null { ... } +updateStatus(id: string, status: QueueItemStatus): void { ... } + +// From extraction.ts +export async function extractTextAndThumbnail( + url: string, + onProgress?: ProgressCallback +): Promise { ... } + +// From parser.ts +export async function extractRecipe(text: string): Promise { ... } +``` + +### Types & Interfaces + +#### Interfaces & Types +- **PascalCase** for interface names +- Prefix with `I` is **NOT** used +- Exported types use `export type` or `export interface` + +**Examples:** +```typescript +// From queue/types.ts +export interface QueueItem { + id: string; + url: string; + status: QueueItemStatus; + enqueuedAt: string; + // ... +} + +export interface QueueStatusUpdate { + type: string; + itemId: string; + status: QueueItemStatus; + // ... +} + +export type QueueItemStatus = + | 'pending' + | 'in_progress' + | 'completed' + | 'failed'; + +// From extraction.ts +export interface ExtractedContent { + text: string; + thumbnailUrl?: string; +} + +export type ProgressCallback = (event: ProgressEvent) => void; +``` + +#### Zod Schemas +- **PascalCase** with `Schema` suffix +- Inferred types without suffix + +**Examples:** +```typescript +// From parser.ts +const RecipeSchema = z.object({ + name: z.string(), + description: z.string(), + servings: z.number(), + // ... +}); + +export type Recipe = z.infer; + +// From tandoor.ts +const TandoorRecipeSchema = z.object({ + // ... +}); + +export type TandoorRecipe = z.infer; +``` + +### Classes + +#### Class Names +- **PascalCase** for class names +- Descriptive suffixes: `Manager`, `Service`, `Processor`, `Handler` + +**Examples:** +```typescript +// From QueueManager.ts +export class QueueManager { + private items: Map = new Map(); + // ... +} + +// From QueueProcessor.ts +export class QueueProcessor { + private processing: Set = new Set(); + // ... +} + +// From PushNotificationService.ts +class PushNotificationService { + private subscriptions: Map = new Map(); + // ... +} +``` + +#### Singleton Export Pattern +```typescript +// Class definition +export class QueueManager { + // Implementation +} + +// Singleton instance export +export const queueManager = new QueueManager(); +``` + +--- + +## Indentation & Formatting + +### General Rules +- **Indentation:** 2 spaces (enforced by Prettier) +- **No tabs** +- **Max line length:** 100 characters (soft limit, not enforced) +- **Trailing commas:** Yes (multiline) +- **Semicolons:** Yes (always) +- **Single quotes:** Yes (enforced by Prettier) + +### Code Examples + +#### Function Declarations +```typescript +// From QueueManager.ts +enqueue(url: string): QueueItem { + const now = new Date().toISOString(); + const item: QueueItem = { + id: uuidv4(), + url, + status: 'pending', + enqueuedAt: now, + createdAt: now, + updatedAt: now, + phases: [ + { name: 'extraction', status: 'pending' }, + { name: 'parsing', status: 'pending' }, + { name: 'uploading', status: 'pending' } + ], + logs: [], + progressEvents: [], + retryCount: 0, + maxRetries: 3 + }; + + this.items.set(item.id, item); + return item; +} +``` + +#### Async Functions +```typescript +// From extraction.ts +export async function extractTextAndThumbnail( + url: string, + onProgress?: ProgressCallback +): Promise { + const browser = await getBrowser(); + const context = await createBrowserContext(browser); + const page = await context.newPage(); + + try { + await page.goto(url, { waitUntil: 'networkidle' }); + // ... + } finally { + await context.close(); + } +} +``` + +#### Object Destructuring +```typescript +// From route handlers +export const POST: RequestHandler = async ({ request }) => { + const { url } = await request.json(); + // ... +}; + +export const GET: RequestHandler = async ({ params }) => { + const { id } = params; + // ... +}; +``` + +--- + +## Import Patterns + +### Import Order +1. External dependencies (Node.js built-ins, npm packages) +2. SvelteKit imports (`$lib`, `$app`, `$env`) +3. Relative imports (`./ `, `../`) +4. Type imports (separate from value imports when beneficial) + +**Example:** +```typescript +// From QueueProcessor.ts + +// External dependencies +import { v4 as uuidv4 } from 'uuid'; + +// SvelteKit imports +import { queueManager } from './QueueManager'; +import { extractTextAndThumbnail } from '$lib/server/extraction'; +import { extractRecipe } from '$lib/server/parser'; +import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor'; +import { pushNotificationService } from '$lib/server/notifications/PushNotificationService'; +import { queueConfig } from './config'; + +// Type imports +import type { ProgressEvent } from '$lib/server/extraction'; +import type { QueueItem } from './types'; +``` + +### Import Styles + +#### Named Imports (Preferred) +```typescript +import { json } from '@sveltejs/kit'; +import { queueManager } from '$lib/server/queue/QueueManager'; +import { validateInstagramUrl } from '$lib/server/validation/instagram-url'; +``` + +#### Type-Only Imports +```typescript +import type { RequestHandler } from './$types'; +import type { QueueItem, QueueItemStatus } from './types'; +``` + +#### Default Imports +```typescript +import OpenAI from 'openai'; +import fs from 'fs'; +import path from 'path'; +``` + +### Export Patterns + +#### Named Exports (Preferred) +```typescript +// Export functions +export async function extractRecipe(text: string): Promise { ... } + +// Export classes +export class QueueManager { ... } + +// Export constants +export const queueConfig = { ... }; + +// Export types +export type Recipe = z.infer; +export interface QueueItem { ... } +``` + +#### Singleton Pattern Export +```typescript +// Define class +export class QueueManager { ... } + +// Export singleton instance +export const queueManager = new QueueManager(); +``` + +--- + +## Comments & Documentation + +### JSDoc Style +Used extensively for public APIs and exported functions. + +**Function Documentation:** +```typescript +/** + * Add URL to processing queue + * + * @param url - Instagram URL to process + * @returns Newly created queue item + * + * @example + * ```typescript + * const item = queueManager.enqueue('https://instagram.com/p/abc123'); + * console.log('Queued with ID:', item.id); + * ``` + */ +enqueue(url: string): QueueItem { + // Implementation +} +``` + +**Class Documentation:** +```typescript +/** + * Singleton queue manager for processing Instagram URLs + * + * Features: + * - FIFO queue with unique IDs + * - Status tracking and updates + * - Progress event accumulation + * - Retry support for failed items + * - Pub/sub for real-time updates + * + * @example + * ```typescript + * import { queueManager } from './QueueManager'; + * + * // Add item to queue + * const item = queueManager.enqueue('https://instagram.com/p/abc123'); + * ``` + */ +export class QueueManager { + // Implementation +} +``` + +**Module-Level Documentation:** +```typescript +/** + * Queue Manager - Core queue operations and event management + * + * Manages an in-memory queue of Instagram URL processing jobs. + * Provides CRUD operations and pub/sub mechanism for queue updates. + * + * Architecture: Domain Layer (Hexagonal Architecture) + * - Port: Defines queue operations interface + * - Implementation: In-memory Map-based storage + */ +``` + +### Inline Comments + +#### Single-line Comments +```typescript +// Set restrictive permissions +fs.chmodSync(authFile, 0o600); + +// FIFO order - get oldest pending item +const pendingItems = Array.from(this.items.values()) + .filter(item => item.status === 'pending'); +``` + +#### Block Comments (Avoided) +Single-line comments preferred. Block comments used only for large comment blocks or temporarily disabling code during development. + +### TODO Comments +```typescript +// TODO: Add retry logic with exponential backoff +// FIXME: Handle race condition when multiple workers dequeue +``` + +--- + +## TypeScript Patterns + +### Type Safety + +#### Strict Mode Enabled +```json +// tsconfig.json +{ + "compilerOptions": { + "strict": true, + "forceConsistentCasingInFileNames": true + } +} +``` + +#### Type Annotations +```typescript +// Explicit return types for public functions +export async function extractRecipe(text: string): Promise { ... } + +// Explicit parameter types +function processItem(item: QueueItem, config: QueueConfig): void { ... } + +// Type inference for local variables (acceptable) +const items = queueManager.getAll(); // Type inferred +``` + +### Union Types +```typescript +export type QueueItemStatus = + | 'pending' + | 'in_progress' + | 'completed' + | 'failed'; + +export type ProcessingPhase = + | 'extraction' + | 'parsing' + | 'uploading'; + +export type ProgressEventType = + | 'status' + | 'method' + | 'retry' + | 'error' + | 'thumbnail' + | 'complete'; +``` + +### Generics +```typescript +// Generic function +async function fetchFromTandoor( + url: string, + options: Partial = { method: 'GET' } +): Promise<{ ok: boolean; data?: T; error?: string }> { + // Implementation +} +``` + +--- + +## Svelte 5 Patterns + +### Runes (Reactivity) + +#### $state (Reactive Variables) +```svelte + +``` + +#### $props (Component Props) +```svelte + +``` + +#### $derived (Computed Values) +```svelte + +``` + +#### $effect (Side Effects) +```svelte + +``` + +### Component Structure +```svelte + + + +
+ +
+ + + +``` + +--- + +## Error Handling + +### Custom Error Classes +```typescript +// From api/errors.ts +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} + +export class NotFoundError extends Error { + constructor(resource: string) { + super(`${resource} not found`); + this.name = 'NotFoundError'; + } +} + +export class ConflictError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConflictError'; + } +} +``` + +### Try-Catch Pattern +```typescript +export const POST: RequestHandler = async ({ request }) => { + try { + const { url } = await request.json(); + + if (!url) { + throw new ValidationError('URL is required'); + } + + const item = queueManager.enqueue(url); + return json(item, { status: 201 }); + + } catch (error) { + return handleApiError(error); + } +}; +``` + +--- + +## Linting Configuration + +### ESLint +**Config:** `eslint.config.js` + +- Base: `@eslint/js` recommended +- TypeScript: `typescript-eslint` recommended +- Svelte: `eslint-plugin-svelte` recommended +- Formatting: `eslint-config-prettier` + +**Rules:** +```javascript +{ + rules: { + "no-undef": 'off' // TypeScript handles this + } +} +``` + +### Prettier +**Config:** `.prettierrc` + +```json +{ + "useTabs": false, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"] +} +``` + +--- + +## Testing Conventions + +### Test Structure +```typescript +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +describe('QueueManager', () => { + let manager: QueueManager; + + beforeEach(() => { + manager = new QueueManager(); + }); + + it('should enqueue items', () => { + const item = manager.enqueue('https://instagram.com/p/test'); + expect(item.status).toBe('pending'); + }); + + it('should dequeue items in FIFO order', () => { + manager.enqueue('url1'); + manager.enqueue('url2'); + + const first = manager.dequeue(); + expect(first?.url).toBe('url1'); + }); +}); +``` + +### Mock Pattern +```typescript +vi.mock('$lib/server/extraction', () => ({ + extractTextAndThumbnail: vi.fn().mockResolvedValue({ + text: 'Mock text', + thumbnailUrl: 'https://example.com/thumb.jpg' + }) +})); +``` + +--- + +## File Headers + +### Module Documentation Pattern +Every major module includes a header comment: + +```typescript +/** + * Module Name - Brief Description + * + * Detailed description of the module's purpose and functionality. + * + * Architecture: Layer Name (Hexagonal Architecture) + * - Port: Description of port interface + * - Implementation: Description of concrete implementation + */ +``` + +**Example:** +```typescript +/** + * Queue Manager - Core queue operations and event management + * + * Manages an in-memory queue of Instagram URL processing jobs. + * Provides CRUD operations and pub/sub mechanism for queue updates. + * + * Architecture: Domain Layer (Hexagonal Architecture) + * - Port: Defines queue operations interface + * - Implementation: In-memory Map-based storage + */ +``` + +--- + +## Additional Conventions + +### Environment Variables +```typescript +import { env } from '$env/dynamic/private'; + +const apiKey = env.OPENAI_API_KEY; +const tandoorUrl = env.TANDOOR_URL || null; +``` + +### Date Handling +ISO8601 strings throughout the application: +```typescript +const now = new Date().toISOString(); +// Output: "2026-02-15T12:30:45.123Z" +``` + +### Null vs Undefined +- `null`: Intentional absence of value +- `undefined`: Not yet initialized or optional parameters +- Prefer `null` for API responses and data structures + +### Async/Await +Always preferred over Promise chains: +```typescript +// Preferred +async function fetchData() { + const response = await fetch(url); + const data = await response.json(); + return data; +} + +// Avoid +function fetchData() { + return fetch(url) + .then(response => response.json()) + .then(data => data); +} +``` + +--- + +**Document Version:** 1.0 +**Enforced By:** ESLint, Prettier, TypeScript +**Generated by:** Initializer Agent diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md new file mode 100644 index 0000000..882153d --- /dev/null +++ b/docs/FINDINGS.md @@ -0,0 +1,361 @@ +# Findings & Research Documentation + +**Last Updated:** 2026-02-15T00:00:00.000Z +**JIRA:** RECIPE-0001 +**Status:** Initialized + +--- + +## Purpose + +This document tracks research findings, analysis results, and technical discoveries made during development. Each agent (Planner, Developer, Reviewer) appends findings as they work through the pipeline. + +--- + +## Initial Codebase Analysis + +### Language & Framework +- **Primary Language**: TypeScript 5.9.3 +- **Framework**: SvelteKit 2.48.5 with Svelte 5.43.8 +- **Runtime**: Node.js 22+ +- **Package Manager**: npm + +### Project Type +Progressive Web Application (PWA) for extracting recipes from Instagram posts and uploading them to Tandoor Recipe Manager. + +### Architecture Style +**Hexagonal Architecture** (Ports and Adapters): +- Domain logic in `src/lib/server/` +- External system adapters: Instagram, Tandoor, LLM, Browser +- Clear separation between client and server code + +### Key Technical Components +1. **Queue Management System**: In-memory FIFO queue with async processing +2. **Three-Phase Pipeline**: Extraction → Parsing → Uploading +3. **Real-Time Updates**: Server-Sent Events (SSE) for progress tracking +4. **Push Notifications**: Web Push API for background notifications +5. **PWA Features**: Service worker, manifest, install prompts + +### Design Patterns Identified +- **Singleton**: QueueManager, QueueProcessor, PushNotificationService +- **Factory**: createLLM(), createBrowserContext(), initializeBrowser() +- **Observer**: Queue subscription system, SSE streaming +- **Adapter**: Instagram, Tandoor, LLM, Browser adapters +- **Strategy**: Multiple extraction methods with fallback + +### Dependencies Overview +**Production** (6 dependencies): +- Browser automation: `playwright` +- LLM integration: `openai` +- Utilities: `uuid`, `date-fns`, `zod` + +**Development** (26+ dependencies): +- Framework: `@sveltejs/kit`, `svelte`, `vite` +- Testing: `vitest`, `@vitest/browser-playwright` +- Styling: `tailwindcss` +- Tooling: `typescript`, `eslint`, `prettier` + +### File Structure +``` +52 total TypeScript/JavaScript files +├── 39 TypeScript files (.ts) +├── 10+ Svelte components (.svelte) +├── 3 JavaScript config files (.js) +└── Multiple test files (.spec.ts) +``` + +### Code Quality Indicators +- **Strict TypeScript**: `strict: true` enabled +- **Comprehensive Testing**: 138 tests across unit, integration, and browser tests +- **Linting**: ESLint with TypeScript and Svelte plugins +- **Formatting**: Prettier with Svelte and Tailwind plugins +- **Type Safety**: Zod schemas for runtime validation + +### Environment Configuration +Required variables: +- `OPENAI_API_KEY` - LLM access +- `TANDOOR_URL` - Recipe manager URL (optional) +- `TANDOOR_TOKEN` - API authentication (optional) +- `QUEUE_CONCURRENCY` - Processing limit (default: 2) +- `QUEUE_MAX_RETRIES` - Retry attempts (default: 3) + +### Deployment Setup +- **Docker**: Dockerfile with Node.js 22 Alpine + Chromium +- **HTTPS**: Local SSL certificates for PWA features +- **Production**: Node.js adapter for SvelteKit + +### Notable Features +1. **Multi-Method Extraction**: 4-strategy cascade with intelligent fallback +2. **Progress Tracking**: Real-time callbacks throughout extraction pipeline +3. **Thumbnail Validation**: HTTP status code checking for image URLs +4. **Retry Logic**: Configurable retry attempts for failed extractions +5. **Scheduler**: Background task execution with authentication + +--- + +## Technical Debt & Opportunities + +### Identified Issues +1. **Deprecated Endpoints**: `/api/extract` returns 410 Gone (migration helper) +2. **In-Memory Queue**: No persistence - items lost on server restart +3. **Single Instance**: Queue state not shared across multiple server instances + +### Potential Improvements +1. **Queue Persistence**: Redis or database-backed queue for durability +2. **Horizontal Scaling**: Shared queue state for multi-instance deployments +3. **Rate Limiting**: Instagram request throttling to avoid blocks +4. **Caching**: Extracted content caching to reduce redundant processing + +--- + +## Research Findings + +*This section will be populated by the Planner agent during task analysis.* + +### [Planner] Research Notes - RECIPE-0001 (2026-02-15) + +**Task:** Fix model loading issue and frontend error display + +#### Issue 1: Model Loading - "400 No models loaded" +**Research Date:** 2026-02-15 +**Source:** Stack trace analysis, OpenAI SDK documentation, LM Studio/LiteLLM API patterns + +**Problem Analysis:** +- Error occurs at `detectRecipe()` in [src/lib/server/parser.ts](src/lib/server/parser.ts#L30) +- OpenAI-compatible APIs (LM Studio, LiteLLM, Ollama, etc.) often require models to be explicitly loaded +- Current implementation assumes model is already loaded +- Error message contains provider-specific instructions ("use the 'lms load' command") + +**OpenAI-Compatible Model Loading Patterns:** +1. **LM Studio**: Uses `/v1/models` endpoint to list available models + - Loaded models appear in response with `"id": "model-name"` + - No programmatic loading endpoint (manual load in UI) + +2. **LiteLLM**: Uses `/v1/models` to list loaded models + - Models must be configured in server startup + - No dynamic loading endpoint + +3. **Ollama**: Uses `/api/tags` for model list and `/api/pull` for loading + - Different API structure (not `/v1` prefix) + +4. **Generic OpenAI-compatible**: Most follow OpenAI's `/v1/models` endpoint + - No standard for dynamic model loading + - Usually require pre-configuration + +**Solution Approach:** +- Check if model exists via `client.models.list()` +- If model not found/loaded, provide clear user-facing error +- Remove provider-specific error messages +- Add notification when model check succeeds +- Consider future enhancement: detect provider type and attempt auto-load if supported + +**Files Affected:** +- [src/lib/server/llm.ts](src/lib/server/llm.ts) - Add model availability check +- [src/lib/server/parser.ts](src/lib/server/parser.ts) - Handle model not loaded error +- [src/lib/server/queue/QueueProcessor.ts](src/lib/server/queue/QueueProcessor.ts) - User notification + +--- + +#### Issue 2: Frontend Error Display - "[object Object]" +**Research Date:** 2026-02-15 +**Source:** Code analysis of QueueItemCard.svelte, types.ts, QueueManager.ts + +**Problem Analysis:** +- Error structure is an object: `{ phase, message, recoverable, timestamp }` +- Frontend displays `{item.error}` directly (line 205 of QueueItemCard.svelte) +- Svelte renders object.toString() → "[object Object]" + +**Current Implementation:** +```typescript +// types.ts - Error is an object +error?: { + phase: ProcessingPhase; + message: string; + recoverable: boolean; + timestamp: string; +} + +// QueueItemCard.svelte line 205 - Displays object directly +
{item.error}
+``` + +**Solution:** +Change to: `{item.error?.message || item.error}` +- Handles object error (gets .message) +- Handles legacy string errors (fallback) +- Type-safe with optional chaining + +**Files Affected:** +- [src/routes/components/QueueItemCard.svelte](src/routes/components/QueueItemCard.svelte#L205) - Display error.message + +--- + +#### Dependencies & Constraints (from ARCHITECTURE.md) +- Using `openai@^4.20.0` SDK +- Environment: `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `LLM_MODEL` +- Current config example: `http://192.168.1.10:1234/v1` (LM Studio) +- Must maintain OpenAI-compatible API contract +- No assumption about specific provider implementation + +#### Code Style Requirements (from CODE_STYLE.md) +- Use SvelteKit `$env/dynamic/private` for env vars (already correct) +- Error handling: try-catch with descriptive messages +- Console logging: `[Component] Message` format +- Type safety: TypeScript strict mode enabled + + + +--- + +### [Developer] Implementation Notes + + + +--- + +### [Reviewer] Review Notes + + + +--- + +## API Endpoint Catalog + +### Active Endpoints + +#### Queue Management +- `POST /api/queue` - Enqueue Instagram URL for processing +- `GET /api/queue` - List queue items (supports filtering, pagination) +- `GET /api/queue/stream` - SSE stream for real-time updates +- `GET /api/queue/{id}` - Get specific queue item details +- `DELETE /api/queue/{id}` - Remove item from queue +- `POST /api/queue/{id}/retry` - Retry failed extraction + +#### Push Notifications +- `POST /api/notifications/subscribe` - Subscribe to push notifications +- `DELETE /api/notifications/subscribe` - Unsubscribe from notifications +- `GET /api/notifications/vapid-key` - Get VAPID public key + +#### Health & Status +- `GET /api/health` - Application health check +- `GET /api/llm-health` - LLM service availability check + +#### Tandoor Integration +- `POST /api/tandoor` - Upload recipe to Tandoor +- `GET /api/tandoor-config` - Get Tandoor configuration status + +#### Legacy/Deprecated +- `POST /api/extract` - ⚠️ Deprecated (returns 410 Gone) + +--- + +## Known Constraints + +### Browser Automation +- Requires Chromium/Chrome installation +- Headless mode used in production +- Cookie handling for authenticated Instagram content + +### LLM Integration +- Requires OpenAI-compatible API endpoint +- Configurable model selection +- Structured output using Zod schemas + +### Tandoor Integration +- Optional feature (disabled without credentials) +- Requires Tandoor API token +- Supports ingredient partitioning across steps + +### SSL Requirements +- HTTPS required for Service Worker registration +- Local development uses self-signed certificates +- Certificates managed via external Caddy CA + +--- + +## Testing Coverage + +### Test Distribution +- **Unit Tests**: Core logic validation +- **Integration Tests**: Multi-component workflows +- **API Tests**: Endpoint behavior verification +- **Browser Tests**: Svelte component rendering + +### Test Files +- `queue-manager.spec.ts` +- `queue-processor.spec.ts` +- `queue-api.spec.ts` +- `queue-sse.spec.ts` +- `scheduler.spec.ts` +- `instagram-url-validation.spec.ts` +- `thumbnail-validation.spec.ts` +- `extraction-url-validation.integration.spec.ts` +- `page.svelte.spec.ts` + +### Mock Strategy +- Environment variables mocked via `vi.mock('$env/dynamic/private')` +- External services mocked at module level +- Browser automation mocked for unit tests + +--- + +## Documentation Inventory + +### Existing Documentation +- `README.md` - Project overview and setup +- `docs/API.md` - API endpoint specifications +- `docs/MIGRATION.md` - Migration guides +- `docs/SVELTEKIT_SSR_GUIDE.md` - SSR implementation notes +- `docs/TESTING.md` - Testing guide and mocking patterns +- `docs/Tandoor (2.3.6).yaml` - OpenAPI spec for Tandoor + +### Plan Documentation +`docs/plans/` contains 20+ implementation plans: +- Execution plans for completed features +- Technical specifications +- Story breakdowns with acceptance criteria + +### Outcome Documentation +`docs/outcomes/` contains 20+ outcome reports: +- Implementation summaries +- Changes made +- Testing results +- Lessons learned + +--- + +## Agent Pipeline Notes + +### Build Commands +- **Build**: `npm run build` +- **Test**: `npm test` (alias for `npm run test:unit -- --run`) +- **Dev**: `npm run dev` +- **Lint**: `npm run lint` +- **Format**: `npm run format` + +### Development Workflow +1. Make changes in `src/` +2. Run tests: `npm test` +3. Verify build: `npm run build` +4. Test locally: `npm run dev` + +### Continuous Integration +- ESLint checks code quality +- Prettier enforces formatting +- TypeScript checks type safety +- Vitest runs test suite + +--- + +## Next Steps + +This document will be updated by subsequent agents: +1. **Planner**: Append research findings and analysis +2. **Developer**: Document implementation discoveries +3. **Reviewer**: Record review observations and recommendations + +--- + +**Document Version:** 1.0 +**Generated by:** Initializer Agent +**Next Update:** Planner Agent diff --git a/secrets/auth.json b/secrets/auth.json index 45b8f35..e70cae4 100644 --- a/secrets/auth.json +++ b/secrets/auth.json @@ -5,7 +5,7 @@ "value": "SDRORLyWEsWWty2ZoVGdER", "domain": ".instagram.com", "path": "/", - "expires": 1805679336.753547, + "expires": 1805681301.059089, "httpOnly": false, "secure": true, "sameSite": "Lax" @@ -45,7 +45,7 @@ "value": "59661903731", "domain": ".instagram.com", "path": "/", - "expires": 1778895336.753668, + "expires": 1778897301.059226, "httpOnly": false, "secure": true, "sameSite": "None" @@ -60,25 +60,25 @@ "secure": true, "sameSite": "Lax" }, - { - "name": "rur", - "value": "\"CLN\\05459661903731\\0541802655336:01feb1b7e2710ac48e6833d9c5ea2ec2780a10752766110266c6865bb13b99965b41753b\"", - "domain": ".instagram.com", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": true, - "sameSite": "Lax" - }, { "name": "wd", "value": "1280x720", "domain": ".instagram.com", "path": "/", - "expires": 1771724138, + "expires": 1771726102, "httpOnly": false, "secure": true, "sameSite": "Lax" + }, + { + "name": "rur", + "value": "\"CLN\\05459661903731\\0541802657301:01fec16d6deae7b6c05aaffac223726730fb177eaca51f86202541f6dfb9b897008bcb7f\"", + "domain": ".instagram.com", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" } ], "origins": [ @@ -87,39 +87,47 @@ "localStorage": [ { "name": "chatd-deviceid", - "value": "44fa3b1b-c2d0-4356-9a64-133d971a1c45" - }, - { - "name": "hb_timestamp", - "value": "1771119338970" + "value": "5888cc2d-dc1b-4d02-a951-e87d1a92bf9a" }, { "name": "IGSession", - "value": "53laur:1771121139044" + "value": "53laur:1771123102860" + }, + { + "name": "mutex_polaris_banzai", + "value": "nxd84o:1771121303859" }, { "name": "pixel_fire_ts", - "value": "1766282683056" - }, - { - "name": "signal_flush_timestamp", - "value": "1771119338992" + "value": "1771121302843" }, { "name": "Session", - "value": "k3dql4:1771119374044" + "value": "bqsbrq:1771121337860" }, { "name": "has_interop_upgraded", "value": "{\"lastCheckedAt\":1766366944051,\"status\":false}" }, { - "name": "ig_boost_on_web_campaign_upsell_shown", - "value": "false" + "name": "mutex_banzai", + "value": "nxd84o:1771121303859" }, { "name": "banzai:last_storage_flush", "value": "1771119337751.8" + }, + { + "name": "hb_timestamp", + "value": "1771119338970" + }, + { + "name": "signal_flush_timestamp", + "value": "1771119338992" + }, + { + "name": "ig_boost_on_web_campaign_upsell_shown", + "value": "false" } ] } diff --git a/src/lib/server/llm.spec.ts b/src/lib/server/llm.spec.ts new file mode 100644 index 0000000..a9492ae --- /dev/null +++ b/src/lib/server/llm.spec.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { checkModelAvailability } from './llm'; + +const { mockEnv } = vi.hoisted(() => { + return { + mockEnv: { + OPENAI_BASE_URL: 'http://localhost:1234/v1', + OPENAI_API_KEY: 'test-key', + LLM_MODEL: 'test-model' + } + }; +}); + +vi.mock('$env/dynamic/private', () => ({ + env: mockEnv +})); + +const mockModelsList = vi.fn(); + +vi.mock('openai', () => ({ + default: vi.fn(function OpenAI() { + return { + models: { + list: mockModelsList + } + }; + }) +})); + +describe('checkModelAvailability', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return available: true when model is found', async () => { + mockModelsList.mockResolvedValue({ + data: [{ id: 'test-model' }, { id: 'gpt-4o' }, { id: 'llama2' }] + }); + + const result = await checkModelAvailability('test-model'); + + expect(result).toEqual({ available: true }); + expect(mockModelsList).toHaveBeenCalledOnce(); + }); + + it('should return available: false with message when model not found', async () => { + mockModelsList.mockResolvedValue({ + data: [{ id: 'gpt-4o' }, { id: 'llama2' }] + }); + + const result = await checkModelAvailability('missing-model'); + + expect(result.available).toBe(false); + expect(result.message).toContain('Model "missing-model" not found'); + expect(result.message).toContain('Available models: gpt-4o, llama2'); + }); + + it('should handle API errors gracefully', async () => { + mockModelsList.mockRejectedValue(new Error('API connection failed')); + + const result = await checkModelAvailability('test-model'); + + expect(result.available).toBe(false); + expect(result.message).toContain('Failed to check model availability'); + expect(result.message).toContain('API connection failed'); + }); + + it('should match models by exact ID (case-sensitive)', async () => { + mockModelsList.mockResolvedValue({ + data: [{ id: 'test-model' }, { id: 'Test-Model' }] + }); + + const result1 = await checkModelAvailability('test-model'); + expect(result1.available).toBe(true); + + const result2 = await checkModelAvailability('TEST-MODEL'); + expect(result2.available).toBe(false); + }); + + it('should handle empty model list', async () => { + mockModelsList.mockResolvedValue({ + data: [] + }); + + const result = await checkModelAvailability('any-model'); + + expect(result.available).toBe(false); + expect(result.message).toContain('Available models: '); + }); +}); diff --git a/src/lib/server/llm.ts b/src/lib/server/llm.ts index f2a896a..e4744f6 100644 --- a/src/lib/server/llm.ts +++ b/src/lib/server/llm.ts @@ -40,4 +40,41 @@ export async function checkLLMHealth(): Promise { console.error('[LLM] Health check failed:', e); return false; } +} + +/** + * Check if a specific model is available in the OpenAI-compatible API + * @param model - The model ID to check for availability + * @returns Object with available status and optional message + */ +export async function checkModelAvailability( + model: string +): Promise<{ available: boolean; message?: string }> { + try { + console.log('[LLM] Checking model availability:', model); + const { client } = createLLM(); + const response = await client.models.list(); + const models = response.data || []; + + const foundModel = models.find((m) => m.id === model); + + if (foundModel) { + console.log('[LLM] Model available:', model); + return { available: true }; + } else { + const availableModels = models.map((m) => m.id).join(', '); + console.warn('[LLM] Model not found:', model); + console.warn('[LLM] Available models:', availableModels); + return { + available: false, + message: `Model "${model}" not found. Available models: ${availableModels}` + }; + } + } catch (e) { + console.error('[LLM] Model availability check failed:', e); + return { + available: false, + message: `Failed to check model availability: ${(e as Error).message}` + }; + } } \ No newline at end of file diff --git a/src/lib/server/parser.ts b/src/lib/server/parser.ts index 638a554..528d6d0 100644 --- a/src/lib/server/parser.ts +++ b/src/lib/server/parser.ts @@ -1,4 +1,4 @@ -import { createLLM } from './llm'; +import { createLLM, checkModelAvailability } from './llm'; import { zodResponseFormat } from 'openai/helpers/zod'; import { z } from 'zod'; import { RECIPE_DETECTION_PROMPT, RECIPE_EXTRACTION_PROMPT } from './prompts/recipe-extraction'; @@ -56,6 +56,21 @@ export async function detectRecipe(text: string): Promise { } catch (e) { console.error('[LLM] Recipe detection error:', e); console.error('[LLM] Stack trace:', (e as Error).stack); + + // Check if this is a model-related error + const errorMessage = (e as Error).message || ''; + const isModelError = errorMessage.includes('400') && + (errorMessage.toLowerCase().includes('model') || + errorMessage.toLowerCase().includes('load')); + + if (isModelError) { + const { model } = createLLM(); + const modelCheck = await checkModelAvailability(model); + if (!modelCheck.available) { + throw new Error(modelCheck.message || `Model "${model}" is not available`); + } + } + throw new Error(`Failed to detect recipe: ${(e as Error).message}`); } } @@ -100,6 +115,20 @@ export async function parseRecipe(text: string): Promise { console.error('[LLM] Recipe parsing error:', e); console.error('[LLM] Stack trace:', (e as Error).stack); + // Check if this is a model-related error + const errorMessage = (e as Error).message || ''; + const isModelError = errorMessage.includes('400') && + (errorMessage.toLowerCase().includes('model') || + errorMessage.toLowerCase().includes('load')); + + if (isModelError) { + const { model } = createLLM(); + const modelCheck = await checkModelAvailability(model); + if (!modelCheck.available) { + throw new Error(modelCheck.message || `Model "${model}" is not available`); + } + } + // If structured output fails, try standard completion if ((e as any).message?.includes('response_format') || (e as any).message?.includes('structured output')) { diff --git a/src/routes/components/QueueItemCard.svelte b/src/routes/components/QueueItemCard.svelte index cbc0053..2847704 100644 --- a/src/routes/components/QueueItemCard.svelte +++ b/src/routes/components/QueueItemCard.svelte @@ -202,7 +202,18 @@
Processing Error
-
{item.error}
+
+ {#if typeof item.error === 'object' && item.error?.message} + {item.error.message} + {:else} + {item.error} + {/if} +
+ {#if typeof item.error === 'object' && item.error?.phase} +
+ Failed during: {item.error.phase} +
+ {/if}