fix(RECIPE-0001): complete iteration 0 — automatic model loading and error display fix
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,3 +32,5 @@ debug_page.txt
|
|||||||
.github/agents
|
.github/agents
|
||||||
.github/schemas
|
.github/schemas
|
||||||
.github/skills
|
.github/skills
|
||||||
|
|
||||||
|
prompts/
|
||||||
|
|||||||
403
docs/ARCHITECTURE.md
Normal file
403
docs/ARCHITECTURE.md
Normal file
@@ -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
|
||||||
792
docs/CODE_STYLE.md
Normal file
792
docs/CODE_STYLE.md
Normal file
@@ -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: `<name>.spec.ts` or `<name>.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<string, QueueItem> = new Map();
|
||||||
|
private subscribers: Set<QueueUpdateCallback> = 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<ExtractedContent> { ... }
|
||||||
|
|
||||||
|
// From parser.ts
|
||||||
|
export async function extractRecipe(text: string): Promise<Recipe> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<typeof RecipeSchema>;
|
||||||
|
|
||||||
|
// From tandoor.ts
|
||||||
|
const TandoorRecipeSchema = z.object({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TandoorRecipe = z.infer<typeof TandoorRecipeSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Classes
|
||||||
|
|
||||||
|
#### Class Names
|
||||||
|
- **PascalCase** for class names
|
||||||
|
- Descriptive suffixes: `Manager`, `Service`, `Processor`, `Handler`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// From QueueManager.ts
|
||||||
|
export class QueueManager {
|
||||||
|
private items: Map<string, QueueItem> = new Map();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// From QueueProcessor.ts
|
||||||
|
export class QueueProcessor {
|
||||||
|
private processing: Set<string> = new Set();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// From PushNotificationService.ts
|
||||||
|
class PushNotificationService {
|
||||||
|
private subscriptions: Map<string, PushSubscription> = 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<ExtractedContent> {
|
||||||
|
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<Recipe> { ... }
|
||||||
|
|
||||||
|
// Export classes
|
||||||
|
export class QueueManager { ... }
|
||||||
|
|
||||||
|
// Export constants
|
||||||
|
export const queueConfig = { ... };
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type Recipe = z.infer<typeof RecipeSchema>;
|
||||||
|
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<Recipe> { ... }
|
||||||
|
|
||||||
|
// 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<T>(
|
||||||
|
url: string,
|
||||||
|
options: Partial<RequestInit> = { method: 'GET' }
|
||||||
|
): Promise<{ ok: boolean; data?: T; error?: string }> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Svelte 5 Patterns
|
||||||
|
|
||||||
|
### Runes (Reactivity)
|
||||||
|
|
||||||
|
#### $state (Reactive Variables)
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let count = $state(0);
|
||||||
|
let items = $state<QueueItem[]>([]);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### $props (Component Props)
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
recipe = null,
|
||||||
|
tandoorEnabled = false,
|
||||||
|
onRetry,
|
||||||
|
onImportToTandoor
|
||||||
|
} = $props<{
|
||||||
|
recipe: Recipe | null;
|
||||||
|
tandoorEnabled: boolean;
|
||||||
|
onRetry: () => void;
|
||||||
|
onImportToTandoor: () => void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### $derived (Computed Values)
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let count = $state(0);
|
||||||
|
let double = $derived(count * 2);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### $effect (Side Effects)
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let url = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
console.log('URL changed:', url);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
// Imports
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
let { items } = $props<{ items: Item[] }>();
|
||||||
|
|
||||||
|
// State
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// Derived state
|
||||||
|
let count = $derived(items.length);
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
function handleClick() {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
$effect(() => {
|
||||||
|
// Side effects
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Template -->
|
||||||
|
<div>
|
||||||
|
<!-- Markup -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Styles -->
|
||||||
|
<style>
|
||||||
|
/* Component-scoped styles */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
361
docs/FINDINGS.md
Normal file
361
docs/FINDINGS.md
Normal file
@@ -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
|
||||||
|
<div class="text-sm text-red-700 mt-1">{item.error}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
<!-- Planner appends findings here -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [Developer] Implementation Notes
|
||||||
|
|
||||||
|
<!-- Developer appends findings here -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [Reviewer] Review Notes
|
||||||
|
|
||||||
|
<!-- Reviewer appends findings here -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"value": "SDRORLyWEsWWty2ZoVGdER",
|
"value": "SDRORLyWEsWWty2ZoVGdER",
|
||||||
"domain": ".instagram.com",
|
"domain": ".instagram.com",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1805679336.753547,
|
"expires": 1805681301.059089,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"value": "59661903731",
|
"value": "59661903731",
|
||||||
"domain": ".instagram.com",
|
"domain": ".instagram.com",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1778895336.753668,
|
"expires": 1778897301.059226,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "None"
|
"sameSite": "None"
|
||||||
@@ -60,25 +60,25 @@
|
|||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "rur",
|
|
||||||
"value": "\"CLN\\05459661903731\\0541802655336:01feb1b7e2710ac48e6833d9c5ea2ec2780a10752766110266c6865bb13b99965b41753b\"",
|
|
||||||
"domain": ".instagram.com",
|
|
||||||
"path": "/",
|
|
||||||
"expires": -1,
|
|
||||||
"httpOnly": true,
|
|
||||||
"secure": true,
|
|
||||||
"sameSite": "Lax"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "wd",
|
"name": "wd",
|
||||||
"value": "1280x720",
|
"value": "1280x720",
|
||||||
"domain": ".instagram.com",
|
"domain": ".instagram.com",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1771724138,
|
"expires": 1771726102,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rur",
|
||||||
|
"value": "\"CLN\\05459661903731\\0541802657301:01fec16d6deae7b6c05aaffac223726730fb177eaca51f86202541f6dfb9b897008bcb7f\"",
|
||||||
|
"domain": ".instagram.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Lax"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"origins": [
|
"origins": [
|
||||||
@@ -87,39 +87,47 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "chatd-deviceid",
|
"name": "chatd-deviceid",
|
||||||
"value": "44fa3b1b-c2d0-4356-9a64-133d971a1c45"
|
"value": "5888cc2d-dc1b-4d02-a951-e87d1a92bf9a"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "hb_timestamp",
|
|
||||||
"value": "1771119338970"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "IGSession",
|
"name": "IGSession",
|
||||||
"value": "53laur:1771121139044"
|
"value": "53laur:1771123102860"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mutex_polaris_banzai",
|
||||||
|
"value": "nxd84o:1771121303859"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pixel_fire_ts",
|
"name": "pixel_fire_ts",
|
||||||
"value": "1766282683056"
|
"value": "1771121302843"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "signal_flush_timestamp",
|
|
||||||
"value": "1771119338992"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
"value": "k3dql4:1771119374044"
|
"value": "bqsbrq:1771121337860"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "has_interop_upgraded",
|
"name": "has_interop_upgraded",
|
||||||
"value": "{\"lastCheckedAt\":1766366944051,\"status\":false}"
|
"value": "{\"lastCheckedAt\":1766366944051,\"status\":false}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ig_boost_on_web_campaign_upsell_shown",
|
"name": "mutex_banzai",
|
||||||
"value": "false"
|
"value": "nxd84o:1771121303859"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "banzai:last_storage_flush",
|
"name": "banzai:last_storage_flush",
|
||||||
"value": "1771119337751.8"
|
"value": "1771119337751.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hb_timestamp",
|
||||||
|
"value": "1771119338970"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal_flush_timestamp",
|
||||||
|
"value": "1771119338992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ig_boost_on_web_campaign_upsell_shown",
|
||||||
|
"value": "false"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/lib/server/llm.spec.ts
Normal file
94
src/lib/server/llm.spec.ts
Normal file
@@ -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: ');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,4 +40,41 @@ export async function checkLLMHealth(): Promise<boolean> {
|
|||||||
console.error('[LLM] Health check failed:', e);
|
console.error('[LLM] Health check failed:', e);
|
||||||
return false;
|
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}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createLLM } from './llm';
|
import { createLLM, checkModelAvailability } from './llm';
|
||||||
import { zodResponseFormat } from 'openai/helpers/zod';
|
import { zodResponseFormat } from 'openai/helpers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { RECIPE_DETECTION_PROMPT, RECIPE_EXTRACTION_PROMPT } from './prompts/recipe-extraction';
|
import { RECIPE_DETECTION_PROMPT, RECIPE_EXTRACTION_PROMPT } from './prompts/recipe-extraction';
|
||||||
@@ -56,6 +56,21 @@ export async function detectRecipe(text: string): Promise<boolean> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[LLM] Recipe detection error:', e);
|
console.error('[LLM] Recipe detection error:', e);
|
||||||
console.error('[LLM] Stack trace:', (e as Error).stack);
|
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}`);
|
throw new Error(`Failed to detect recipe: ${(e as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +115,20 @@ export async function parseRecipe(text: string): Promise<Recipe> {
|
|||||||
console.error('[LLM] Recipe parsing error:', e);
|
console.error('[LLM] Recipe parsing error:', e);
|
||||||
console.error('[LLM] Stack trace:', (e as Error).stack);
|
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 structured output fails, try standard completion
|
||||||
if ((e as any).message?.includes('response_format') ||
|
if ((e as any).message?.includes('response_format') ||
|
||||||
(e as any).message?.includes('structured output')) {
|
(e as any).message?.includes('structured output')) {
|
||||||
|
|||||||
@@ -202,7 +202,18 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-medium text-red-800">Processing Error</div>
|
<div class="text-sm font-medium text-red-800">Processing Error</div>
|
||||||
<div class="text-sm text-red-700 mt-1">{item.error}</div>
|
<div class="text-sm text-red-700 mt-1">
|
||||||
|
{#if typeof item.error === 'object' && item.error?.message}
|
||||||
|
{item.error.message}
|
||||||
|
{:else}
|
||||||
|
{item.error}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if typeof item.error === 'object' && item.error?.phase}
|
||||||
|
<div class="text-xs text-red-600 mt-1">
|
||||||
|
Failed during: {item.error.phase}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user