# Canvassing Session System ## Overview The canvassing system provides a complete door-to-door organizing workflow with GPS tracking, walking route optimization, visit recording, and progress tracking. It enables volunteers to efficiently canvass assigned territories using mobile devices with real-time location updates. **Key Capabilities:** - **Session Lifecycle**: ACTIVE → COMPLETED → ABANDONED (auto-close after 12h) - **Walking Route Algorithm**: Nearest-neighbor optimization from volunteer GPS position - **Visit Recording**: 7 outcome types with support level updates - **GPS Integration**: Live tracking via TrackingSession (1:1 relationship) - **Rate Limiting**: 30 visits/min per IP to prevent abuse - **Progress Tracking**: Cut completion percentage auto-calculated - **Admin Oversight**: Active sessions dashboard, activity feed, leaderboard - **Volunteer Portal**: Full-screen map with bottom-sheet visit recording **Use Cases:** - Door-to-door canvassing for electoral campaigns - Voter ID (identifying supporter levels) - GOTV (Get Out The Vote) efforts - Sign placement tracking - Petition signature collection - Issue surveys - Volunteer coordination ## Architecture ```mermaid graph TD A[Volunteer] -->|Start Session| B[VolunteerMapPage] B -->|POST /api/map/canvass/sessions| C[Canvass Service] C -->|Create| D[(CanvassSession)] C -->|Start GPS| E[Tracking Service] E -->|Create| F[(TrackingSession)] B -->|Load Addresses| C C -->|Filter by Cut| G[Spatial Utils] G -->|Point-in-Polygon| H[(Location Model)] B -->|Calculate Route| I[Walking Route Service] I -->|Nearest Neighbor| J[Haversine Distance] J -->|Return Route| B K[Volunteer GPS] -->|Submit Points| E E -->|Save| L[(TrackPoint)] E -->|Calculate Distance| J B -->|Record Visit| C C -->|Create| M[(CanvassVisit)] C -->|Update Address| N[(Address Model)] C -->|Update Progress| D O[Admin] -->|View Dashboard| P[CanvassDashboardPage] P -->|GET /api/map/canvass/admin/activity| C C -->|Aggregate Stats| D C -->|Activity Feed| M D -->|1:1| F D -->|1:N| M M -->|N:1| N style D fill:#e1f5ff style F fill:#e1f5ff style M fill:#e1f5ff style N fill:#e1f5ff style H fill:#e1f5ff ``` **Flow Description:** 1. **Volunteer starts session** → Creates CanvassSession + TrackingSession, loads addresses within cut 2. **Calculate route** → Walking route service uses nearest-neighbor from volunteer GPS position 3. **GPS tracking** → Auto-submit points every 10s, calculate distance with haversine 4. **Record visit** → Create CanvassVisit with outcome, update Address support level, update session progress 5. **End session** → Mark session COMPLETED, end tracking session, calculate final stats 6. **Admin oversight** → View active sessions, activity feed, cut progress, volunteer leaderboard ## Database Models ### CanvassSession Model See [CanvassSession Model Documentation](../../database/models/canvass.md#canvasssession-model) for full schema. **Key Fields:** - `userId`: Foreign key to volunteer User - `cutId`: Foreign key to Cut (territory) - `shiftId`: Optional foreign key to Shift (if started from shift) - `status`: ACTIVE | COMPLETED | ABANDONED - `startedAt`: Session start timestamp - `endedAt`: Session end timestamp (null while active) - `totalVisits`: Count of CanvassVisit records - `completionPercentage`: Auto-calculated from cut progress **Status Lifecycle:** ``` ACTIVE (session running) ↓ (volunteer ends session) COMPLETED OR ACTIVE (session running > 12 hours) ↓ (auto-cleanup cron) ABANDONED ``` ### CanvassVisit Model See [CanvassVisit Model Documentation](../../database/models/canvass.md#canvassvisit-model) for full schema. **Key Fields:** - `sessionId`: Foreign key to CanvassSession - `userId`: Foreign key to volunteer User - `addressId`: Foreign key to Address (specific unit visited) - `outcome`: Visit result (7 types) - `supportLevel`: Updated support level (LEVEL_1-4 or null) - `signRequested`: Boolean - resident wants lawn/window sign - `notes`: Free-text canvass notes - `visitedAt`: Visit timestamp - `durationSeconds`: Time spent at door (auto-calculated) **Visit Outcome Enum:** ```typescript enum VisitOutcome { NOT_HOME // Nobody answered door REFUSED // Refused to speak MOVED // Resident moved away ALREADY_VOTED // Already voted (GOTV) SPOKE_WITH // Had conversation LEFT_LITERATURE // Left campaign material COME_BACK_LATER // Asked to return later } ``` **Related Models:** - [TrackingSession](../../database/models/canvass.md#trackingsession-model) — GPS tracking (1:1) - [Address](../../database/models/map.md#address-model) — Updated with visit data - [Cut](../../database/models/map.md#cut-model) — Territory boundary - [Shift](../../database/models/map.md#shift-model) — Optional shift assignment ## API Endpoints See [Canvass Backend Module Documentation](../../backend/modules/map/canvass.md) for full API reference. **Volunteer Endpoints:** | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | GET | `/api/map/canvass/volunteer/assignments` | Any logged-in user | Get shifts with cut assignments | | GET | `/api/map/canvass/volunteer/stats` | Any logged-in user | Get volunteer canvass statistics | | GET | `/api/map/canvass/volunteer/visits` | Any logged-in user | List own canvass visits with pagination | | POST | `/api/map/canvass/sessions` | Any logged-in user | Start new canvass session | | PATCH | `/api/map/canvass/sessions/:id` | Any logged-in user | Update session (end session) | | GET | `/api/map/canvass/sessions/:id/addresses` | Any logged-in user | Get addresses within session cut | | POST | `/api/map/canvass/sessions/:id/route` | Any logged-in user | Calculate walking route | | POST | `/api/map/canvass/visits` | Any logged-in user | Record single visit | | POST | `/api/map/canvass/visits/bulk` | Any logged-in user | Record multiple visits (batch) | | PATCH | `/api/map/canvass/volunteer/locations/:id` | Any logged-in user | Update location from canvass | **Admin Endpoints:** | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | GET | `/api/map/canvass/admin/activity` | MAP_ADMIN | Get recent canvass activity feed | | GET | `/api/map/canvass/admin/sessions` | MAP_ADMIN | List active canvass sessions | | GET | `/api/map/canvass/admin/visits` | MAP_ADMIN | List all canvass visits with filters | | GET | `/api/map/canvass/admin/progress` | MAP_ADMIN | Get cut completion progress | | GET | `/api/map/canvass/admin/leaderboard` | MAP_ADMIN | Get volunteer visit leaderboard | ## Configuration ### Environment Variables | Variable | Type | Default | Description | |----------|------|---------|-------------| | `CANVASS_SESSION_TIMEOUT_HOURS` | number | `12` | Auto-abandon active sessions after N hours | | `CANVASS_VISIT_RATE_LIMIT` | number | `30` | Max visits per minute per IP | ### Rate Limiting **Visit Recording Rate Limit:** - **Limit**: 30 visits/min per IP address - **Window**: 60 seconds - **Redis Prefix**: `rl:canvass-visit:` - **Purpose**: Prevent accidental bulk submissions from GPS auto-submit bugs ### Session Auto-Cleanup **Abandoned Session Detection:** System automatically marks sessions as ABANDONED if: - **Status**: ACTIVE - **Started**: >12 hours ago - **No Activity**: No visits in last hour **Cleanup Schedule:** - **Startup**: On API server startup (check all active sessions) - **Cron**: Every hour at :00 (setInterval) ```typescript // api/src/server.ts setInterval(async () => { await canvassService.cleanupAbandonedSessions(); }, 60 * 60 * 1000); // 1 hour ``` ## Admin Workflow ### Viewing Active Sessions **Step 1: Navigate to Canvass Dashboard** Navigate to **Map → Canvass Dashboard** in the admin sidebar. ![CanvassDashboardPage Screenshot Placeholder] **Step 2: View Active Sessions** **Active Sessions** card displays: - **Volunteer Name**: Who is canvassing - **Cut Name**: Territory being canvassed - **Start Time**: When session started - **Duration**: Time elapsed (live updating) - **Visits**: Number of visits recorded - **Status**: ACTIVE badge **Step 3: View Session Details** Click session row to view: - **Volunteer GPS Location**: Last known position on map - **Route**: Walking route polyline - **Visited Addresses**: Green markers - **Unvisited Addresses**: Blue markers - **Recent Visits**: Last 10 visits with outcomes ### Monitoring Canvass Activity **Step 1: View Activity Feed** **Recent Activity** section displays: - **Volunteer Name**: Who recorded visit - **Address**: Location visited - **Outcome**: Visit result (icon + label) - **Support Level**: Updated support level (color-coded) - **Time Ago**: "5 minutes ago" **Step 2: Filter Activity** Use filters: - **Date Range**: Last hour / day / week / month - **Outcome**: Filter by specific outcome type - **Volunteer**: Filter by volunteer name - **Cut**: Filter by territory **Step 3: Export Activity** Click **Export CSV** to download activity feed for reporting. ### Tracking Cut Completion **Step 1: View Cut Progress** **Cut Progress** card displays: - **Cut Name**: Territory name - **Total Addresses**: Count of addresses in cut - **Visited**: Count of addresses with CanvassVisit records - **Completion**: Percentage (progress bar) - **Last Activity**: Time since last visit **Step 2: View Detailed Progress** Click cut row to view: - **Address List**: All addresses in cut with visit status - **Visit Heatmap**: Map showing visited (green) vs unvisited (blue) - **Outcome Breakdown**: Pie chart of visit outcomes - **Volunteer Breakdown**: Who visited which addresses ### Volunteer Leaderboard **Step 1: View Leaderboard** **Leaderboard** card displays: - **Rank**: 1st, 2nd, 3rd place - **Volunteer Name**: Volunteer name - **Total Visits**: Visit count - **Doors/Hour**: Efficiency metric - **Top Outcome**: Most common outcome **Step 2: Filter by Time Period** Toggle time period: - **Today**: Visits since midnight - **This Week**: Visits since Monday - **This Month**: Visits since 1st of month - **All Time**: Total visits ## Volunteer Workflow ### Starting a Canvass Session **Step 1: Login** Login at `/login` with volunteer account (or use TEMP account from shift signup). **Step 2: View Assignments** Navigate to **Volunteer → My Assignments**. **Step 3: Select Shift** Click **Start Canvass** on a shift with cut assignment. **Step 4: Grant GPS Permission** Browser requests geolocation permission. Click **Allow**. **Step 5: Start Session** System redirects to `/volunteer/canvass/:cutId` (full-screen map). System will: 1. Create CanvassSession (status=ACTIVE) 2. Create TrackingSession (linked 1:1) 3. Load addresses within cut polygon 4. Calculate walking route from current GPS position 5. Start GPS auto-tracking (submit points every 10s) ### Following Walking Route **Step 1: View Route on Map** Map displays: - **Blue Polyline**: Optimized walking route - **Blue Markers**: Unvisited addresses (ordered by route) - **Green Markers**: Visited addresses - **Red Marker**: Current GPS position (live updating) **Step 2: Navigate to First Address** Follow route to nearest unvisited address. Route recalculates when you move. **Step 3: View Address Details** Tap marker to view: - **Address**: Street address + unit number - **Resident Name**: First/Last name (if available) - **Support Level**: Previous support level (if available) - **Last Visit**: Previous visit outcome + date (if applicable) ### Recording a Visit **Step 1: Knock on Door** Approach address and knock/ring doorbell. **Step 2: Open Visit Recording Form** Tap **Record Visit** button in bottom toolbar. Bottom sheet slides up. **Step 3: Select Outcome** Choose visit outcome: - **Not Home**: Nobody answered - **Refused**: Refused to speak - **Moved**: Resident moved away - **Already Voted**: Already voted (GOTV campaigns) - **Spoke With**: Had conversation - **Left Literature**: Left campaign material - **Come Back Later**: Asked to return later **Step 4: Update Support Level (if applicable)** For "Spoke With" outcome, select support level: - **Level 1 (Strong)**: Green badge - **Level 2 (Leaning)**: Yellow badge - **Level 3 (Undecided)**: Gray badge - **Level 4 (Opposed)**: Red badge **Step 5: Sign Request (optional)** Toggle **Sign Requested** if resident wants lawn/window sign. **Step 6: Add Notes (optional)** Enter free-text notes (e.g., "Asked about healthcare policy", "Concerned about taxes"). **Step 7: Save Visit** Tap **Save Visit**. System will: 1. Create CanvassVisit record with outcome + timestamp 2. Update Address with new support level + sign status + notes 3. Increment session.totalVisits count 4. Update cut.completionPercentage 5. Create LocationHistory audit record 6. Submit GPS trackpoint with eventType=VISIT_RECORDED 7. Update marker to green (visited) 8. Recalculate walking route (exclude visited address) ### Ending a Canvass Session **Step 1: Finish Route** Complete visits for all addresses (or as many as possible). **Step 2: End Session** Tap **End Session** button in header. **Step 3: Confirm** Confirmation modal displays session summary: - **Duration**: Total session time - **Visits**: Number of visits recorded - **Distance**: Total distance walked (from GPS tracking) - **Doors/Hour**: Efficiency metric **Step 4: Submit** Tap **End Session**. System will: 1. Update CanvassSession (status=COMPLETED, endedAt=now) 2. End TrackingSession (isActive=false, endedAt=now) 3. Calculate final stats (totalVisits, totalDistanceM) 4. Redirect to `/volunteer/activity` (visit history page) ### Viewing Visit History **Step 1: Navigate to My Activity** Navigate to **Volunteer → My Activity**. **Step 2: View Visit List** Table displays: - **Address**: Location visited - **Outcome**: Visit result (icon + label) - **Support Level**: Updated support level - **Visit Date**: Formatted date/time - **Notes**: Canvassing notes (truncated) **Step 3: Filter Visits** Use filters: - **Date Range**: Last week / month / all time - **Outcome**: Filter by specific outcome - **Support Level**: Filter by support level **Step 4: View Session History** Navigate to **My Routes** to view: - **Session List**: Past canvass sessions - **Session Map**: GPS route polyline + visited markers - **Session Stats**: Duration, visits, distance, doors/hour ## Code Examples ### Start Canvass Session (Backend) ```typescript // api/src/modules/map/canvass/canvass.service.ts async startSession(userId: string, data: StartSessionInput) { const { cutId, shiftId, startLat, startLng } = data; // Check for existing active session const existing = await prisma.canvassSession.findFirst({ where: { userId, status: CanvassSessionStatus.ACTIVE }, }); if (existing) { throw new AppError(400, 'Already have an active session', 'SESSION_ACTIVE'); } // Create session + tracking session in transaction const session = await prisma.$transaction(async (tx) => { const canvassSession = await tx.canvassSession.create({ data: { userId, cutId, shiftId, status: CanvassSessionStatus.ACTIVE, }, }); if (startLat && startLng) { await tx.trackingSession.create({ data: { userId, canvassSessionId: canvassSession.id, lastLatitude: new Prisma.Decimal(startLat), lastLongitude: new Prisma.Decimal(startLng), lastRecordedAt: new Date(), }, }); } return canvassSession; }); setActiveCanvassSessions( await prisma.canvassSession.count({ where: { status: CanvassSessionStatus.ACTIVE }, }) ); return session; } ``` ### Calculate Walking Route (Backend) ```typescript // api/src/modules/map/canvass/canvass-route.service.ts export function calculateWalkingRoute( locations: RouteLocation[], startLat?: number, startLng?: number, cutGeojson?: string, ): RouteResult { if (locations.length === 0) { return { orderedLocations: [], totalDistanceMeters: 0, estimatedMinutes: 0 }; } // Determine starting point let currentLat: number; let currentLng: number; if (startLat !== undefined && startLng !== undefined) { currentLat = startLat; currentLng = startLng; } else if (cutGeojson) { const polygons = parseGeoJsonPolygon(cutGeojson); const centroid = calculateCentroid(polygons[0]!); currentLat = centroid.lat; currentLng = centroid.lng; } else { // Use first location as starting point currentLat = locations[0]!.latitude; currentLng = locations[0]!.longitude; } const remaining = [...locations]; const ordered: RouteLocation[] = []; let totalDistance = 0; // Nearest-neighbor algorithm while (remaining.length > 0) { let nearestIdx = 0; let nearestDist = Infinity; for (let i = 0; i < remaining.length; i++) { const loc = remaining[i]!; const dist = haversineDistance(currentLat, currentLng, loc.latitude, loc.longitude); if (dist < nearestDist) { nearestDist = dist; nearestIdx = i; } } const nearest = remaining.splice(nearestIdx, 1)[0]!; ordered.push(nearest); totalDistance += nearestDist; currentLat = nearest.latitude; currentLng = nearest.longitude; } const WALKING_SPEED_MPS = 5000 / 60; // 5 km/h in m/min const MINUTES_PER_DOOR = 2; const walkingMinutes = totalDistance / WALKING_SPEED_MPS; const doorMinutes = ordered.length * MINUTES_PER_DOOR; const estimatedMinutes = Math.round(walkingMinutes + doorMinutes); return { orderedLocations: ordered, totalDistanceMeters: Math.round(totalDistance), estimatedMinutes, }; } ``` ### Record Visit (Backend) ```typescript // api/src/modules/map/canvass/canvass.service.ts async recordVisit(userId: string, data: RecordVisitInput) { const { sessionId, addressId, outcome, supportLevel, signRequested, notes } = data; // Verify session ownership and active status const session = await prisma.canvassSession.findFirst({ where: { id: sessionId, userId, status: CanvassSessionStatus.ACTIVE }, }); if (!session) { throw new AppError(404, 'Active session not found', 'SESSION_NOT_FOUND'); } // Create visit + update address in transaction const visit = await prisma.$transaction(async (tx) => { const canvassVisit = await tx.canvassVisit.create({ data: { sessionId, userId, addressId, outcome, supportLevel, signRequested: signRequested ?? false, notes, }, }); // Update address with new data if (supportLevel || signRequested !== undefined || notes) { await tx.address.update({ where: { id: addressId }, data: { ...(supportLevel && { supportLevel }), ...(signRequested !== undefined && { sign: signRequested }), ...(notes && { notes }), }, }); } // Increment session visit count await tx.canvassSession.update({ where: { id: sessionId }, data: { totalVisits: { increment: 1 } }, }); // Update cut completion percentage if (session.cutId) { const cutId = session.cutId; const totalAddresses = await tx.address.count({ where: { location: { // Point-in-polygon query omitted for brevity }, }, }); const visitedAddresses = await tx.canvassVisit.count({ where: { session: { cutId }, }, }); const completionPercentage = Math.round((visitedAddresses / totalAddresses) * 100); await tx.cut.update({ where: { id: cutId }, data: { completionPercentage }, }); } return canvassVisit; }); recordCanvassVisit(outcome); return visit; } ``` ### GPS Auto-Tracking (Frontend) ```typescript // admin/src/components/canvass/GPSTracker.tsx useEffect(() => { if (!session || !geolocationEnabled) return; const watchId = navigator.geolocation.watchPosition( (position) => { const point = { latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, recordedAt: new Date().toISOString(), }; // Add to local buffer setPointsBuffer((prev) => [...prev, point]); // Update current position setCurrentPosition([point.latitude, point.longitude]); }, (error) => { console.error('GPS error:', error); message.error('GPS tracking failed'); }, { enableHighAccuracy: true, maximumAge: 0, timeout: 10000, } ); // Submit buffered points every 10 seconds const interval = setInterval(async () => { if (pointsBuffer.length === 0) return; try { await api.post(`/map/tracking/sessions/${trackingSessionId}/points`, { points: pointsBuffer, }); setPointsBuffer([]); } catch (error) { console.error('Failed to submit GPS points:', error); } }, 10000); return () => { navigator.geolocation.clearWatch(watchId); clearInterval(interval); }; }, [session, geolocationEnabled, pointsBuffer, trackingSessionId]); ``` ### Visit Recording Form (Frontend) ```typescript // admin/src/components/canvass/VisitRecordingForm.tsx const handleSubmit = async (values: any) => { try { await api.post('/map/canvass/visits', { sessionId: session.id, addressId: selectedAddress.id, outcome: values.outcome, supportLevel: values.supportLevel, signRequested: values.signRequested, notes: values.notes, }); message.success('Visit recorded'); form.resetFields(); onVisitRecorded(); } catch (error) { message.error('Failed to record visit'); } }; ``` ## Troubleshooting ### Issue: Walking Route Not Optimal **Symptoms:** - Route backtracks frequently - Total distance much longer than expected - Route doesn't start from volunteer GPS position **Causes:** - Nearest-neighbor algorithm is greedy (not globally optimal) - Starting position not provided (defaults to cut centroid) - GPS accuracy poor (volunteer position inaccurate) **Solutions:** 1. **Use volunteer GPS position as start**: ```typescript // Always pass volunteer GPS position to route calculation const route = await calculateWalkingRoute( locations, currentLat, currentLng, cut.geojson ); ``` 2. **Consider alternative algorithms**: For better optimization, use 2-opt or genetic algorithms (computationally expensive): ```typescript // Install optimization library npm install routing-js // Use 2-opt algorithm import { twoOpt } from 'routing-js'; const optimized = twoOpt(locations, distanceMatrix); ``` 3. **Pre-optimize routes for shifts**: Admin can pre-calculate optimal routes and assign to volunteers: ```typescript // Calculate route once, store in Shift model const route = await calculateWalkingRoute(locations); await prisma.shift.update({ where: { id: shiftId }, data: { preCalculatedRoute: JSON.stringify(route) }, }); ``` ### Issue: Session Auto-Abandoned Prematurely **Symptoms:** - Active session marked ABANDONED while volunteer still canvassing - Session timeout after <12 hours - Volunteer can't record visits after timeout **Causes:** - `CANVASS_SESSION_TIMEOUT_HOURS` set too low - Volunteer paused for lunch/break (no activity for >1 hour) - System clock drift **Solutions:** 1. **Increase timeout**: ```bash # In .env CANVASS_SESSION_TIMEOUT_HOURS=24 # Was 12, increase to 24 ``` 2. **Record "heartbeat" visits**: Add periodic "still active" ping to prevent timeout: ```typescript // Volunteer app sends heartbeat every 30 minutes setInterval(async () => { await api.post(`/map/canvass/sessions/${sessionId}/heartbeat`); }, 30 * 60 * 1000); ``` 3. **Allow session resumption**: Let volunteers resume ABANDONED sessions: ```typescript // Backend: Add resume endpoint async resumeSession(userId: string, sessionId: string) { const session = await prisma.canvassSession.findFirst({ where: { id: sessionId, userId, status: CanvassSessionStatus.ABANDONED }, }); if (!session) { throw new AppError(404, 'Abandoned session not found', 'SESSION_NOT_FOUND'); } return prisma.canvassSession.update({ where: { id: sessionId }, data: { status: CanvassSessionStatus.ACTIVE }, }); } ``` ### Issue: GPS Tracking Draining Battery **Symptoms:** - Volunteer phone battery drains rapidly - Phone overheats during canvassing - GPS tracking fails after 2-3 hours **Causes:** - `enableHighAccuracy` uses GPS + WiFi + cellular (power-hungry) - Watchposition submits too frequently (every second) - Screen stays on during entire session **Solutions:** 1. **Reduce GPS accuracy**: ```typescript navigator.geolocation.watchPosition( callback, errorCallback, { enableHighAccuracy: false, // Use WiFi/cellular only (less accurate but lower power) maximumAge: 5000, // Cache position for 5s timeout: 30000, // Longer timeout } ); ``` 2. **Reduce submission frequency**: ```typescript // Submit GPS points every 30s instead of 10s const SUBMIT_INTERVAL_MS = 30000; // Was 10000 ``` 3. **Pause tracking during breaks**: Add "Pause Tracking" button to stop GPS watchPosition: ```typescript const pauseTracking = () => { navigator.geolocation.clearWatch(watchId); setTrackingPaused(true); }; const resumeTracking = () => { // Start watchPosition again setTrackingPaused(false); }; ``` ## Performance Considerations ### Visit Recording Rate Limiting **Prevent Abuse:** Rate limit prevents accidental bulk submissions: ```typescript // api/src/middleware/rate-limit.ts const canvassVisitLimiter = new RateLimiterRedis({ storeClient: redis, keyPrefix: 'rl:canvass-visit:', points: 30, // 30 visits duration: 60, // per 60 seconds blockDuration: 300, // block for 5 minutes if exceeded }); ``` **Legitimate Use Cases:** - **Bulk data entry**: Admin can bypass rate limit for importing historical data - **Offline sync**: Mobile app queues visits offline, submits when online (batch endpoint) ### Session Cleanup Performance **Efficient Abandoned Session Query:** ```sql -- Index for abandoned session cleanup CREATE INDEX idx_canvass_sessions_abandoned ON "CanvassSession" ("status", "startedAt") WHERE status = 'ACTIVE'; -- Efficient query SELECT id FROM "CanvassSession" WHERE status = 'ACTIVE' AND "startedAt" < NOW() - INTERVAL '12 hours'; ``` ### Cut Completion Calculation **Avoid N+1 Queries:** ```typescript // Inefficient: query per cut for (const cut of cuts) { const visited = await prisma.canvassVisit.count({ where: { session: { cutId: cut.id } }, }); const total = await getAddressesInCut(cut.id).length; cut.completionPercentage = (visited / total) * 100; } // Efficient: single aggregation query const completionStats = await prisma.canvassSession.groupBy({ by: ['cutId'], where: { status: CanvassSessionStatus.COMPLETED }, _count: { visits: true }, }); ``` ## Related Documentation **Backend Modules:** - [Canvass Backend Module](../../backend/modules/map/canvass.md) — API implementation - [Walking Route Service](../../backend/modules/map/canvass-route.md) — Route optimization - [Tracking Service](../../backend/modules/map/tracking.md) — GPS tracking **Frontend Pages:** - [VolunteerMapPage](../../frontend/pages/volunteer/volunteer-map-page.md) — Full-screen canvass map - [CanvassDashboardPage](../../frontend/pages/admin/canvass-dashboard.md) — Admin oversight - [MyActivityPage](../../frontend/pages/volunteer/my-activity-page.md) — Visit history **Database:** - [CanvassSession Model](../../database/models/canvass.md#canvasssession-model) — Session schema - [CanvassVisit Model](../../database/models/canvass.md#canvassvisit-model) — Visit records - [TrackingSession Model](../../database/models/canvass.md#trackingsession-model) — GPS tracking **Features:** - [Cuts](./cuts.md) — Territory boundaries for canvassing - [Shifts](./shifts.md) — Shift-based canvass scheduling - [Tracking](./tracking.md) — GPS tracking system - [Locations](./locations.md) — Address management