Removed unused imports and state variables that were causing TypeScript build errors:
- Removed YachtSearch import (not used)
- Removed calculateTripCarbon import (not used)
- Removed getVesselData import (only used in unused handleSearch)
- Removed CarbonCalculation type import (not used)
- Removed unused state variables: loading, error, vesselData
- Removed unused handleSearch function
This fixes the Docker build failure: 'YachtSearch' is declared but its value is never read.
Remove unused 'React' imports from App.tsx and all component files in project/src/components/. These imports are not needed with the modern JSX transform and were causing TypeScript build errors in strict mode.
Files updated:
- project/src/App.tsx
- project/src/components/About.tsx
- project/src/components/CarbonOffset.tsx
- project/src/components/Contact.tsx
- project/src/components/CurrencySelect.tsx
- project/src/components/ErrorBoundary.tsx
- project/src/components/Home.tsx
- project/src/components/HowItWorks.tsx
- project/src/components/OffsetOrder.tsx
- project/src/components/PuffinAdvantage.tsx
- project/src/components/TripCalculator.tsx
- project/src/components/YachtSearch.tsx
This fixes the Docker build failure caused by TypeScript strict mode detecting unused variables.
This is a major migration from Vite to Next.js 16.0.1 for improved
performance, better SEO, and modern React features.
## Next.js Migration Changes
- Upgraded to Next.js 16.0.1 with Turbopack (from Vite 6)
- Migrated from client-side routing to App Router architecture
- Created app/ directory with Next.js page structure
- Added server components and client components pattern
- Configured standalone Docker builds for production
## Bug Fixes - React Hooks
- Fixed infinite loop in Header.tsx scroll behavior (removed lastScrollY state dependency)
- Fixed infinite loop in useCalculatorState.ts (wrapped saveState/clearState in useCallback)
- Fixed infinite loop in OffsetOrder.tsx (removed savedState from useEffect dependencies)
- Removed unused React imports from all client components
## Environment Variable Migration
- Migrated all VITE_ variables to NEXT_PUBLIC_ prefix
- Updated src/utils/config.ts to use direct static references (required for Next.js)
- Updated src/api/checkoutClient.ts, emailClient.ts, aisClient.ts for Next.js env vars
- Updated src/vite-env.d.ts types for Next.js environment
- Maintained backward compatibility with Docker window.env
## Layout & UX Improvements
- Fixed footer to always stay at bottom of viewport using flexbox
- Updated app/layout.tsx with flex-1 main content area
- Preserved glass morphism effects and luxury styling
## TypeScript & Build
- Fixed TypeScript strict mode compilation errors
- Removed unused imports and variables
- Fixed Axios interceptor types in project/src/api/wrenClient.ts
- Production build verified and passing
## Testing & Verification
- Tested calculator end-to-end in Playwright
- Verified Wren API integration working (11 portfolios fetched)
- Confirmed calculation: 5000L → 13.47 tons CO₂ → $3,206 total
- All navigation routes working correctly
- Footer positioning verified across all pages
## Files Added
- app/ directory with Next.js routes
- components/ directory with client components
- next.config.mjs, next-env.d.ts
- ENV_MIGRATION.md, NEXTJS_MIGRATION_COMPLETE.md documentation
## Files Modified
- Docker configuration for Next.js standalone builds
- package.json dependencies (Next.js, React 19)
- ts config.json for Next.js
- All API clients for new env var pattern
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Carbon Offset Distribution section:
- Remove "Carbon Offset" subheader from each project
- Remove colored dots next to project names
- Reduce padding from 30px to 20px
- Reduce title margin from 24px to 16px
- Reduce item padding from 14px to 10px
- Reduce spacing between items from 12px to 8px
Your Impact section:
- Reduce padding from 32px to 20px
- Reduce title font size from 26px to 22px
- Reduce subtitle margin from 28px to 16px
- Reduce card padding from 20px to 14px
- Reduce icon size from 36px to 30px
- Reduce value font size from 28px to 24px
- Reduce grid gap from 16px to 12px
- Reduce footer note margin from 20px to 12px
Overall more compact layout while maintaining readability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Update default admin email from admin@ to matt@puffinoffset.com
- Add ADMIN_EMAIL environment variable to docker-compose.yml
- Add complete Email Configuration section to .env.example
- Admin email receives contact form submissions and order notifications
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove portfolio ID display from both:
- Email receipt template (receipt.hbs)
- Digital receipt page (CheckoutSuccess.tsx)
Portfolio information is internal and not needed in customer-facing receipts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove escaped dollar signs in currency displays
- Fix percentage calculations (multiply decimal by 100)
- Force white text color on mobile for "Your Impact" header
- Add comma formatting to currency values (16343.46 → 16,343.46)
- Update portfolioColors.js to properly convert Wren API decimal percentages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Replace Formspree integration with new backend email API
- Update Contact, OffsetOrder, and MobileOffsetOrder components
- Remove Formspree config from environment variables
- Add emailClient API for backend communication
- Centralize email sending through SMTP service
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add beautiful HTML email templates for receipts, admin notifications, and contact forms
- Implement SMTP email service with Nodemailer and Handlebars templating
- Add carbon equivalency calculations with EPA/DEFRA/IMO 2024 conversion factors
- Add portfolio color palette system for project visualization
- Integrate Wren API portfolio fetching in webhook handler
- Add light mode enforcement for email client compatibility
- Include Puffin logo from MinIO S3 in all templates
- Add test email endpoint for template validation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Implement aggressive spacing compression (padding: 0.5rem, margins: 0.25rem)
- Reduce all font sizes by 10-25% for better vertical fit
- Compress line-height to 1.3 for tighter spacing
- Add print-color-adjust: exact to force gradients and colors to print
- Optimize logo, grid gaps, and section spacing
- Keep ALL gradient backgrounds and colorful styling
Result: Payment ID, Status, Email, Date now fit on page 1 with beautiful colors
Based on comprehensive Zen analysis of print layout optimization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Replace pie chart with print-optimized table to eliminate label overlap
- Add extensive @media print CSS for high-contrast, professional output
- Convert all gradients to white backgrounds with black borders
- Optimize spacing and font sizes for print media
- Update CarbonImpactComparison with high-contrast print styling
- Ensure full page width utilization and proper page breaks
- Zero new dependencies (Pure CSS approach per consensus)
Resolves print quality issues: label collision, poor contrast, wasted whitespace
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Combine project name and percentage into single label line
- Format: "Project Name (25.0%)"
- Reduces vertical space and improves readability
- Cleaner, more compact label design
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increase max width from max-w-3xl to max-w-4xl/5xl for better screen fit
- Add responsive breakpoints: full width on mobile, constrained on larger screens
- Add print-page-break class for automatic page breaks between sections
- Each major section now prints on separate page:
- Page 1: Order summary with payment details
- Page 2: Portfolio distribution chart
- Page 3: Carbon impact comparisons
- Improve print styling: remove shadows, borders, rounded corners
- Set print page size to standard letter format
- Reduce padding on mobile for better space utilization
Fixes receipt layout to be responsive on all screen sizes while
maintaining excellent print quality with proper page breaks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove middle tonnage line from chart labels
- Keep only project name and percentage
- Tonnage still visible in legend below chart
- Reduces label clutter and improves readability
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Reduce label font sizes: 10px name, 9px details (was 14px/12px)
- Add print-friendly smaller sizes: 7px/6px
- Make container fully responsive with max-width constraints
- Reduce label extension distance from ±50px to ±30px
- Reduce label radius from outerRadius+60 to outerRadius+35
- Use fixed pie dimensions (80/50) instead of calculated sizes
- Reduce connector line width from 2px to 1.5px
- Add print-specific max-width and height constraints
Fixes text overflow and makes chart fit properly on page while
maintaining readability and print quality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Change buildcache from mode=max to mode=min to reduce cache layer size
and fix "413 Request Entity Too Large" error when pushing to Gitea registry.
mode=min only caches layers likely to be reused, significantly reducing
the cache size while maintaining build performance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Recharts dependency for professional chart rendering
- Create RechartsPortfolioPieChart component with built-in label collision detection
- Replace StaticPortfolioPieChart usage in CheckoutSuccess page
- Remove old StaticPortfolioPieChart component with problematic label overlap
- Fixes jumbled/overlapping labels on portfolio distribution chart
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Backend:
- Add getWrenPortfolios() to wrenClient.js with detailed logging including certification status
- Modify checkout endpoint to fetch and include portfolio data in order response
- Add stripeSessionId to order response for Payment ID display
Frontend:
- Create new StaticPortfolioPieChart component for printable receipt
- Add portfolio distribution visualization to CheckoutSuccess page
- Update receipt to show Payment ID (Stripe) and Offsetting Order ID (Wren)
- Implement responsive grid layout for order IDs
- Add print-friendly styling with Tailwind print classes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed request parameters to match Wren API docs:
- 'portfolio' → 'portfolioId' (correct parameter name)
- 'dry_run' → 'dryRun' (camelCase as per API)
- Removed 'currency' and 'amount_charged' (API calculates amount)
- Removed 'source' object (not in API spec)
- Added 'note' with customer email for record keeping
API will now calculate and return the amount charged based on tons and portfolio.
Reference: https://www.wren.co/api/offset-orders documentation
Root causes fixed:
1. State only initialized on mount, not on navigation - Added useEffect to restore offset state when navigating to calculator page
2. CheckoutCancel auto-redirected too quickly (100ms) - Removed auto-redirect, user manually clicks Try Again
3. No restoration logic existed - New useEffect watches currentPage and savedState, restores when showOffsetOrder=true
Changes:
- App.tsx: Added restoration useEffect that triggers when currentPage='calculator' and savedState has offset state
- App.tsx: Added console logs for debugging state restoration
- CheckoutCancel.tsx: Removed auto-redirect useEffect and useEffect import
- CheckoutSuccess.tsx: Already correctly clears state on successful payment (verified)
User flow now:
1. Fill calculator → proceeds to offset order (state saved)
2. Click Proceed to Payment → Stripe checkout
3. Click browser back → lands on cancel page
4. Click Try Again → navigates to calculator
5. App detects savedState.showOffsetOrder=true → restores offset order screen with preserved data
- Added detailed request/response logging for createWrenOffsetOrder
- Added detailed request/response logging for getWrenOffsetOrder
- Logs include timestamp, duration, status, parameters, and full responses
- API token is masked for security (shows first 8 and last 4 chars)
- Logs will appear in Docker container logs with [WREN API SERVER] prefix
- Makes it easier to track and debug Wren API calls from webhooks
- Added payment_intent_data.receipt_email to checkout session
- Stripe will now automatically send receipt emails to customers after successful payment
- Extended CalculatorState interface to include offset order fields (showOffsetOrder, offsetTons, monetaryAmount, calculatorTypeUsed)
- App.tsx now saves offset state to localStorage when user proceeds to offset order
- App.tsx restores offset state from localStorage on mount
- App.tsx clears offset state when user navigates back from offset order
- Fixes issue where browser back button from Stripe returns to calculator input instead of offset order screen
Updated CheckoutCancel component to automatically redirect to the calculator
page after 100ms when the component mounts. This ensures:
- Users are immediately taken back to the calculator with their saved state
- No need to manually click "Try Again" button
- Calculator state is preserved from localStorage
- URL properly updates to /calculator via existing handleNavigate logic
User experience:
1. User cancels Stripe checkout
2. Stripe redirects to /checkout/cancel
3. CheckoutCancel component mounts and immediately redirects to calculator
4. Calculator displays with all previously entered values restored
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed receipt page logo from /puffin-logo.svg to /puffinOffset.webp
to match the logo used on the homepage for consistent branding.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Payment Methods Added:
- ACH bank transfers (us_bank_account) for US customers
- Stripe Link for saved payment methods including bank accounts
- Kept credit/debit card payments
Business Payment Features:
- customer_creation: 'always' - Creates Stripe customer for each payment
- billing_address_collection: 'required' - Collects full billing address
- phone_number_collection: Enabled for contact information
- tax_id_collection: Enabled for business tax ID (VAT, EIN, etc.)
Benefits:
- Businesses can now enter their tax ID for invoicing
- Customers can pay via bank transfer (ACH) as well as cards
- Full billing information collected for business records
- Stripe Link provides one-click checkout for returning customers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Changed line item name from "Processing Fee (5%)" to "Processing Fee (3%)"
- This updates what customers see on the Stripe checkout page
- Backend constant was already updated to 0.03 in previous commit
- Frontend receipt page was already showing 3%
- All processing fee displays now consistently show 3%
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Updated TripCalculator custom amount field to use formatNumber helper
- Updated MobileCalculator custom amount field to use formatNumber helper
- Changed input type from "number" to "text" to display formatted values
- Strip commas when converting to numbers for calculations
- Custom amount inputs now display thousand separators for better readability
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added detailed request/response logging for getPortfolios()
- Added detailed request/response logging for createOffsetOrder()
- Logs include timestamps, durations, request parameters, and response details
- Visual indicators: 🔵 for info, ✅ for success, ❌ for errors, ⚠️ for warnings
- Helps track API calls and debug issues in production console logs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
## Changes Made:
### Backend (server/routes/checkout.js):
- Changed PROCESSING_FEE_PERCENT from 0.05 to 0.03
- Now calculates 3% fee instead of 5%
### Frontend (src/pages/CheckoutSuccess.tsx):
- Updated display text from "Processing Fee (5%)" to "Processing Fee (3%)"
- Receipt now shows correct percentage
## Impact:
- All new orders will have 3% processing fee
- Calculation, display, and Stripe charges all consistent at 3%
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
## Issues Fixed:
1. **Better Icon**: Replaced bell icon with leaf/plant icon for carbon offset
- More appropriate visual representation
2. **Currency Formatting**: Added thousand separators to all amounts
- Added formatCurrency() helper function
- Total now shows "$13,414.12" instead of "$13414.12"
- Applied to base amount, processing fee, and total
3. **Print Functionality**: Fixed empty page printing
- Removed "no-print" class from outer wrapper
- Only interactive elements (buttons) hidden when printing
- Receipt content now prints properly
4. **Status Display**: Shows "Confirmed" immediately after payment
- Uses Stripe session.paymentStatus for just-completed payments
- Falls back to order.status for older orders
- Prevents showing "Processing" when payment is already complete
## Technical Changes:
- CheckoutSuccess.tsx (line 27-32): Added formatCurrency helper
- CheckoutSuccess.tsx (line 120-121): Use Stripe payment status
- CheckoutSuccess.tsx (line 145): Removed no-print from wrapper
- CheckoutSuccess.tsx (line 208-210): Changed to leaf SVG icon
- CheckoutSuccess.tsx (lines 228, 234, 241): Use formatCurrency()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
## Problem
- Calculator displayed rounded-up price per ton (e.g., $238/ton)
- Stripe checkout used exact Wren API price (e.g., $237.20/ton)
- This caused confusion when checkout amount differed from calculator
Example with 20,000 liters (53.9 tons):
- Calculator showed: 53.9 × $238 = $12,819
- Stripe charged: 53.9 × $237.20 = $12,775.35
## Solution
Modified both OffsetOrder.tsx and MobileOffsetOrder.tsx to:
- Keep displaying rounded-up price (Math.ceil) to users
- Pass roundedPricePerTon to Stripe checkout session
- Ensures calculator and Stripe always match exactly
## Changes
- OffsetOrder.tsx (line 170): Pass roundedPricePerTon to createCheckoutSession
- MobileOffsetOrder.tsx (lines 207-218): Add rounding to price display and calculations
Now the price users see is exactly what they pay.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
## Changes Made:
### 1. Remove Carbon Impact Comparisons from Calculator Pages
- Removed from OffsetOrder.tsx (lines 532-542)
- Removed from MobileOffsetOrder.tsx (lines 429-432)
- Keep comparisons ONLY on CheckoutSuccess receipt page per user request
### 2. Comprehensive CheckoutSuccess Page Redesign
- Add Puffin logo prominently at top of receipt
- Implement status mapping: paid/fulfilled → "Confirmed", pending → "Processing"
- Add comprehensive print CSS (@media print rules)
- Hide interactive elements (buttons) when printing
- Optimize layout and spacing for printed receipt
- Professional receipt aesthetics with enhanced design:
* Beautiful gradient header with logo
* Highlighted carbon offset display with icon
* Enhanced pricing breakdown section
* Better typography and spacing throughout
* Professional metadata section with date
* Improved button styling with gradients and hover effects
* Email confirmation notice with icon
* Footer with contact information
### Benefits:
- Cleaner calculator UX (comparisons only on success)
- Professional printable receipt
- Clear "Confirmed" status (not "PENDING")
- Beautiful modern design that matches brand
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Issue: Calculator inputs were not being saved before navigating to checkout
because navigation happened synchronously before useEffect could run.
Solution: Call saveState() directly in handleCalculate before navigation
in both TripCalculator and MobileCalculator components.
This ensures calculator inputs (fuel amount, distance, etc.) are properly
preserved when users click "Calculate Impact" and then cancel checkout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Features:
- Add useCalculatorState hook with localStorage persistence and 1-hour expiry
- State persists through page reloads and Stripe checkout redirects
- Automatically clears state on successful payment (paid/fulfilled status)
Navigation fixes:
- Fix white page issues on checkout success/cancel pages
- Replace <a> links with button handlers for proper state-based routing
- Pass navigation handlers from App.tsx to checkout pages
State persistence integration:
- TripCalculator: Save/restore calculator inputs (fuel, distance, custom)
- MobileCalculator: Full state persistence for mobile app route
- OffsetOrder: Persist offset percentage and portfolio selection
- MobileOffsetOrder: Persist offset percentage for mobile flow
Carbon impact comparisons:
- Add varied carbon impact comparisons with random selection
- Display 3 comparisons in preview mode, 5 in success mode
- Categories: cars, flights, trees, streaming, homes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated PROCESSING_FEE_PERCENT from 0.03 to 0.05 and updated the
line item description in Stripe checkout from "Processing Fee (3%)"
to "Processing Fee (5%)".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changes:
- Changed portfolio description from "Portfolio {ID}" to "Puffin Portfolio"
- Round pricePerTon UP using Math.ceil() instead of exact decimals
- Round total cost UP using Math.ceil()
Example: $237.19 per ton → $238 per ton
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The backend was using a hardcoded PORTFOLIO_PRICING map that only had prices
for portfolios 1, 2, 3 (defaulting to $18/ton for others). When Wren API
returns Portfolio ID 37 at $237/ton, the backend was calculating with $18/ton
instead, causing massive pricing discrepancies.
Changes:
- Frontend now passes pricePerTon (from Wren API) in checkout request
- Backend uses the received pricePerTon instead of lookup table
- Removed dependency on hardcoded PORTFOLIO_PRICING map
- Added validation for pricePerTon parameter
Example fix:
- Before: 0.03 tons × $18/ton = $0.54 (wrong!)
- After: 0.03 tons × $237/ton = $7.11 (correct!)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed backend validation from hardcoded [1, 2, 3] to accept any positive
integer portfolio ID. The Wren API returns portfolios with their real IDs
(not limited to 1-3), so the backend needs to accept dynamic portfolio IDs.
- Changed validation to check for positive integers instead of [1, 2, 3]
- Added logging to show which portfolio ID is being processed
- Pricing calculation already has fallback to $18/ton for unknown IDs
This fixes the "Invalid portfolio ID" error when trying to checkout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Bumped service worker cache version to v2 to force cache invalidation
- Added activate event to explicitly clear old caches
- Enhanced service worker logging with update checking
- Changed API fallback URL from localhost:3001 to https://puffinoffset.com/api
This fixes the issue where new code deployments were not being used due to
service worker caching the old JavaScript with the timing bug.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
ROOT CAUSE (Zen Debug Investigation):
The API_BASE_URL constant was being evaluated when the module loaded,
BEFORE window.env was populated by env-config.js. This caused the app
to permanently use the fallback localhost:3001 instead of reading the
production URL from the .env file.
TIMING ISSUE:
1. Browser loads index.html
2. env-config.js loads (creates window.env)
3. main.tsx loads as ES module (DEFERRED)
4. Module imports checkoutClient → const API_BASE_URL executes
5. window.env NOT READY YET → falls back to localhost:3001
6. API_BASE_URL locked to wrong value forever
THE FIX:
Changed from module-level constants to request-time lazy evaluation.
Now getApiBaseUrl() is called WHEN THE API REQUEST HAPPENS, not when
the module loads. At that point window.env is guaranteed to exist.
FILES CHANGED:
- src/api/checkoutClient.ts:
* Removed constant API_BASE_URL
* All 3 functions now call getApiBaseUrl() at request time
* Added logging to show which URL is being used
- src/api/aisClient.ts:
* Removed constant API_KEY
* Added getApiKey() function with lazy evaluation
* Same pattern for consistency and future-proofing
This fixes: "FetchEvent for http://localhost:3001 resulted in network error"
Now uses: https://puffinoffset.com/api (from .env file)
🔒 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>