- Fix EventSource is not defined error in queue dashboard - Add browser guards for all EventSource usage - Replace static constants (EventSource.OPEN/CLOSED) with numeric values - Fix setInterval SSR violation in LLM health indicator - Replace $effect anti-pattern with onMount in share page - Add comprehensive SvelteKit SSR best practices documentation - Add SSR audit and testing verification All changes follow SvelteKit best practices and are verified against official documentation. Production build succeeds with no SSR errors. Closes: FixEventSourceSSR See: docs/outcomes/FixEventSourceSSR.md
23 KiB
Execution Plan: Fix Push Notification SSR Bug, Regenerate SSL, and Code Cleanup
Context
The application is experiencing a critical SSR (Server-Side Rendering) bug where PushNotificationManager attempts to access localStorage during server-side rendering, causing the application to crash:
ReferenceError: localStorage is not defined
at PushNotificationManager.generateClientId (src/lib/client/PushNotificationManager.ts:256:20)
at new PushNotificationManager (src/lib/client/PushNotificationManager.ts:31:26)
Additionally:
- The SSL certificate expired on Dec 21, 2025 (yesterday)
- The codebase contains dead/unused code that should be deleted
- There are opportunities to consolidate duplicate code
CRITICAL: All work must be done in the current branch (feat/async-in-memory-processing-queue), not a new branch.
Research Summary
SvelteKit SSR & localStorage Best Practices
From SvelteKit documentation and community best practices:
- Browser API Detection: Use
browserfrom$app/environmentto check if code is running in browser - Lazy Initialization: Don't access browser APIs at module level or in constructors
- onMount Lifecycle: Use Svelte's
onMountfor browser-only initialization - Guard Pattern: Wrap all browser API access with browser checks
Key Pattern:
import { browser } from '$app/environment';
if (browser) {
// Browser-only code here
localStorage.getItem('key');
}
SSL Certificate Strategy
For local development with 10-year validity:
- Leverage the external Caddy container's CA (already trusted on the system)
- Extract Caddy's CA private key to sign a custom certificate with 10-year validity
- Use OpenSSL to generate and sign the certificate with Caddy's CA
- No manual trust steps needed - Caddy CA already trusted
- Alternative: Use Caddy's automatic generation if 10-year validity not strictly required (90-day certs)
User Stories
Story 0: Fix PushNotificationManager SSR Issue 🔴 CRITICAL
As a developer I want the PushNotificationManager to work correctly in SSR context So that the application doesn't crash when components are rendered on the server
Acceptance Criteria:
- ✅ PushNotificationManager constructor does not access
localStorage - ✅
clientIdis generated lazily only in browser context - ✅ All browser APIs (window, Notification, navigator) are guarded with browser checks
- ✅ Module-level singleton instantiation is safe for SSR
- ✅ NotificationSettings.svelte component works without errors
- ✅ No SSR-related errors in console
- ✅ Push notifications still work correctly in browser
Technical Approach:
-
Lazy ClientId Generation:
import { browser } from '$app/environment'; class PushNotificationManager { private _clientId: string | null = null; private get clientId(): string { if (!this._clientId && browser) { this._clientId = this.generateClientId(); } return this._clientId || 'ssr-fallback'; } private generateClientId(): string { if (!browser) return ''; const stored = localStorage.getItem('push-client-id'); if (stored) return stored; const id = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; localStorage.setItem('push-client-id', id); return id; } } -
Guard Browser API Checks:
private checkSupport(): void { if (!browser) { this.state.supported = false; this.state.permission = 'denied'; return; } this.state.supported = ( 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ); this.state.permission = this.state.supported ? Notification.permission : 'denied'; } -
Safe Service Worker Initialization:
private async initializeServiceWorker(): Promise<void> { if (!browser || !this.state.supported) return; // Rest of initialization }
Files:
src/lib/client/PushNotificationManager.ts(update)src/routes/components/NotificationSettings.svelte(verify)
Testing:
- Test component renders without errors in SSR
- Test push notification subscribe/unsubscribe in browser
- Test that clientId persists across browser sessions
- Verify no localStorage access during SSR
Story 1: Generate 10-Year SSL Certificate Using External Caddy CA
As a developer I want a valid SSL certificate with 10-year validity signed by the external Caddy CA So that I don't have to regenerate certificates frequently and they're automatically trusted
Acceptance Criteria:
- ✅ New SSL certificate valid for 10 years (3650 days)
- ✅ Certificate signed by existing Caddy CA (already trusted on system)
- ✅ Certificate files in
.ssl/directory:localhost.key(private key)localhost.crt(certificate signed by Caddy CA)root.crt(Caddy CA certificate - copied from container)
- ✅ Certificate automatically trusted (no manual trust needed)
- ✅
vite.config.tspoints to correct certificate files - ✅ Certificate expiration date verified: ~2035
- ✅ Caddy container ID identified or documented
Technical Approach:
This approach leverages the external Caddy container's CA that's already trusted on the system, but generates a certificate with custom 10-year validity.
-
Identify Caddy Container:
# Find the Caddy container docker ps | grep caddy # Or use the known ID from previous work (might have changed) CADDY_CONTAINER=$(docker ps --filter "ancestor=caddy" --format "{{.ID}}" | head -1) echo "Caddy container: $CADDY_CONTAINER" -
Export Caddy's CA Certificate and Private Key:
# Copy the CA certificate (already done, but verify it exists) docker cp $CADDY_CONTAINER:/data/caddy/pki/authorities/local/root.crt .ssl/root.crt # Copy the CA private key (needed to sign our custom certificate) docker cp $CADDY_CONTAINER:/data/caddy/pki/authorities/local/root.key .ssl/caddy-ca.key # Verify CA certificate openssl x509 -in .ssl/root.crt -text -noout | grep "Subject:" -
Generate New Server Certificate with 10-Year Validity:
# Generate server private key (2048-bit is sufficient) openssl genrsa -out .ssl/localhost.key 2048 # Generate Certificate Signing Request (CSR) openssl req -new \ -key .ssl/localhost.key \ -out .ssl/localhost.csr \ -subj "/O=Caddy Local Authority/CN=localhost" # Create OpenSSL config for Subject Alternative Names (SAN) cat > .ssl/localhost.ext << 'EOF'
authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names
[alt_names] DNS.1 = localhost DNS.2 = *.localhost IP.1 = 127.0.0.1 IP.2 = ::1 EOF
Sign the certificate with Caddy's CA (10 years = 3650 days)
openssl x509 -req
-in .ssl/localhost.csr
-CA .ssl/root.crt
-CAkey .ssl/caddy-ca.key
-CAcreateserial
-out .ssl/localhost.crt
-days 3650
-sha256
-extfile .ssl/localhost.ext
Cleanup temporary files and CA private key (security)
rm .ssl/localhost.csr .ssl/localhost.ext .ssl/caddy-ca.key
Set restrictive permissions
chmod 600 .ssl/localhost.key chmod 644 .ssl/localhost.crt .ssl/root.crt
4. **Verify Certificate:**
```bash
# Check expiration date (should be ~2035)
openssl x509 -enddate -noout -in .ssl/localhost.crt
# Verify certificate is signed by Caddy CA
openssl verify -CAfile .ssl/root.crt .ssl/localhost.crt
# Check certificate details
openssl x509 -in .ssl/localhost.crt -text -noout | grep -A 1 "Subject:"
openssl x509 -in .ssl/localhost.crt -text -noout | grep -A 3 "Subject Alternative Name"
- Verify Vite Configuration:
# Ensure vite.config.ts already points to correct files grep -A 3 "https:" vite.config.ts
Alternative: If Caddy CA Private Key is Not Accessible
If the CA private key is not accessible from the container, use Caddy's built-in certificate generation but with a workaround:
-
Trigger Caddy Certificate Generation:
# Run temporary Caddy reverse-proxy to trigger cert generation docker exec -d $CADDY_CONTAINER caddy reverse-proxy \ --from localhost:8443 \ --to localhost:8080 # Wait for certificate generation (5-10 seconds) sleep 10 # Stop the temporary process docker exec $CADDY_CONTAINER pkill -f "caddy reverse-proxy" -
Copy Generated Certificates:
# Copy Caddy-generated certificates docker cp $CADDY_CONTAINER:/data/caddy/certificates/local/localhost/localhost.crt .ssl/ docker cp $CADDY_CONTAINER:/data/caddy/certificates/local/localhost/localhost.key .ssl/ docker cp $CADDY_CONTAINER:/data/caddy/pki/authorities/local/root.crt .ssl/ -
Note on Validity:
- Caddy-generated certificates typically have 90-day validity
- If 10-year validity is required, must use OpenSSL approach with CA key
- Document renewal process in README if using short-lived certs
Files:
.ssl/localhost.key(create - server private key).ssl/localhost.crt(create - server certificate signed by Caddy CA).ssl/root.crt(copy from Caddy container - CA certificate)README.md(update with certificate info and renewal instructions).gitignore(verify .ssl/ is ignored except for .gitkeep)
Testing:
- Verify certificate dates:
openssl x509 -enddate -noout -in .ssl/localhost.crt - Verify CA signature:
openssl verify -CAfile .ssl/root.crt .ssl/localhost.crt - Test HTTPS server starts:
npm run dev - Verify browser shows secure connection (should be automatic - CA already trusted)
- Test certificate valid until ~2035 (if using OpenSSL approach)
Documentation Note: Since the Caddy CA is already trusted on the system, no manual trust steps are needed. Document in README:
- How to check certificate expiration
- How to regenerate using same process
- Caddy container identification steps
Story 2: Audit and Delete Dead/Unused Code
As a developer I want to remove all dead and unused code from the codebase So that the codebase is cleaner and easier to maintain
Acceptance Criteria:
- ✅ All unused imports removed
- ✅ All unreferenced functions/types deleted
- ✅ All commented-out code blocks removed
- ✅ Unused test fixtures cleaned up
- ✅ No deprecation markers (code is deleted, not deprecated)
- ✅ All tests still passing
- ✅ No broken imports or references
Audit Areas:
-
Check for Unused Imports:
# Use TypeScript compiler to find unused imports npx tsc --noEmit # Or use eslint if configured npm run lint -
Scan for Unreferenced Code:
- Search for functions/classes that are never imported
- Check test files for unused fixtures
- Look for commented-out code blocks (
//,/* */)
-
Verify Deprecated Endpoints:
/api/extractreturns 410 Gone ✅ KEEP (migration helper)/api/extract-streamalready deleted ✅- Check for any other deprecated routes
-
Clean Up Test Files:
src/tests/fixtures.ts- review localStorage fixtures- Remove any unused test helpers
- Delete obsolete test files
-
Review Client Components:
ServiceWorkerMessageHandler.ts- verify usage- Check for unused utility functions
Files to Review:
src/lib/client/*- Client utilitiessrc/tests/*- Test files and fixturessrc/routes/components/*- UI components- All import statements across codebase
Deletion Checklist:
- Unused imports removed
- Commented-out code deleted
- Unreferenced functions deleted
- Obsolete test fixtures removed
- Dead code paths eliminated
- Verify no broken imports with
npx tsc --noEmit
Testing:
- Run full test suite:
npm test - Build project:
npm run build - Check for TypeScript errors:
npx tsc --noEmit - Verify dev server starts:
npm run dev
Story 3: Consolidate Duplicate Code
As a developer I want to consolidate duplicate and similar code So that the codebase has less redundancy and is easier to maintain
Acceptance Criteria:
- ✅ Duplicate type definitions merged
- ✅ Similar utility functions consolidated
- ✅ Repeated code blocks extracted to functions
- ✅ Common patterns extracted to shared utilities
- ✅ No functionality broken
- ✅ All tests still passing
Consolidation Areas:
-
Type Definitions:
- Check for duplicate interfaces/types across files
- Move shared types to appropriate locations:
- Domain types →
src/lib/server/queue/types.ts - Client types →
src/lib/client/types.ts(create if needed) - Shared types →
src/lib/types.ts(create if needed)
- Domain types →
-
Utility Functions:
- Look for similar string formatting functions
- Check for duplicate validation logic
- Identify common data transformation patterns
-
Component Patterns:
- Similar error handling across components
- Repeated state management patterns
- Common UI patterns
-
API Response Handling:
- Similar fetch patterns
- Duplicate error handling
- Common response transformations
Investigation Steps:
-
Search for Duplicate Type Definitions:
# Look for common type names grep -r "interface.*State" src/ grep -r "type.*Config" src/ -
Find Similar Function Signatures:
# Look for validation functions grep -r "function validate" src/ grep -r "async function.*fetch" src/ -
Identify Repeated Patterns:
- SSE connection setup
- Error handling blocks
- Loading state management
- Form validation
Consolidation Strategy:
For each duplicate found:
- Determine the most complete/correct version
- Extract to shared location if used in multiple places
- Update all references to use shared version
- Delete duplicate versions
- Verify tests pass
Files:
- Potentially create:
src/lib/utils/directory for shared utilities - Potentially create:
src/lib/types.tsfor shared types - Update all files with consolidated references
Testing:
- Run full test suite after each consolidation
- Verify no regression in functionality
- Check TypeScript compilation succeeds
Story 4: Verify and Test Complete Solution
As a developer I want to verify all changes work correctly together So that the fixes are production-ready
Acceptance Criteria:
- ✅ All unit tests passing
- ✅ Integration tests passing
- ✅ No SSR errors in development
- ✅ No SSR errors in production build
- ✅ SSL certificate works correctly
- ✅ Push notifications work in browser
- ✅ No console warnings or errors
- ✅ Application builds successfully
- ✅ All TypeScript errors resolved
Testing Checklist:
-
SSR Testing:
# Test dev server (SSR enabled) npm run dev # Visit pages and check console for errors # Test production build npm run build npm run preview -
Push Notification Testing:
- Open NotificationSettings component
- Verify no SSR errors
- Test subscribe/unsubscribe in browser
- Verify clientId persists across refresh
-
SSL Certificate Testing:
- Verify HTTPS connection works
- Check certificate validity in browser
- Test across different browsers (Chrome, Firefox)
-
Code Quality:
# TypeScript check npx tsc --noEmit # Linting npm run lint # Unit tests npm test # Build npm run build -
Manual Testing:
- Test all queue operations
- Test extraction flow
- Verify push notifications
- Check HTTPS connection
- Test on mobile browsers (if applicable)
Regression Testing:
- Queue creation works
- SSE progress updates work
- Extraction completes successfully
- Tandoor integration works
- All existing features functional
Performance Check:
- Bundle size acceptable
- No memory leaks
- Reasonable load times
- No performance degradation
Technical Specifications
Browser API Guard Pattern
All browser API access must follow this pattern:
import { browser } from '$app/environment';
// Module level - safe for SSR
class MyClass {
private browserOnlyState: SomeType | null = null;
// Constructor - safe for SSR
constructor() {
// NO browser API access here
}
// Methods can check browser context
someMethod() {
if (!browser) {
return; // or return safe default
}
// Browser APIs safe here
const data = localStorage.getItem('key');
}
// Lazy initialization pattern
private _clientId: string | null = null;
private get clientId(): string {
if (!this._clientId && browser) {
this._clientId = this.initializeClientId();
}
return this._clientId || 'fallback-value';
}
}
SSL Certificate File Structure
.ssl/
├── localhost.key # Server private key (2048-bit RSA)
├── localhost.crt # Server certificate (signed by Caddy CA, 10 years)
├── root.crt # Caddy CA certificate (copied from container, already trusted)
└── .gitkeep # Track directory but ignore contents
Code Deletion Guidelines
-
Before Deleting:
- Search entire codebase for references
- Check test files for usage
- Verify not used in comments or documentation
- Check git history for context
-
Safe to Delete:
- No references found
- Confirmed not used in any import
- Not referenced in documentation
- Clearly obsolete/deprecated
-
Keep but Document:
- Migration helper endpoints (like /api/extract)
- Fallback strategies (like legacy extraction)
- Backward compatibility shims
-
Delete Immediately:
- Commented-out code
- Unused imports
- Unreferenced functions
- Obsolete test fixtures
Dependencies
Story Dependencies
- Story 0 (SSR Fix) → No dependencies, can start immediately
- Story 1 (SSL) → No dependencies, can start immediately
- Story 2 (Dead Code) → Should wait for Story 0 completion
- Story 3 (Consolidation) → Should wait for Story 2 completion
- Story 4 (Verification) → Depends on all previous stories
Execution Order
- Story 0 - Critical SSR fix (blocks development)
- Story 1 - SSL regeneration (parallel with Story 0)
- Story 2 - Dead code cleanup
- Story 3 - Code consolidation
- Story 4 - Final verification and testing
Risk Assessment
High Risk
Risk: Breaking push notification functionality
- Impact: Users lose real-time updates
- Likelihood: Medium
- Mitigation: Thorough testing in browser and SSR contexts
- Rollback: Revert PushNotificationManager changes, keep old version
Risk: SSL certificate not trusted by system
- Impact: Development blocked, HTTPS warnings
- Likelihood: Low (clear instructions provided)
- Mitigation: Detailed trust instructions for all platforms
- Rollback: Regenerate old certificate or disable HTTPS temporarily
Medium Risk
Risk: Deleting code that's actually used
- Impact: Runtime errors, broken functionality
- Likelihood: Low (comprehensive search before delete)
- Mitigation: Thorough searching, test suite verification
- Rollback: Git revert specific deletions
Risk: Consolidation introducing subtle bugs
- Impact: Broken functionality in edge cases
- Likelihood: Low
- Mitigation: Incremental consolidation, test after each change
- Rollback: Git revert to pre-consolidation state
Low Risk
Risk: TypeScript compilation errors after changes
- Impact: Development blocked temporarily
- Likelihood: Very Low
- Mitigation: Run tsc check frequently
- Rollback: Easy to fix type errors
Testing Strategy
Unit Tests
- Test PushNotificationManager in isolation
- Mock browser APIs for testing
- Test lazy initialization patterns
- Verify state management
Integration Tests
- Test NotificationSettings component
- Verify SSE integration still works
- Test queue system end-to-end
- Verify extraction pipeline
SSR Tests
- Render components server-side
- Verify no localStorage access
- Check no window/navigator access
- Ensure safe module initialization
Manual Tests
- Browser push notifications
- SSL certificate trust
- HTTPS connection
- Cross-browser compatibility
Documentation Updates
README.md
Add/update sections:
- SSL Certificate Setup (detailed trust instructions)
- HTTPS Development Setup
- Browser Requirements
- Troubleshooting SSL issues
Code Comments
- Document browser API guard patterns
- Explain lazy initialization approach
- Note SSR safety considerations
- Document clientId generation logic
Success Metrics
- Zero SSR Errors: No localStorage or browser API errors during SSR
- Push Notifications Working: Subscribe/unsubscribe functional in browser
- SSL Valid: Certificate valid until ~2035, trusted by browsers
- Clean Codebase: No unused imports, no dead code, no duplicates
- All Tests Passing: 100% test suite success rate
- TypeScript Clean: Zero compilation errors
- No Console Errors: Clean browser console in dev and prod
Rollback Plan
If critical issues arise:
-
SSR Fix Rollback:
git revert <commit-hash-of-ssr-fix> # Or restore old PushNotificationManager.ts -
SSL Rollback:
# Generate quick temporary certificate openssl req -x509 -newkey rsa:2048 -nodes \ -keyout .ssl/localhost.key \ -out .ssl/localhost.crt \ -days 365 -subj "/CN=localhost" -
Code Cleanup Rollback:
git revert <cleanup-commit-hash> # Or restore specific deleted files from git history -
Full Rollback:
# Reset to before all changes git reset --hard <commit-before-changes>
Timeline Estimate
- Story 0 (SSR Fix): 2-3 hours
- Story 1 (SSL): 1-2 hours (can be parallel)
- Story 2 (Dead Code): 2-4 hours
- Story 3 (Consolidation): 3-5 hours
- Story 4 (Verification): 1-2 hours
Total Estimated Time: 9-16 hours
Branch Strategy
⚠️ IMPORTANT: All work MUST be done in the current branch:
- Branch:
feat/async-in-memory-processing-queue - Do NOT create a new feature branch
- Commit incrementally with clear messages
- Keep all changes contained in this branch
Completion Criteria
The plan is complete when:
- ✅ PushNotificationManager works in both SSR and browser contexts
- ✅ No localStorage errors in any context
- ✅ SSL certificate valid for 10 years
- ✅ HTTPS development server working
- ✅ All dead code deleted (not deprecated)
- ✅ All duplicate code consolidated
- ✅ All tests passing
- ✅ No TypeScript errors
- ✅ No console warnings/errors
- ✅ Application builds successfully
- ✅ Documentation updated
- ✅ All changes committed to current branch
Notes
- SvelteKit documentation emphasizes avoiding browser APIs in SSR context
- The
browserenvironment variable is the recommended pattern - SSL certificates for local development typically don't need to be from a real CA
- 10-year validity is reasonable for local development certificates
- Code should be deleted, not deprecated, when truly unused
- Consolidation should focus on real duplicates, not just similar patterns
- Keep backward compatibility for migration helper endpoints