feat(pwa): Add PWA support for installable kiosk app
- Added manifest.json for 'Add to Home Screen' installation - Added service worker for offline caching - Added app icons (192px, 512px) - Updated index.html with mobile meta tags - Created spec for Escort Handoff Mode (009)
This commit is contained in:
parent
b35c32279c
commit
f51a1072fe
6 changed files with 263 additions and 150 deletions
|
|
@ -1,12 +1,35 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CA Grow Ops Manager</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#10b981" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Visitor Kiosk" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
||||
<title>777 Wolfpack - Visitor Kiosk</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
// Register service worker for PWA
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('SW registered:', registration.scope);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('SW registration failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
frontend/public/icons/icon-192.png
Normal file
BIN
frontend/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 KiB |
BIN
frontend/public/icons/icon-512.png
Normal file
BIN
frontend/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 KiB |
|
|
@ -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": "Returning Visitor",
|
||||
"short_name": "Return",
|
||||
"url": "/kiosk?mode=returning",
|
||||
"icons": [
|
||||
{
|
||||
"name": "Quick Log",
|
||||
"url": "/touch-points",
|
||||
"description": "Log a touch point"
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,35 +1,27 @@
|
|||
/// <reference lib="webworker" />
|
||||
|
||||
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));
|
||||
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);
|
||||
});
|
||||
|
||||
async function networkFirstWithCache(request: Request): Promise<Response> {
|
||||
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) {
|
||||
})
|
||||
.catch(() => {
|
||||
// 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<Response> {
|
||||
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
|
||||
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 { };
|
||||
|
|
|
|||
182
specs/009_escort_handoff_mode.md
Normal file
182
specs/009_escort_handoff_mode.md
Normal file
|
|
@ -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 |
|
||||
Loading…
Add table
Reference in a new issue