diff --git a/frontend/index.html b/frontend/index.html index 8fde53e..a7da640 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,12 +1,35 @@ - - - - CA Grow Ops Manager - - -
- - - + + + + + + + + + + + 777 Wolfpack - Visitor Kiosk + + + +
+ + + + + \ No newline at end of file diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 0000000..8fe3024 Binary files /dev/null and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000..8fe3024 Binary files /dev/null and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index a3139b6..6f82e72 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,21 +1,21 @@ { - "name": "777 Wolfpack - Grow Ops Manager", - "short_name": "Wolfpack", - "description": "Cannabis cultivation management system", - "start_url": "/", + "name": "777 Wolfpack Visitor Kiosk", + "short_name": "Visitor Kiosk", + "description": "Digital visitor check-in kiosk for 777 Wolfpack facility", + "start_url": "/kiosk", "display": "standalone", + "orientation": "portrait", "background_color": "#0f172a", "theme_color": "#10b981", - "orientation": "portrait-primary", "icons": [ { - "src": "/icon-192.png", + "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icon-512.png", + "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" @@ -27,19 +27,26 @@ ], "shortcuts": [ { - "name": "Tasks", - "url": "/tasks", - "description": "View and manage tasks" + "name": "New Visitor", + "short_name": "New", + "url": "/kiosk?mode=new", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192" + } + ] }, { - "name": "Batches", - "url": "/batches", - "description": "View all batches" - }, - { - "name": "Quick Log", - "url": "/touch-points", - "description": "Log a touch point" + "name": "Returning Visitor", + "short_name": "Return", + "url": "/kiosk?mode=returning", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192" + } + ] } ] } \ No newline at end of file diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 3ef049f..19b1c6b 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,35 +1,27 @@ -/// - -const CACHE_NAME = 'wolfpack-v1'; +const CACHE_NAME = 'wolfpack-kiosk-v1'; const STATIC_ASSETS = [ - '/', - '/index.html', + '/kiosk', + '/badges', '/manifest.json' ]; -const API_CACHE = 'wolfpack-api-v1'; -const API_CACHE_MAX_AGE = 5 * 60 * 1000; // 5 minutes - -declare const self: ServiceWorkerGlobalScope; - // Install event - cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { - console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS); }) ); self.skipWaiting(); }); -// Activate event - clean old caches +// Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames - .filter((name) => name !== CACHE_NAME && name !== API_CACHE) + .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) @@ -37,118 +29,27 @@ self.addEventListener('activate', (event) => { self.clients.claim(); }); -// Fetch event - network-first for API, cache-first for assets +// Fetch event - network first, falling back to cache self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); - // Skip non-GET requests - if (request.method !== 'GET') return; + if (event.request.method !== 'GET') return; - // API requests - network first, fallback to cache - if (url.pathname.startsWith('/api')) { - event.respondWith(networkFirstWithCache(request)); - return; - } + // Skip API requests (always fetch from network) + if (event.request.url.includes('/api/')) return; - // Static assets - cache first, fallback to network - event.respondWith(cacheFirstWithNetwork(request)); -}); - -async function networkFirstWithCache(request: Request): Promise { - try { - const response = await fetch(request); - - // Cache successful GET responses - if (response.ok) { - const cache = await caches.open(API_CACHE); - cache.put(request, response.clone()); - } - - return response; - } catch (error) { - // Network failed, try cache - const cached = await caches.match(request); - if (cached) { - console.log('[SW] Serving from cache:', request.url); - return cached; - } - - // Return offline response for API - return new Response( - JSON.stringify({ error: 'You are offline', offline: true }), - { - status: 503, - headers: { 'Content-Type': 'application/json' } - } - ); - } -} - -async function cacheFirstWithNetwork(request: Request): Promise { - const cached = await caches.match(request); - if (cached) { - return cached; - } - - try { - const response = await fetch(request); - - // Cache successful responses - if (response.ok) { - const cache = await caches.open(CACHE_NAME); - cache.put(request, response.clone()); - } - - return response; - } catch (error) { - // Return offline page for navigation requests - if (request.mode === 'navigate') { - const offlinePage = await caches.match('/'); - if (offlinePage) return offlinePage; - } - - throw error; - } -} - -// Background sync for offline actions -self.addEventListener('sync', (event) => { - if (event.tag === 'sync-touchpoints') { - event.waitUntil(syncTouchpoints()); - } -}); - -async function syncTouchpoints() { - // This would sync any queued offline actions - console.log('[SW] Syncing offline actions...'); -} - -// Push notifications -self.addEventListener('push', (event) => { - if (!event.data) return; - - const data = event.data.json(); - - event.waitUntil( - self.registration.showNotification(data.title || '777 Wolfpack', { - body: data.body, - icon: '/icon-192.png', - badge: '/badge-72.png', - tag: data.tag || 'notification', - data: data.url - }) + event.respondWith( + fetch(event.request) + .then((response) => { + // Clone and cache the response + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseClone); + }); + return response; + }) + .catch(() => { + // Network failed, try cache + return caches.match(event.request); + }) ); }); - -self.addEventListener('notificationclick', (event) => { - event.notification.close(); - - if (event.notification.data) { - event.waitUntil( - self.clients.openWindow(event.notification.data) - ); - } -}); - -export { }; diff --git a/specs/009_escort_handoff_mode.md b/specs/009_escort_handoff_mode.md new file mode 100644 index 0000000..5533f76 --- /dev/null +++ b/specs/009_escort_handoff_mode.md @@ -0,0 +1,182 @@ +# Visitor Kiosk - Escort Handoff Mode Specification + +## Overview + +This document specifies the "Escort Handoff Mode" for the mobile visitor kiosk. In this mode, a staff member hands their tablet/phone to the visitor for registration, but the digital badge remains on the staff device rather than transferring to the visitor's personal phone. + +## Use Case + +In controlled facilities, staff may prefer to: + +1. Hand their mobile device to a visitor for self-registration +2. Keep the visitor badge displayed on the staff device +3. Maintain "escort mode" where staff carries the badge throughout the visit + +## User Flow + +### 1. Staff Initiates Handoff + +``` +Staff Device (Kiosk Mode) +├── Staff taps "Register Visitor" +├── Device locks into "Handoff Mode" +│ └── Shows: "Please hand device to visitor" +└── Optional: Staff enters their ID as escort +``` + +### 2. Visitor Self-Registration (On Staff Device) + +``` +Visitor receives device +├── Fills in: +│ ├── Name (required) +│ ├── Company +│ ├── Purpose (required) +│ └── Type (Visitor, Contractor, etc.) +├── Takes selfie using front camera +├── Accepts NDA (checkbox) +└── Taps "Complete Registration" +``` + +### 3. Badge Display (Stays on Staff Device) + +``` +After successful check-in: +├── Badge displays on staff's device +├── Shows: "Return device to staff" overlay +├── QR code for security scan +├── Staff name shown as "Escort" +└── NO auto-redirect to visitor's phone +``` + +### Key Differences from Standard Mode + +| Feature | Standard Kiosk | Escort Handoff | +|---------|---------------|----------------| +| Badge Location | Visitor's phone (via QR scan) | Staff's device | +| Escort Assignment | Optional | Automatic (logged-in staff) | +| Device Return | Not applicable | Required step | +| Badge Portability | On visitor's device | Carried by escort | + +## Technical Implementation + +### 1. Mode Detection + +```typescript +type KioskMode = + | 'standard' // Fixed tablet kiosk + | 'escort-handoff' // Mobile escort mode + | 'self-service'; // Visitor's own device + +interface HandoffSession { + escortId: string; // Staff user ID + escortName: string; // For display + startedAt: Date; + deviceId?: string; // For audit +} +``` + +### 2. API Changes + +```typescript +// POST /api/visitors/:id/check-in +{ + escortId: string; // Required in handoff mode + handoffMode: boolean; // Flag for escort mode + ndaAccepted: boolean; + photoUrl?: string; +} + +// Response includes escortRequired: true +``` + +### 3. Badge Page Updates + +When in escort-handoff mode, the badge page should: + +- Display "Escorted by: [Staff Name]" +- Show a prominent "ESCORT REQUIRED" badge +- Add "Return Device to Staff" button after registration +- Disable "Send to My Phone" option + +### 4. Staff Authentication + +For staff to initiate handoff mode, they should: + +- Be logged into the app on their device +- Have appropriate permissions (e.g., `can_escort_visitors`) +- Their user ID auto-populates as escort + +## UI Mockup Concepts + +### Handoff Initiation Screen (Staff View) + +``` +┌─────────────────────────────────────┐ +│ 777 Wolfpack Visitor Kiosk │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ 📱 Escort Mode Active │ │ +│ │ │ │ +│ │ Hand this device to the │ │ +│ │ visitor for registration │ │ +│ │ │ │ +│ │ Escort: John Smith │ │ +│ │ Badge will stay on device │ │ +│ │ │ │ +│ │ [Cancel Handoff] │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────┘ +``` + +### Post-Registration Screen (Visitor View on Staff Device) + +``` +┌─────────────────────────────────────┐ +│ ✓ Registration Complete │ +│ │ +│ [Visitor Photo] │ +│ Jane Doe │ +│ ACME Corp │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ 🔒 ESCORT REQUIRED │ │ +│ │ Badge: V-20251211-042 │ │ +│ │ Escort: John Smith │ │ +│ └─────────────────────────────┘ │ +│ │ +│ Please return this device to │ +│ your escort to complete check-in │ +│ │ +│ [ Return to Escort View ] │ +│ │ +└─────────────────────────────────────┘ +``` + +## Security Considerations + +1. **Session Timeout**: Handoff mode should timeout after 5 minutes of inactivity +2. **Escort Validation**: Staff must be logged in with valid credentials +3. **Audit Trail**: Log device handoffs for compliance +4. **Data Isolation**: Visitor cannot access staff's other data on device +5. **Photo Storage**: Photos stored securely, not accessible via device gallery + +## Future Enhancements + +1. **Geofencing**: Alert if escorted visitor moves outside allowed zones +2. **NFC Badge Tap**: Staff can tap NFC to transfer badge to facility kiosk for checkout +3. **Multi-Visitor Escort**: Staff manages multiple visitors on one device +4. **Offline Mode**: Queue registrations when network is unavailable + +## Implementation Priority + +| Phase | Feature | Effort | +|-------|---------|--------| +| P1 | Basic handoff mode toggle | Low | +| P1 | Escort auto-assignment | Low | +| P1 | Badge stays on device | Medium | +| P2 | "Return device" confirmation | Low | +| P2 | Session timeout | Low | +| P3 | Staff authentication | Medium | +| P3 | Geofencing | High |