diff --git a/frontend/package.json b/frontend/package.json index 5c04762..e698548 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,6 +64,7 @@ "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", "cropperjs": "^1.6.2", + "heic2any": "^0.0.4", "maplibre-gl": "^5.24.0" } } diff --git a/frontend/src/lib/components/admin/ImageUpload.svelte b/frontend/src/lib/components/admin/ImageUpload.svelte index 53f10cc..589419f 100644 --- a/frontend/src/lib/components/admin/ImageUpload.svelte +++ b/frontend/src/lib/components/admin/ImageUpload.svelte @@ -4,6 +4,7 @@ import FileDropZone from '$lib/components/ui/file-drop-zone.svelte'; import { authStore } from '$lib/stores/auth.svelte'; import * as AlertDialog from '$lib/components/ui/alert-dialog'; + import heic2any from 'heic2any'; // =============== Image Upload =============== let uploading = $state(false); @@ -28,9 +29,60 @@ let hasOversizedFiles = $derived(uploadFiles.some(isFileTooBig)); - function handleFilesDropped(files: File[]) { - uploadFiles = files; - generatePreviews(files); + function isHeicFile(file: File): boolean { + const name = file.name.toLowerCase(); + return name.endsWith('.heic') || name.endsWith('.heif'); + } + + /** + * Check if the browser can natively decode a HEIC file. + * Safari (macOS/iOS) and Chrome-on-Android have native HEIC support, + * which preserves HDR metadata and color profiles better than any conversion. + */ + function supportsNativeHeicDecode(file: File): Promise { + return new Promise((resolve) => { + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { URL.revokeObjectURL(url); resolve(true); }; + img.onerror = () => { URL.revokeObjectURL(url); resolve(false); }; + img.src = url; + }); + } + + async function convertHeicToPng(file: File): Promise { + const result = await heic2any({ blob: file, toType: 'image/png' }); + const blob = Array.isArray(result) ? result[0] : result; + const pngName = file.name.replace(/\.(heic|heif)$/i, '.png'); + return new File([blob], pngName, { type: 'image/png' }); + } + + async function handleFilesDropped(files: File[]) { + const converted = await Promise.all( + files.map(async (file) => { + if (!isHeicFile(file)) return file; + + // Try native browser HEIC decode first (Safari, Chrome w/ HEVC) + // This preserves HDR metadata & color profiles at full quality + try { + const nativeOk = await supportsNativeHeicDecode(file); + if (nativeOk) return file; + } catch { + // Fall through to heic2any + } + + // Fallback: convert to PNG via heic2any (libheif WASM) + // PNG is lossless, libheif handles ICC profiles correctly, + // and the downstream canvas pipeline is 8-bit anyway. + try { + return await convertHeicToPng(file); + } catch (e) { + console.error('HEIC conversion failed for', file.name, e); + return file; + } + }) + ); + uploadFiles = converted; + generatePreviews(converted); } /** Generate scaled previews for each file */ diff --git a/obsidian/.obsidian/workspace.json b/obsidian/.obsidian/workspace.json index fe98355..d730413 100644 --- a/obsidian/.obsidian/workspace.json +++ b/obsidian/.obsidian/workspace.json @@ -13,12 +13,12 @@ "state": { "type": "markdown", "state": { - "file": "Crussell/Technical Manual.md", + "file": "Crussell/Future Work - Gap Backlog.md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "Technical Manual" + "title": "Future Work - Gap Backlog" } } ] @@ -78,8 +78,7 @@ } ], "direction": "horizontal", - "width": 300, - "collapsed": true + "width": 300 }, "right": { "id": "2750d7726f904ef3", @@ -172,10 +171,10 @@ }, "active": "6776d739ee18449c", "lastOpenFiles": [ + "Crussell/Technical Manual.md", "Crussell/Future Work - Gap Backlog.md", "Crussell/Loyalty & Discount System Reference.md", "Crussell/Overview.md", - "Crussell/Technical Manual.md", "Crussell/User Manual.md", "Crussell/Admin Manual.md", "Crussell/Test Implementation Plan.md", diff --git a/obsidian/Crussell/Future Work - Gap Backlog.md b/obsidian/Crussell/Future Work - Gap Backlog.md index 4050e30..d6adecc 100644 --- a/obsidian/Crussell/Future Work - Gap Backlog.md +++ b/obsidian/Crussell/Future Work - Gap Backlog.md @@ -38,23 +38,23 @@ These are blockers: missing functionality that prevents daily operations, legal These improve the experience or add features, but the business can operate without them. -| # | Gap | Effort | Area | Notes | -|---|---|---|---|---| -| 12 | **One-off custom services** | M (1-2d) | Full-stack | Admin can't create single-use services. Every custom job (bridal party, special request) must be added to the permanent catalog. | -| 13 | **One-off exceptional hours** | M (1d) | Full-stack | Single-day overrides (dentist appointment, afternoon off) require creating a full exceptional group. Time blockers handle unavailable periods; one-off *open* hours (e.g., "open Sunday 2pm-5pm") still need simplification. | -| 14 | **Referral system UI** | M (1-2d) | Full-stack | Backend complete — registration validates codes, relationships recorded. Users can't see their referral code or track uses. Admin can't manage referral campaigns. | -| 15 | **Analytics endpoints** | M (1-2d) | Backend | `handlers/admin/analytics.go` is 1 line. `get_monthly_business_summary()`, `get_sales_totals()` SQL functions exist but not wired. | -| 16 | **API documentation** | M (1-2d) | Backend | No OpenAPI/Swagger spec. No generated docs. New developers must read code. | -| 17 | **Per-user rate limiting** | M (1d) | Backend | Rate limiter is IP-based. Authenticated users could abuse from multiple IPs. Should track by user ID + IP. Pure Go — no Redis needed for single-instance. | -| 18 | **Begin button (Today page)** | S (2-3h) | Full-stack | Manual start for early arrivals. Currently auto-inferred only. Gray out if >3 hours away. | -| 19 | **Booking conflict detection for users** | S (2-3h) | Backend | Users can theoretically double-book themselves in two tabs. Reservation system helps but doesn't fully prevent. | -| 20 | **Service category/tag management** | M (1-2d) | Full-stack | Services have no category field. Admin scrolls through a flat list. No way to group (manicure vs pedicure vs nail art). | -| 21 | **No-show tracking dashboard** | S (2-3h) | Frontend | `forgiven_no_shows` table exists but no UI. Admin can't see which users have accumulated no-shows. | -| 22 | **Dark mode** | M (1-2d) | Frontend | SvelteKit + Tailwind supports it. No toggle or `prefers-color-scheme` support. | -| 23 | **PWA support** | L (3-5d) | Frontend | No service worker, no manifest.json, no offline support. Customers can't "install" the app. | -| 24 | **Recurring bookings** | L (3-5d) | Full-stack | Customers can't book the same slot weekly/monthly. Would need `recurring_bookings` table + background job. | -| 25 | **Idempotency key cleanup** | S (1h) | Backend | `idempotency_key` columns added to `bookings`, `payments`, `till_sales` with unique constraints. No retention policy — keys accumulate indefinitely. | -| 26 | **Gift card self-service portal** | M (1d) | Frontend | Users can see gift card balance on Account page but can't independently redeem to balance without admin. | +| # | Gap | Effort | Area | Notes | +| --- | ---------------------------------------- | -------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 12 | **One-off custom services** | M (1-2d) | Full-stack | Admin can't create single-use services. Every custom job (bridal party, special request) must be added to the permanent catalog. | +| 13 | **One-off exceptional hours** | M (1d) | Full-stack | Single-day overrides (dentist appointment, afternoon off) require creating a full exceptional group. Time blockers handle unavailable periods; one-off *open* hours (e.g., "open Sunday 2pm-5pm") still need simplification. | +| 14 | **Referral system UI** | M (1-2d) | Full-stack | Backend complete — registration validates codes, relationships recorded. Users can't see their referral code or track uses. Admin can't manage referral campaigns. | +| 15 | **Analytics endpoints** | M (1-2d) | Backend | `handlers/admin/analytics.go` is 1 line. `get_monthly_business_summary()`, `get_sales_totals()` SQL functions exist but not wired. | +| 16 | **API documentation** | M (1-2d) | Backend | No OpenAPI/Swagger spec. No generated docs. New developers must read code. | +| 17 | **Per-user rate limiting** | M (1d) | Backend | Rate limiter is IP-based. Authenticated users could abuse from multiple IPs. Should track by user ID + IP. Pure Go — no Redis needed for single-instance. | +| 18 | **Begin button (Today page)** | S (2-3h) | Full-stack | Manual start for early arrivals. Currently auto-inferred only. Gray out if >3 hours away. | +| 19 | **Booking conflict detection for users** | S (2-3h) | Backend | Users can theoretically double-book themselves in two tabs. Reservation system helps but doesn't fully prevent. | +| 20 | **Service category/tag management** | M (1-2d) | Full-stack | Services have no category field. Admin scrolls through a flat list. No way to group (manicure vs pedicure vs nail art). | +| 21 | **No-show tracking dashboard** | S (2-3h) | Frontend | `forgiven_no_shows` table exists but no UI. Admin can't see which users have accumulated no-shows. | +| 22 | **Dark mode** | M (1-2d) | Frontend | SvelteKit + Tailwind supports it. No toggle or `prefers-color-scheme` support. | +| 23 | **PWA support** | L (3-5d) | Frontend | No service worker, no manifest.json, no offline support. Customers can't "install" the app. | +| 24 | **Recurring bookings** | L (3-5d) | Full-stack | Customers can't book the same slot weekly/monthly. Would need `recurring_bookings` table + background job. | +| 25 | **Idempotency key cleanup** | S (1h) | Backend | `idempotency_key` columns added to `bookings`, `payments`, `till_sales` with unique constraints. No retention policy — keys accumulate indefinitely. | +| 26 | **Gift card self-service portal** | M (1d) | Frontend | Users can see gift card balance on Account page but can't independently redeem to balance without admin. | ### External Stretch (Requires Third-Party Access) @@ -78,31 +78,3 @@ E10 Sentry → error tracking, 5xx alerting ``` All local (MVP + Stretch) items have zero external dependencies. - ---- - -## Notes on Removed Items - -The following items were completed and removed from this document: - -- Delete account endpoint, GDPR data export, delete_guest_user SQL function, GDPR anonymization -- Walk-in guest booking, walk-in slot blocking, reservation transitions -- ApprovalModal decline/cancel, admin notification panel, admin schedule page -- BookingFlow welcome step, shared format utilities, patch_test_duration_hours -- created_by_name on bookings, admin login redirect, customer relationship view -- Admin UI for edit requests, booking rescheduling (user + admin) -- Auto lunch protection, health check endpoint, graceful shutdown -- HSTS + Referrer-Policy headers, console.log cleanup, alert prototypes -- Idempotency keys for bookings, loyalty stamp redemption + discount system -- Test DB optimization, image optimization, image deletion fix -- User notification preferences UI, payment integration (Square), deposit payments -- Tips UI, gift card system (backend + admin UI), referral code registration -- CharCounter, loginInProgress rate limiting, profile picture limits, formatDateISO -- Portfolio upload limits, JWT revocation, MapLibre GL, NavBar enhancements -- TodayCalendar improvements, Svelte 5 $derived, SvelteDate, $app/paths resolve -- Vite WASM support, portfolio multi-format images, loyalty stamp redesign -- Admin role restrictions, account restrictions, BookingsByCreatedRange, enriched edit requests -- Email check endpoint, PhoneInput, business settings API, gift card audit log -- Gift card expiry, idle account cleanup, inventory cards, expired balance recovery -- Till sale idempotency, VAT on till sales, test infrastructure upgrades -- Financial data retention, gift card transaction audit log