1021 lines
28 KiB
Markdown

# 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