Files
insta-recipe/docs/plans/RefactorSharePageAndEnhanceThumbnails.md
Giancarmine Salucci 7e4d82de8d feat(share): refactor page and enhance thumbnail extraction
- Extract 8 reusable components from monolithic share page
- Add LLM health indicator with 30s polling
- Implement stealth thumbnail extraction with 4-method cascade
- Integrate real-time thumbnail preview component
- Reduce share page from 306 to ~140 lines
- Add comprehensive outcome documentation

Components:
- UrlInputSection: URL input and extraction trigger
- ProgressIndicator: Loading state display
- ExtractedTextViewer: Collapsible text preview
- RecipeCard: Recipe display with Tandoor integration
- ErrorState: Error handling UI
- LogViewer: System logs with color coding
- LlmHealthIndicator: LLM status with polling
- ThumbnailPreview: Real-time thumbnail display

Thumbnail Methods:
1. Meta tag extraction (og:image, twitter:image)
2. Video poster attribute
3. Instagram embedded JSON data
4. Screenshot fallback

Stories Completed:
- Story 1: Component extraction and refactoring
- Story 2: LLM health status indicator
- Story 3: Enhanced stealth thumbnail extraction
- Story 4: Thumbnail preview integration

Closes: RefactorSharePageAndEnhanceThumbnails
2025-12-21 04:18:38 +01:00

915 lines
27 KiB
Markdown

# Execution Plan: Refactor Share Page and Enhance Thumbnails
**Outcome Name:** RefactorSharePageAndEnhanceThumbnails
**Created:** 2025-12-21
**Status:** Ready for Implementation
---
## Overview
This plan addresses three key improvements to the InstaChef PWA:
1. **Component Modularization**: Split the monolithic 306-line share page into focused, reusable components
2. **LLM Health Monitoring**: Add visual health status indicator for the LLM service
3. **Stealthy Thumbnail Extraction**: Enhance thumbnail extraction with Instagram-friendly stealth techniques
---
## Problem Statement
### Current Issues
1. **Share Page Complexity**: The `+page.svelte` file contains 306 lines with mixed concerns (state management, UI rendering, business logic), making it difficult to maintain and test
2. **No LLM Visibility**: Users have no way to know if the LLM service is healthy before attempting extraction
3. **Basic Thumbnail Extraction**: Current screenshot-based approach is detectable and may trigger Instagram's anti-bot measures
### User Impact
- Difficult to maintain and extend the share page functionality
- Poor user experience when LLM service is down (only discover during extraction)
- Risk of Instagram blocking due to detectable automation patterns
---
## Technical Context
### Current Architecture
**Frontend:**
- Svelte 5.43.8 with modern runes (`$state`, `$derived`, `$effect`)
- TailwindCSS 4.1.17 for styling
- Share page uses snippets for UI modularity
**Backend:**
- Playwright 1.56.1 for browser automation
- Existing `/api/llm-health` endpoint for service monitoring
- `extractThumbnail()` function uses screenshot-based approach
### Hexagonal Architecture Alignment
- **Domain**: extraction.ts contains business logic for thumbnail extraction
- **Adapters**:
- Primary (Driving): Svelte components, API routes
- Secondary (Driven): Playwright Page interface
- **Ports**: Clear interfaces between components and domain logic
---
## Stories
### Story 1: Refactor Share Page into Modular Components
**Priority:** High
**Complexity:** Medium
**Estimated Effort:** 4 hours
#### Description
Extract the current snippets from `+page.svelte` into standalone, reusable Svelte components. This improves maintainability, testability, and follows single responsibility principle.
#### Acceptance Criteria
- [ ] Create `src/routes/share/components/` directory
- [ ] Extract 6 components from current snippets:
1. `UrlInputSection.svelte` - URL input and extraction trigger
2. `ProgressIndicator.svelte` - Loading state display
3. `ExtractedTextViewer.svelte` - Collapsible text preview
4. `RecipeCard.svelte` - Recipe display with Tandoor integration
5. `ErrorState.svelte` - Error handling UI
6. `LogViewer.svelte` - System logs display
- [ ] Parent `+page.svelte` orchestrates state and passes props to components
- [ ] Reduced `+page.svelte` from 306 to ~100 lines
- [ ] All components use Svelte 5 runes (`$state`, `$props`)
- [ ] Maintain existing functionality with no regressions
- [ ] TailwindCSS styling preserved
#### Technical Specifications
**Component Interfaces:**
```typescript
// UrlInputSection.svelte
interface Props {
targetUrl: string | null;
sharedText: string;
sharedUrl: string;
status: string;
onProcess: () => void;
}
// ProgressIndicator.svelte
interface Props {
status: string;
}
// ExtractedTextViewer.svelte
interface Props {
bodyText: string;
}
// RecipeCard.svelte
interface Props {
recipe: Recipe | null;
tandoorEnabled: boolean;
tandoorImporting: boolean;
tandoorError: string | null;
onRetry: () => void;
onImportToTandoor: () => void;
}
// ErrorState.svelte
interface Props {
status: string;
bodyText: string;
onRetry: () => void;
}
// LogViewer.svelte
interface Props {
logs: string[];
currentMethod: string;
status: string;
}
```
#### Implementation Steps
1. Create `src/routes/share/components/` directory
2. For each component:
- Create new `.svelte` file
- Extract relevant snippet code
- Define props interface using `let { prop1, prop2 } = $props()`
- Convert callbacks to prop functions
- Preserve TailwindCSS classes
3. Update `+page.svelte`:
- Import all components
- Remove snippet definitions
- Replace `{@render snippet()}` with `<Component />`
- Pass state and callbacks as props
#### Testing Strategy
- Visual regression testing (manual verification)
- Test each component in isolation
- Verify state flow from parent to children
- Verify callbacks work correctly
- Test with real Instagram URL extraction
#### Files Modified
- `src/routes/share/+page.svelte`
#### Files Created
- `src/routes/share/components/UrlInputSection.svelte`
- `src/routes/share/components/ProgressIndicator.svelte`
- `src/routes/share/components/ExtractedTextViewer.svelte`
- `src/routes/share/components/RecipeCard.svelte`
- `src/routes/share/components/ErrorState.svelte`
- `src/routes/share/components/LogViewer.svelte`
- `src/routes/share/components/ThumbnailPreview.svelte` (Story 4)
- `src/routes/share/components/LlmHealthIndicator.svelte` (Story 2)
---
### Story 2: Add LLM Health Status Component
**Priority:** Medium
**Complexity:** Low
**Estimated Effort:** 2 hours
#### Description
Create a component that monitors the LLM service health using the existing `/api/llm-health` endpoint and displays a visual indicator to users.
#### Acceptance Criteria
- [ ] Create `LlmHealthIndicator.svelte` component
- [ ] Component polls `/api/llm-health` every 30 seconds
- [ ] Visual indicator shows service status:
- 🟢 Green: healthy
- 🟡 Yellow: checking/loading
- 🔴 Red: unhealthy/error
- [ ] Tooltip/hover shows detailed status message
- [ ] Polling starts on mount and cleans up on unmount
- [ ] Component is non-blocking (doesn't prevent extraction)
- [ ] Integrated into share page header area
#### Technical Specifications
**API Contract:**
```typescript
// GET /api/llm-health response
{
status: 'healthy' | 'unhealthy' | 'error';
message: string;
}
```
**Component Interface:**
```typescript
// LlmHealthIndicator.svelte
interface Props {
pollInterval?: number; // default: 30000ms
}
interface HealthState {
status: 'checking' | 'healthy' | 'unhealthy' | 'error';
message: string;
lastChecked: Date | null;
}
```
**Implementation Pattern:**
```svelte
<script lang="ts">
let { pollInterval = 30000 } = $props();
let health = $state<HealthState>({
status: 'checking',
message: '',
lastChecked: null
});
async function checkHealth() {
try {
const res = await fetch('/api/llm-health');
const data = await res.json();
health = {
status: data.status === 'healthy' ? 'healthy' : 'unhealthy',
message: data.message,
lastChecked: new Date()
};
} catch (e) {
health = {
status: 'error',
message: e instanceof Error ? e.message : 'Network error',
lastChecked: new Date()
};
}
}
$effect(() => {
checkHealth(); // Initial check
const interval = setInterval(checkHealth, pollInterval);
return () => clearInterval(interval);
});
</script>
<div class="flex items-center gap-2 text-sm">
<div class="flex items-center gap-1">
{#if health.status === 'checking'}
🟡 <span>Checking LLM...</span>
{:else if health.status === 'healthy'}
🟢 <span class="text-green-600">LLM Ready</span>
{:else if health.status === 'unhealthy'}
🔴 <span class="text-red-600">LLM Unavailable</span>
{:else}
🔴 <span class="text-red-600">LLM Error</span>
{/if}
</div>
<div class="text-xs text-gray-500" title={health.message}>
{health.lastChecked ? `Last: ${health.lastChecked.toLocaleTimeString()}` : ''}
</div>
</div>
```
#### Implementation Steps
1. Create `src/routes/share/components/LlmHealthIndicator.svelte`
2. Implement health checking logic with polling
3. Add visual status indicator with appropriate colors
4. Implement cleanup in `$effect` return
5. Add component to share page header
6. Test polling behavior and visual states
#### Testing Strategy
- Test all health states (checking, healthy, unhealthy, error)
- Verify polling interval works correctly
- Verify cleanup on component unmount
- Test network error handling
- Manual testing with LM Studio running/stopped
#### Files Created
- `src/routes/share/components/LlmHealthIndicator.svelte`
#### Files Modified
- `src/routes/share/+page.svelte` (add health indicator to header)
---
### Story 3: Enhance Thumbnail Extraction with Stealth Techniques
**Priority:** High
**Complexity:** High
**Estimated Effort:** 6 hours
#### Description
Replace the basic screenshot-based thumbnail extraction with a multi-layered stealth approach that tries less detectable methods first, falling back to screenshots only when necessary.
#### Acceptance Criteria
- [ ] Implement `extractThumbnailStealth()` function in `extraction.ts`
- [ ] Try 4 extraction methods in order:
1. Meta tags (og:image, twitter:image)
2. Video poster attribute
3. Instagram window data structures
4. Screenshot fallback (improved)
- [ ] Each method logged for debugging
- [ ] Return base64 data URI for consistency
- [ ] No new dependencies added
- [ ] Backward compatible with existing code
- [ ] Handle all edge cases (missing elements, CORS, etc.)
- [ ] Add 'thumbnail' to ProgressEventType union
- [ ] Emit progress event when thumbnail is extracted
- [ ] Frontend receives thumbnail data in real-time via SSE
#### Technical Specifications
**Research Findings:**
From web research, Instagram thumbnails can be extracted using:
1. **Meta Tags** (Most Stealthy):
- `og:image` - OpenGraph thumbnail
- `twitter:image` - Twitter card thumbnail
- No detection risk, reads HTML only
2. **Video Poster Attribute**:
- `<video poster="...">` attribute
- Direct thumbnail URL
- Low detection risk
3. **Instagram Data Structures**:
- `window.__additionalDataLoaded` object
- GraphQL data in page
- Medium detection risk
4. **Screenshot Fallback**:
- Existing method as last resort
- High detection risk but guaranteed to work
**Progress Event Integration:**
Update the ProgressEventType to include thumbnail events:
```typescript
export type ProgressEventType = 'status' | 'method' | 'retry' | 'error' | 'thumbnail' | 'complete';
```
Emit progress event when thumbnail is extracted:
```typescript
// After successful thumbnail extraction
if (progressCallback) {
progressCallback({
type: 'thumbnail',
message: 'Thumbnail extracted successfully',
data: { thumbnail: thumbnailDataUri },
timestamp: new Date().toISOString()
});
}
```
**Implementation:**
```typescript
/**
* Extract thumbnail from Instagram post using stealth techniques
* Tries multiple methods in order of stealth:
* 1. Meta tags (og:image)
* 2. Video poster attribute
* 3. Instagram window data
* 4. Screenshot fallback
*/
async function extractThumbnailStealth(page: Page, progressCallback?: ProgressCallback): Promise<string | null> {
console.log('[Thumbnail] Starting stealth extraction');
// Method 1: Try meta tags (most stealthy)
try {
const ogImage = await page.getAttribute('meta[property="og:image"]', 'content');
if (ogImage) {
console.log('[Thumbnail] Found og:image meta tag');
const imageBuffer = await fetchImageAsBase64(ogImage);
if (imageBuffer) {
return imageBuffer;
}
}
const twitterImage = await page.getAttribute('meta[name="twitter:image"]', 'content');
if (twitterImage) {
console.log('[Thumbnail] Found twitter:image meta tag');
const imageBuffer = await fetchImageAsBase64(twitterImage);
if (imageBuffer) {
return imageBuffer;
}
}
} catch (e) {
console.log('[Thumbnail] Meta tag method failed:', e);
}
// Method 2: Try video poster attribute
try {
const poster = await page.getAttribute('video', 'poster');
if (poster) {
console.log('[Thumbnail] Found video poster attribute');
const imageBuffer = await fetchImageAsBase64(poster);
if (imageBuffer) {
return imageBuffer;
}
}
} catch (e) {
console.log('[Thumbnail] Video poster method failed:', e);
}
// Method 3: Try Instagram window data structures
try {
const thumbnailUrl = await page.evaluate(() => {
// Check for Instagram's internal data structures
const data = (window as any).__additionalDataLoaded;
if (data) {
// Navigate through Instagram's data structure
for (const key in data) {
const item = data[key];
if (item?.graphql?.shortcode_media?.display_url) {
return item.graphql.shortcode_media.display_url;
}
if (item?.graphql?.shortcode_media?.thumbnail_src) {
return item.graphql.shortcode_media.thumbnail_src;
}
}
}
return null;
});
if (thumbnailUrl) {
console.log('[Thumbnail] Found thumbnail in Instagram data structures');
const imageBuffer = await fetchImageAsBase64(thumbnailUrl);
if (imageBuffer) {
return imageBuffer;
}
}
} catch (e) {
console.log('[Thumbnail] Instagram data method failed:', e);
}
// Method 4: Screenshot fallback (existing method)
console.log('[Thumbnail] Falling back to screenshot method');
return extractThumbnailScreenshot(page);
}
/**
* Helper: Fetch image from URL and convert to base64 data URI
*/
async function fetchImageAsBase64(imageUrl: string): Promise<string | null> {
try {
const response = await fetch(imageUrl);
if (!response.ok) return null;
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const contentType = response.headers.get('content-type') || 'image/jpeg';
return `data:${contentType};base64,${buffer.toString('base64')}`;
} catch (e) {
console.error('[Thumbnail] Failed to fetch image:', e);
return null;
}
}
/**
* Screenshot-based thumbnail extraction (existing method, renamed)
*/
async function extractThumbnailScreenshot(page: Page): Promise<string | null> {
const videoBounds = await page.evaluate(() => {
const video = document.querySelector('video');
if (!video) return null;
const rect = video.getBoundingClientRect();
return {
x: Math.max(0, rect.left),
y: Math.max(0, rect.top),
width: Math.min(rect.width, window.innerWidth),
height: Math.min(rect.height, window.innerHeight)
};
});
let screenshotBuffer: Buffer;
if (videoBounds && videoBounds.width > 0 && videoBounds.height > 0) {
screenshotBuffer = await page.screenshot({
type: 'jpeg',
quality: 85,
clip: videoBounds
});
} else {
console.warn('[Thumbnail] Video element not found, taking full page screenshot');
screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 85 });
}
return `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`;
}
```
#### Implementation Steps
1. Add 'thumbnail' to ProgressEventType union in extraction.ts
2. Rename existing `extractThumbnail()` to `extractThumbnailScreenshot()`
3. Create new `extractThumbnailStealth()` function with optional progressCallback parameter
4. Implement meta tag extraction (Method 1)
5. Implement video poster extraction (Method 2)
6. Implement Instagram data structure extraction (Method 3)
7. Implement `fetchImageAsBase64()` helper
8. Emit progress event after successful thumbnail extraction
9. Add comprehensive logging for debugging
10. Update all extraction method calls to pass progressCallback
11. Test with various Instagram posts (reels, videos, carousels)
#### Edge Cases & Error Handling
- **CORS Issues**: Image URLs might have CORS restrictions - handle gracefully
- **Missing Elements**: Not all posts have all meta tags - try multiple
- **Invalid URLs**: Validate URLs before fetching
- **Network Errors**: Timeout and retry logic
- **Instagram Format Changes**: Fallback ensures functionality
- **Private/Deleted Posts**: Handle gracefully with null return
#### Testing Strategy
- Test with multiple Instagram post types:
- Reels
- Video posts
- Carousel posts
- Story highlights
- Test each extraction method independently
- Verify fallback chain works correctly
- Test with network failures
- Monitor Instagram's detection (no blocks)
#### Files Modified
- `src/lib/server/extraction.ts`
#### Migration Notes
Replace all occurrences of:
```typescript
const thumbnail = await extractThumbnail(page);
```
With:
```typescript
const thumbnail = await extractThumbnailStealth(page);
```
The function signature remains the same, ensuring backward compatibility.
---
### Story 4: Create Thumbnail Preview Component
**Priority:** Medium
**Complexity:** Low
**Estimated Effort:** 2 hours
#### Description
Create a dedicated component to display the extracted thumbnail in real-time as soon as it's available, separate from the recipe display. This provides immediate visual feedback to users during the extraction process.
#### Acceptance Criteria
- [ ] Create `ThumbnailPreview.svelte` component
- [ ] Component displays thumbnail image when available
- [ ] Shows loading skeleton while thumbnail is being extracted
- [ ] Shows error state if thumbnail extraction fails
- [ ] Responsive design with proper aspect ratio
- [ ] Integrates into share page between progress indicator and logs
- [ ] Updates in real-time when thumbnail event is received
- [ ] Maintains aspect ratio and prevents layout shift
#### Technical Specifications
**Component Interface:**
```typescript
// ThumbnailPreview.svelte
interface Props {
thumbnail: string | null;
status: 'idle' | 'extracting' | 'success' | 'error';
}
```
**Implementation Pattern:**
```svelte
<script lang="ts">
let { thumbnail = null, status = 'idle' } = $props();
</script>
{#if status === 'extracting'}
<div class="border rounded-lg p-4 bg-gray-50">
<div class="flex items-center gap-3 mb-2">
<div class="animate-spin text-blue-600">🎨</div>
<span class="text-sm font-medium text-gray-700">Extracting thumbnail...</span>
</div>
<!-- Loading skeleton -->
<div class="w-full aspect-square bg-gray-200 animate-pulse rounded"></div>
</div>
{:else if status === 'success' && thumbnail}
<div class="border rounded-lg p-4 bg-white shadow-sm">
<div class="flex items-center gap-2 mb-2">
<span class="text-green-600"></span>
<span class="text-sm font-medium text-gray-700">Thumbnail extracted</span>
</div>
<img
src={thumbnail}
alt="Post thumbnail"
class="w-full aspect-square object-cover rounded"
/>
</div>
{:else if status === 'error'}
<div class="border border-red-200 rounded-lg p-4 bg-red-50">
<div class="flex items-center gap-2">
<span class="text-red-600"></span>
<span class="text-sm font-medium text-red-700">Thumbnail extraction failed</span>
</div>
</div>
{/if}
```
**Integration with Share Page:**
Update the share page to:
1. Add thumbnail state: `let thumbnail = $state<string | null>(null)`
2. Add thumbnail status: `let thumbnailStatus = $state<'idle' | 'extracting' | 'success' | 'error'>('idle')`
3. Listen for thumbnail progress events in SSE handler
4. Update thumbnail state when event is received
```typescript
// In the SSE event handler (+page.svelte)
if (event.type === 'thumbnail') {
thumbnail = event.data?.thumbnail || null;
thumbnailStatus = thumbnail ? 'success' : 'error';
logs = [...logs, `🎨 ${event.message}`];
}
```
#### Implementation Steps
1. Create `src/routes/share/components/ThumbnailPreview.svelte`
2. Implement loading skeleton state
3. Implement success state with image display
4. Implement error state
5. Add responsive styling with TailwindCSS
6. Update `+page.svelte` to add thumbnail state variables
7. Add thumbnail event handler to SSE processing
8. Integrate component into share page layout
9. Set thumbnailStatus to 'extracting' when extraction starts
10. Test with real Instagram URLs
#### Layout Integration
The component should be placed in the share page layout as:
```svelte
<div class="p-8 max-w-lg mx-auto space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">InstaChef PWA</h1>
<LlmHealthIndicator />
</div>
<UrlInputSection {targetUrl} {sharedText} {sharedUrl} {status} {onProcess} />
<ProgressIndicator {status} />
<!-- Thumbnail Preview - NEW -->
<ThumbnailPreview {thumbnail} status={thumbnailStatus} />
<ExtractedTextViewer {bodyText} />
<RecipeCard {recipe} {tandoorEnabled} {tandoorImporting} {tandoorError} {onRetry} {onImportToTandoor} />
<ErrorState {status} {bodyText} {onRetry} />
<LogViewer {logs} {currentMethod} {status} />
</div>
```
#### Testing Strategy
- Test loading state appears immediately when extraction starts
- Test thumbnail appears when event is received
- Test error state when thumbnail extraction fails
- Verify no layout shift when thumbnail loads
- Test responsive behavior on mobile devices
- Verify aspect ratio is maintained
- Test with various image sizes and formats
#### Files Created
- `src/routes/share/components/ThumbnailPreview.svelte`
#### Files Modified
- `src/routes/share/+page.svelte` (add thumbnail state and event handling)
#### User Experience Benefits
- **Immediate Feedback**: Users see the thumbnail as soon as it's extracted, not waiting for full recipe
- **Visual Confirmation**: Confirms correct post is being processed
- **Progressive Loading**: Shows extraction progress step-by-step
- **Error Visibility**: Clear indication if thumbnail extraction fails
---
## Implementation Order
1. **Story 1**: Component Refactoring (Foundation)
- Establishes clean component structure
- Makes future changes easier
2. **Story 2**: LLM Health Indicator (Quick Win)
- Independent of other stories
- Provides immediate user value
3. **Story 3**: Thumbnail Enhancement (Complex)
- Implements stealth extraction methods
- Adds progress event emission
- Requires thorough testing
4. **Story 4**: Thumbnail Preview Component (Integration)
- Depends on Story 1 (component structure) and Story 3 (thumbnail events)
- Displays extracted thumbnails in real-time
- Enhances user experience
---
## Dependencies & Prerequisites
### External Dependencies
- None (uses existing tech stack)
### Internal Dependencies
- Story 2 is independent and can be done anytime
- Story 1 should be completed first (provides component structure)
- Story 3 should be completed before Story 4 (provides thumbnail events)
- Story 4 depends on Story 1 and Story 3
### Environment Requirements
- LM Studio running for Story 2 testing
- Valid Instagram URLs for Story 3 testing
---
## Risk Assessment
### High Risk
- **Instagram Format Changes**: Instagram may change their data structures
- *Mitigation*: Multi-method approach with screenshot fallback
### Medium Risk
- **Component Refactoring Errors**: Breaking existing functionality during split
- *Mitigation*: Thorough manual testing, incremental migration
### Low Risk
- **LLM Health Polling Performance**: Frequent polling might impact performance
- *Mitigation*: Configurable interval, can be disabled if needed
---
## Success Metrics
### Code Quality
- Share page reduced from 306 to ~100 lines
- Component cohesion: each component < 80 lines
- Test coverage: manual verification of all user flows
### User Experience
- LLM status visible before extraction attempt
- Thumbnail extraction success rate > 95%
- No Instagram blocking/detection
### Performance
- Page load time unchanged
- Health polling doesn't impact extraction speed
- Thumbnail extraction time reduced (meta tags are faster)
---
## Documentation Updates
### Code Documentation
- JSDoc comments for all new functions
- Component prop interfaces documented
- Extraction methods logged for debugging
### User Documentation
- No user-facing docs needed (internal feature)
---
## Rollback Plan
### Story 1 Rollback
- Revert `+page.svelte` to previous version
- Delete component files
### Story 2 Rollback
- Remove `LlmHealthIndicator` import from share page
- Delete component file
### Story 3 Rollback
- Revert extraction.ts changes
- Restore original `extractThumbnail()` function
- Remove 'thumbnail' from ProgressEventType
### Story 4 Rollback
- Remove ThumbnailPreview import from share page
- Remove thumbnail state variables from +page.svelte
- Remove thumbnail event handler from SSE processing
- Delete ThumbnailPreview.svelte component file
---
## Testing Checklist
### Story 1: Component Refactoring
- [ ] All components render correctly
- [ ] State flows from parent to children
- [ ] Callbacks trigger parent functions
- [ ] Styling matches original
- [ ] No console errors
- [ ] Extraction flow works end-to-end
### Story 2: LLM Health Indicator
- [ ] Shows yellow "checking" on load
- [ ] Shows green when LLM is healthy
- [ ] Shows red when LLM is down
- [ ] Polling works (check network tab)
- [ ] Cleanup on unmount (no memory leaks)
- [ ] Tooltip shows status message
### Story 3: Thumbnail Enhancement
- [ ] Meta tag extraction works
- [ ] Video poster extraction works
- [ ] Instagram data extraction works
- [ ] Screenshot fallback works
- [ ] All methods logged correctly
- [ ] No Instagram blocking detected
- [ ] Thumbnail progress event is emitted
- [ ] Event includes thumbnail data
### Story 4: Thumbnail Preview Component
- [ ] Loading skeleton shows when extraction starts
- [ ] Thumbnail displays when event received
- [ ] Error state shows on extraction failure
- [ ] No layout shift when thumbnail loads
- [ ] Responsive on mobile devices
- [ ] Aspect ratio maintained correctly
- [ ] Component receives real-time updates
---
## Estimated Total Effort
- Story 1: 4 hours
- Story 2: 2 hours
- Story 3: 6 hours
- Story 4: 2 hours
- Testing & Integration: 2 hours
**Total: ~16 hours**
---
## Next Steps
Once this plan is approved:
1. Create feature branch: `feature/refactor-share-page-thumbnails`
2. Implement stories in order (1 → 2 → 3)
3. Test each story before moving to next
4. Create pull request with comprehensive testing results
5. Deploy to production after approval
---
## References
### External Documentation
- [Playwright Meta Tag Testing](https://stackoverflow.com/questions/72604449/test-meta-tags-with-playwright)
- [Playwright Stealth Techniques](https://www.webscrappinghq.com/blog/ultimate-guide-to-anti-bot-measures-in-playwright)
- [OpenGraph Protocol](https://ogp.me/)
### Internal Documentation
- [Hexagonal Architecture](.system/abstract_architecture.md)
- [Existing Outcome: RefactorFrontendAndFixLLMExtraction](docs/outcomes/RefactorFrontendAndFixLLMExtraction.md)
---
**Plan Status**: ✅ Ready for Implementation
**Developer Command**: `@dev RefactorSharePageAndEnhanceThumbnails`