# Architecture Reference _Last updated: 2026-09-23 (v1.5.9)_ ## Data Flow ``` Disco DB (open jobs) │ ├─► ServiceTrackerService.BuildDashboard() │ │ Google Sheet CSV │ ├─► BuildDiscoTile() ─┐ │ │ └─► BuildSheetTile() ─┤ ▼ │ ▼ GoogleSheetService List (fetch → cache → parse) │ ▼ Bucket sort into: - ReadyForReturn - OnHold - AwaitingVendor - AwaitingParts - Tiles (main grid) │ ▼ DashboardViewModel │ ▼ WebHandler → HTML string ``` ## Persistence (JSON files) All under `App_Data/Plugins/Disco.Plugins.ServiceTracker/`: | File | Contents | |------|---------| | `config.json` | `ServiceTrackerConfig` — priorities, locations, statuses, technicians, sheet config | | `tickets/` | One JSON file per Disco job (`{jobId}.json`) — `ServiceTicket` overrides, notes, changelog | | `external_tickets/` | One JSON file per NTT sheet row (`{internalId}.json`) — same shape as ServiceTicket | | `sheet_cache.csv` | Last fetched CSV from Google; invalidated by `RefreshMinutes` | ## ID Generation for NTT Tickets Stable integer ID from djb2 hash of `timestamp|email|task|issue` (lowercased, trimmed), modulo 800000 + 100000 offset. Collisions resolved by incrementing. IDs are stable across refreshes as long as those four fields don't change. ## Column Index Contract All Google Sheet column indices live in `GoogleSheetConfig`. Current mapping (updated 2026-09-23): ``` 0 ColTimestamp — Timestamp 1 ColEmail — Email address 2 ColTask — What is the task/request? 3 ColIssue — Detailed Description 4 ColLocation — Location / Classroom 5 ColRequestedBy — Requested By 6 ColPriority — Priority Level 7 ColPreferredDate — Preferred Completion Date 8 ColCampus — Campus (location fallback if ColLocation empty) 9 ColAssignedTo — Assigned to 10 ColStatus — Status 11 — — Column 1 (junk, ignored) 12 — — Campus duplicate (ignored) 13 ColNotes — Notes 14 — — Category (ignored) 15 — — Resolution Confirmed (ignored) ColCompletionDate = -1 (no dedicated column; disabled) ``` Set any index to `-1` to disable it. `SafeGet()` returns null for index < 0. ## Sidebar Bucket Logic Buckets are evaluated in priority order and are mutually exclusive: ```csharp rfr = status contains "ready for return" OR "ready for pickup" onHold = status contains "on hold" AND NOT in rfr awaitingVendor= status contains "awaiting vendor" AND NOT in rfr, onHold awaitingParts = status contains "awaiting parts" AND NOT in rfr, onHold, awaitingVendor main = everything else ``` Status is checked against `StatusOverride ?? DiscoStatus`. ## NTT Closed Ticket Suppression NTT tickets are skipped in `BuildDashboard` if their saved `StatusOverride` contains any of: `resolved, dismissed, complete, closed, done, cancel, finished` This is independent of the sheet's own status column — it prevents jobs dismissed in the tracker from reappearing on every sheet refresh. ## Location Resolution for NTT Tiles ``` ExternalTicket.Location (from ColLocation) └─ empty? → try ColCampus └─ still empty? → null MatchSheetLocation(raw): 1. Exact ID match against cfg.Locations 2. Exact Name match (case-insensitive) 3. Substring match either direction Match found → use matched location (id/name/icon/colour) No match → use DefaultLocation + store raw in RawSheetLocation (displayed as italic 📍 pill on tile) Saved LocationId override (from external_tickets/) always wins over sheet text. ``` ## HTML Generation Pattern No Razor. Every page is a method returning `string` from a `StringBuilder`: - `BuildDashboardPage(DashboardViewModel)` — main dashboard - `BuildDetailPage(Job, ServiceTicket, ...)` — job detail + edit - `BuildConfigPage(ServiceTrackerConfig, ...)` — settings - `TileHtml(DashboardTile)` — single tile card - `SidebarPanel(title, color, items, url, showCollected)` — reusable sidebar section CSS is returned from `CSS()`, `DCSS()`, `CCSS()` string methods and inlined in `