Troubleshooting
The canonical checklist for when something in the compliance stack isn't behaving. Walks through MDS endpoints, the Policy ingester, real-time enforcement, the city portal, and digest emails — in that order, because that's roughly the dependency chain.
MDS endpoints
401 on every request
Symptoms: city's validator returns 401 Unauthorized on /vehicles/status.
Checks:
- The bearer token is missing from
Authorization: Bearer <token>. Cities sometimes paste the token without theBearerprefix. - The token was revoked. Open Settings -> API & Integrations -> MDS Tokens and confirm the token is active.
- The token belongs to a different subaccount. One token per subaccount; cities accidentally testing across multiple jurisdictions will see this.
Validator fails on version field
Symptoms: mds-provider-validator complains the response envelope reports a version other than 2.0.1.
Checks: confirm src/lib/mds/v2/response.ts has const MDS_VERSION = '2.0.1'. If you deployed a stale build, redeploy.
MDS-JWT header missing on a response
Symptoms: city expects every response to carry MDS-JWT: <jwt> and the header is absent.
Checks:
mds_jwks_keyshas nois_active = truerow for the subaccount. Hit/api/mds/<subaccountId>/.well-known/jwks.jsononce to force a lazy mint, then retry.- The keypair was generated but the active row has
private_key_pemcorrupted (rare — usually a manual edit gone wrong). Look at Sentry forMDS sign failedbreadcrumbs.
current_speed_limit_kph always 0
Symptoms: vehicles report current_speed_limit_kph: 0 even when they should be in a zone.
Checks:
- The vehicle has no last-known GPS position. Inspect
vehicles.last_lat/last_lngandvehicles.last_seen. - No operator zone or policy geofence contains the point. Run
SELECT * FROM zones_containing_point('<subaccountId>', <lat>, <lng>)and confirm at least one row is returned. - The active rule's
rule_typeisno_riderather thanspeed— that's a lock, not a speed limit. The status row should showis_disabled = trueinstead.
Policy ingester
City's Policy feed returns 404
Symptoms: audit log entries with status = 'failed' and errors containing HTTP 404.
Checks:
- The URL in
mds_jurisdictions.policy_feed_urlis wrong. Test from your terminal:curl -i <url>. Cities sometimes change paths between provisional and production endpoints. - The auth token expired. Try with and without the token to isolate.
Feed validates but yields no policy_geofences
Symptoms: audit shows status = 'success' and mds_policies rows exist, but policy_geofences is empty.
Checks:
- Every rule referenced geographies the city didn't publish. Inspect
errorsin the audit row — unresolved geography IDs are listed there. - The city published policies for a vehicle type your fleet doesn't operate. Confirm the rules'
vehicle_typesinclude at least one type you have vehicles for.
Same policy keeps appearing in every diff
Symptoms: every poll's diff shows the same policy in modified, even when nothing meaningful changed.
Checks:
- The city is regenerating the feed payload on every request (different
published_date, whitespace, ordering). The sha256 short-circuit fires only on byte-identical payloads. Ask the city to stabilize their serialization. - The city changed
published_dateautomatically — some CMSes do this. The diff is harmless but noisy; can be filtered out in the audit log UI.
Activation never fires
Symptoms: a policy is pending, start_date has passed, but the status never flips to active.
Checks:
- The
mds-policy-activatecron is failing. Check/api/cron/mds-policy-activatein Vercel cron logs. - The
start_dateis in the city's local time but stored as UTC without conversion. The cron compares againstnow() UTC; a mis-set timezone in the feed causes activation to fire much later than expected.
Real-time enforcement
Rule activated but no commands sent
Symptoms: mds_policies.status = 'active' but policy_enforcement_events has zero rows for the rule.
Checks:
- No vehicles are inside the geometry.
SELECT COUNT(*) FROM vehicles WHERE ST_Contains('<rule_geometry>', ...)— if zero, there's nothing to enforce. - All vehicles have stale GPS (>5 min). The skip is by design. Look for
error = 'stale_gps'events in the table. - The vehicles have no linked
iot_devicesrow. Skipped witherror = 'no_iot_device'.
Commands sent but vehicles don't slow down
Symptoms: policy_enforcement_events shows command_sent_at but no command_ack_at, and the vehicle continues at full speed.
Checks:
- OEM-specific OEM offline or asleep. Open the vehicle in the dashboard and inspect last_seen.
- The wrong command format reached the OEM. Inspect
command_responsefor an error code — most OEMs return rejection codes in plain text. - The vehicle's IoT password is wrong. Confirm
iot_devices.iot_passwordmatches what the OEM expects (see OKAI integration, Queclink integration, etc.).
Sub-fleet sees commands, another sub-fleet doesn't
Symptoms: OKAI vehicles obey the slow-zone, Segway vehicles don't (or vice versa).
Checks:
- The per-OEM dispatcher branch is missing or stale for the OEM. Confirm
src/lib/iot/dispatch.tshas a case for that OEM and the command format matches the OEM's protocol version. - Segway BLE-relay latency — the command was sent but hasn't been acked yet because the vehicle is far from any phone. This is a known gap; latency for Segway can exceed 30 seconds.
City portal
Magic link returns "invalid token"
Symptoms: contact clicks the emailed link and lands on an error page.
Checks:
- The token expired (15 minutes). Have them request a new one.
- The contact row was deleted or
portal_access = false. - Two links were minted in quick succession; the first consumed, the second appears stale.
Magic link email never arrives
Symptoms: contact submits the form, gets the "check your email" message, no email comes.
Checks:
- Spam folder.
- The email doesn't match a
city_contactsrow exactly (case-insensitive). Inspect the row. - The email infra rejected the send (Sentry will have the bounce). Look for spam-filter triggers on subject or body.
Portal shows no vehicles even though fleet is deployed
Symptoms: city contact logs in, the fleet map is empty.
Checks:
mds_jurisdictions.bboxis wrong — too tight or pointing elsewhere. Confirm against a map preview.- All vehicles have null
last_lat/last_lng. Inspect at least one.
Digest emails
Digest didn't arrive on the expected day
Symptoms: city contact expected a Monday weekly digest, didn't get one.
Checks:
last_digest_sent_atis in the current week — the cron decided the cadence window hadn't elapsed. Likely a previous digest fired in the same window.- The cron didn't run at all. Vercel cron logs for
/api/cron/compliance-digest. - The digest fired but the email send errored. Check
city_compliance_reports— if the row exists but no email landed, the failure is downstream.
Digest shows a failing condition that you've already fixed
Symptoms: scoreboard in the digest shows red on complaint_sla even though all complaints were responded to.
Checks:
- The digest covers a closed period (yesterday for daily, last week for weekly). It will not retroactively recompute. The current scoreboard at
/dashboard/compliance/{id}is the live view.
PDF attachment missing on monthly digest
Symptoms: monthly digest arrives but pdf_url is null and the attachment is absent.
Checks:
- The PDF renderer errored. Sentry will have the stack trace. The HTML/text body still fires; the PDF is best-effort.
Where to look in the database
| Question | Table |
|---|---|
| Why didn't my Policy poll succeed? | mds_policy_audit |
| What policies are currently active? | mds_policies WHERE status = 'active' |
| What rules came from each policy? | mds_policy_rules WHERE policy_id = ... |
| What geofences materialized from a rule? | policy_geofences WHERE rule_id = ... |
| What IoT commands fired for a rule? | policy_enforcement_events WHERE rule_id = ... |
| What digests has a contact received? | city_compliance_reports WHERE delivered_to_contact_id = ... |
| What jurisdictions does the operator have? | mds_jurisdictions WHERE subaccount_id = ... |
When all else fails
Email support@levyelectric.com with:
- Subaccount ID
- Jurisdiction slug
- The audit
run_idif the issue is in the ingester - The
policy_enforcement_events.idif the issue is in enforcement - The
city_compliance_reports.idif the issue is in a digest
We have direct access to logs and can usually pinpoint the failure within an hour.