Repairs-maintenance

All Help Topics

Repairs & Maintenance

Ticket triage, preventive maintenance, and the supervisor smart planner — a weekly route that packs open work around your scheduled lawn cuts.


How R&M flows through the app

Three things happen in the R&M area, and they connect:

  1. Tickets — store managers (or the AI inbox) submit repair requests with a category, urgency, and description.
  2. Preventive Maintenance (PM) — a 28-task catalog on a monthly / quarterly / annual cadence. Some tasks are the store manager's, some are the supervisor's, some are vendors'.
  3. Lawn schedule — weekly mowing stops at each store.

The Smart Planner on /repairs-maintenance/schedule packs the supervisor's week from those three streams: weekly lawn cuts as fixed stops, open tickets as priority work, due PMs as filler.

Tickets also feed an AI Diagnose thread with built-in memory — every diagnose call retrieves resolved tickets on the same asset, in the same category, and with similar symptoms, then hands them to Claude as ground-truth context. See AI Diagnose — how it finds past fixes below for the full picture and the knobs that control it.

Ticket triage

When a ticket is submitted it starts as new (unacknowledged). The supervisor (or a 3rd-party vendor) acknowledges it, which sets assignedOpcoSupervisor or assignedThirdParty depending on who's taking it. Acknowledged supervisor tickets drive the schedule dashboard's daily ticket list, sorted by ETA.

Urgency tiers (low → critical) determine priority in the smart plan:

  • Critical — drop everything, drive there now.
  • High — fit into the day's plan ahead of normal work.
  • Normal — default. Most tickets land here.
  • Low — slipped in around peak windows when there's room.

To resolve a ticket, open it and tap Resolve — the modal asks for your name, the resolution note, and (encouraged) the time on job in minutes. That time feeds the learning loop (see below) so the planner's future estimates get more accurate.

Preventive maintenance

The catalog lives in src/lib/repairs-maintenance/pm-schedule.ts. Each task has:

  • A dueMonths array (which months it's due each year)
  • A performedBy role: Manager, Maintenance, Manager/Maint, or Vendor

The supervisor's smart plan only pulls PMs marked Maintenance or Manager/Maint — the other 15 of 28 tasks belong to other roles and would just be noise on the supervisor's day.

Tasks are marked complete on /repairs-maintenance/preventative. The planner reads from MaintenanceCompletion rows for the current year + month, so anything already done that month drops out of the queue.

The smart planner

/repairs-maintenance/schedule → toggle Smart Plan at the bottom of the page.

Inputs

  • State filter (OK / TX / NM / KS / MO) — hard boundary. No cross-state moves.
  • Weekly lawn schedule — entered via the "Schedule Lawn" button. Only the first lawn stop of the day carries a clock time (anchors the day's start). Other lawns just record their duration; the planner picks when they happen.
  • Open tickets for stores in state — supervisor's queue (acknowledged-to-me + brand-new untriaged).
  • Due PMs for stores in state — Maintenance + Manager/Maint tasks not yet completed this month.

Output

  • Mon–Fri day cards, each showing an ordered list of stops with clock times and inter-stop drive estimates.
  • Stops grouped by store (consecutive same-store work clusters under a header so you read the day as "first this location, then that location").
  • Tap any ticket row to jump to the ticket detail; tap any PM row to jump to the preventative checklist.
  • "Re-plan week" button next to the state filter — hit it after editing tickets or lawns to refresh the plan.

How the planner picks the next stop

For each empty time slot in the day:

  1. Score every remaining candidate (lawn / ticket / PM) by priority tier:
    • Lawn = 100 (must happen today, can't slip)
    • Critical ticket = 4
    • High ticket = 3
    • Normal ticket = 2
    • PM = 1.5
    • Low ticket = 1
  2. Same-store boost (+1000): anything at the supervisor's current location wins the queue, so you knock out tickets + PMs at the store you're already at before driving away. PMs only get the boost up to 3 per visit — past that, the 4th PM has to compete normally so the supervisor stays moving across stores instead of drowning in one.
  3. Sort by priority desc, then drive distance asc (Haversine × 1.25 road circuity × 55 mph average — same estimateDrive helper the rest of R&M uses).
  4. Pick the top-scored candidate that fits before 4pm. Advance the clock by drive time + the candidate's duration.

Hard rules

  • Workday ends at 4pm (M–F only).
  • Days with no lawn anchor start at 8:30am at the day's first-picked stop.
  • Lawn stops are required — flagged as "didn't fit" on the day card (amber border) if there's not enough time, so you know to reschedule.

The learning loop

Every resolved ticket can record actualMinutes — captured in the resolve modal next to the resolution notes. The planner aggregates those into a running average per (category, urgency) bucket. Once a bucket has at least 3 resolved tickets, the planner uses the running average instead of the default 90-min estimate for future tickets in that class.

In the resolve modal you'll see a live hint like:

Past high tickets in hvac averaged ~75 min across 12 jobs.

— so you know your entry is calibrating the planner. If the bucket is empty: "No baseline yet for high hvac — your entry is part of building it."

The hint endpoint is /api/repairs-maintenance/learned-estimate; the math lives in src/lib/repairs-maintenance/learned-estimates.ts.

Formula: Key Formulas → R&M smart planner.

AI Diagnose — how it finds past fixes

Every repair ticket has a Diagnose (or Continue diagnosing) button. Tapping it opens a chat thread with Claude — but before Claude writes the first reply, the app runs a retrieval pass against the team's history of resolved tickets and feeds the matches to the model as a Related historical fixes block at the top of the prompt. Claude then cites or rules out those past fixes instead of starting from a blank slate.

This retrieval runs only on the first turn of a diagnose thread. Follow-up turns stay focused on the live conversation so the context doesn't bloat.

What Claude sees (most-specific first)

  1. Same asset — up to 3 resolved tickets on this exact unit (matched by assetId). Highest signal: "this walk-in was last defrosted on 5/26 by Corbyn — bleached the drain line."
  2. Same category — up to 3 resolved tickets in the same equipment category (Walk-in Coolers, Brewers, Bubblers, Ice Machines, HVAC, etc.) across the entire fleet in the last 180 days. Catches the "5 stores hit the same Crathco pump-motor failure" pattern.
  3. Similar symptoms — up to 5 resolved tickets whose description is textually similar to the new ticket's description, scored with Postgres trigram similarity (≥ 15% match). Picks up things like "leaking", "freezing over", "FILL ERR" even when the asset or category don't match.

The three sources merge, dedup by ticket id, rank in that order, and cap at 8 total items. Each row shows Claude the matched ticket #, store, category, resolution date, the original symptom, and the actual fix the team applied — tagged [same asset], [same category], or [similar symptom NN%] so the model knows how to weight it.

What affects retrieval quality

  • Tag the asset on every ticket. Untagged resolved tickets never feed the per-asset lookup. The Need asset tag banner at the top of the tickets page (amber when there's a non-zero count, grey at 100% compliance) is your retro-tag queue — tap a row to land on the ticket and assign its asset.
  • Write a real resolution note (≥ 15 characters). "fixed" / "done" / "ok" get filtered out — they don't help future Claude. "Reset breaker", "Replaced GM1068 pump motor", "Bleached drain + cleared P-trap" all flow through.
  • Be specific in the ticket description. Trigram similarity is the broadest net — the more concrete the symptom is in plain English, the more cross-fleet matches you'll see.

Where to verify it's working

After each diagnose call, the server logs [historical-fixes] retrieval with per-source counts (asset / category / similarity / merged). If a diagnose reply doesn't reference past fixes when you'd expect one, check the log — counts: {asset: 0, category: 0, similarity: 0} means the corpus didn't have a match (or the filters dropped them).

Code lives in src/lib/repairs-maintenance/historical-fixes.ts. The tunable knobs are at the top of that file:

  • RECENT_DAYS (180) — how far back the category + similarity searches reach.
  • SIMILARITY_THRESHOLD (0.15) — how loose the trigram match has to be to qualify.
  • MIN_NOTE_LEN (15) — minimum resolution-text length to count.
  • MAX_ITEMS (8) — cap on items injected into the prompt.

Sources run via Promise.allSettled so a single failing source (e.g. a transient SQL error on the similarity query) never wipes the block — the other two still flow through.

How this differs from "the learning loop"

Both features make Claude better over time, but they're different:

  • The learning loop (above) calibrates the planner's time estimates from actualMinutes you record on resolve. It changes ETAs.
  • The diagnose memory loop (this section) feeds the AI's diagnostic context with the actual fixes the team has applied. It changes the AI's troubleshooting suggestions.

Both improve every time a ticket is resolved cleanly. Both penalize sloppy resolution notes and missing asset tags.