Customer Service Manager (psi-service)
Web application for triaging and tracking customer service tickets, replacing the failing legacy VB6 PTIServiceTicket desktop app.
Overview
psi-service is a React/TypeScript single-page application that gives PSI service staff a modern interface over the customer service ticket data in UniData (SERVICE.1287). It connects to the PSI.UniData.API for data access, the same pattern as Redbook Web and PSI Dispatch.
It replaces the VB6 PTIServiceTicket app (C:\GIT\PTI_VBP\PTIServiceTicket_2.0). The full behavioral accounting of the legacy app lives in C:\GIT\Service5\docs\migrations\SERVICE_TICKET_MIGRATION_SPEC.md.
| Feature | Description |
|---|---|
| Production URL | service.progressivesurface.com — Live (2026-06-29) |
| API Backend | https://api.progressivesurface.com/api/service |
| Authentication | Azure AD / Entra (via @progressivesurface/auth); SPA app reg 34d0df1c-57b1-455f-a7fe-782361d714e0 |
| Deployment | Azure App Service psi-service (Linux/Node 24, PS-WEBAPPS) + Private Endpoint 10.160.140.24, publicNetworkAccess=Disabled |
| Repository | C:\GIT\psi-service — ProgressiveSurface/psi-service |
Features
Phase 1 — read-only triage board ✅ (deployed 2026-06-29)
- SLA priority board: color-coded, sortable ticket grid (priority, ticket #, project, service type, account, problem, created, days open, assigned to, status). Row tint and priority chip by SLA band.
- Time-decay priority: a faithful client-side port of the VB6 algorithm — priority decays toward 0 (more urgent) as an open ticket ages within its SLA window. Completed/installation = 10; customer-possession (POS.ARROW = C) does not decay;
HARD.DATEoverrides the band window; past-SLA collapses to ~1; safety flag ×0.1. Computed in the browser so the board re-sorts/re-colors live (replaces the VB6 60-second refresh timer with React Query auto-refresh).- Bands: red 0–1.9, orange 2–3.9, yellow 4–6.9, blue 7–9, green (completed), white (installation).
- Filters: status (open/completed/both), account, assigned-to, project — applied server-side; free-text search applied client-side over problem/account/contact/etc.
- Read-only detail pane: problem/solution/notes, priority factors (urgency, support level, safety, possession), embedded contact, and multi-value lists (parts, vendors, service types, phones, emails).
Planned (later phases)
- Phase 2 — ticket create/edit (
POST/PUT /api/service) with optimistic concurrency. - Phase 3 — contact management (reuses the existing
CONTACT.1287endpoints). - Phase 4 — reports (service/contact/activity), Excel export, email notifications.
- Activity-log grid (
ACTIVITY.LOG.1287); adopt@progressivesurface/uiCSS once on Tailwind v4.
Architecture
psi-service (React SPA) ──Entra access token (API scope)──▶ PSI.UniData.API ──U2──▶ UniData SERVICE.1287
| Component | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build | Vite 5 |
| Styling | TailwindCSS 3 (brand green #027A54 via config) |
| Server state | TanStack React Query |
| Client state | Zustand (filters) |
| Auth | @progressivesurface/auth (MSAL/Entra) from GHE Packages |
Backend (PSI.UniData.API): ServiceEndpoints / ServiceService read SERVICE.1287 via the VB_GETLIST.REV1 + BSELECT pattern (shared with the Dispatch list). The API returns the raw SLA inputs rather than a computed priority, so the board can recompute live. Per-user Entra→UniData session for audit; anonymous service-account /dev/* routes for local development.
Auth profile
Approved client/app-layer auth application under the PSI Web App Compliance Standard:
- SPA auth via Entra/MSAL using redirect flow + localStorage cache (the defaults
@progressivesurface/authencodes). - Requests an access token for the PSI.UniData.API scope (
api://b3db69d9-…/user_impersonation); the API validates the token audience. VITE_USE_DEV_ENDPOINTS=truebypasses auth and uses the API’s anonymous/devroutes for local development only.
Deployment
Production deployment runs through GitHub Actions on the PSI self-hosted runner using identity-based Azure login (az login --identity) — no publish profiles or basic credentials. The workflow authenticates npm to GHE Packages with the job GITHUB_TOKEN (packages: read) to resolve @progressivesurface/*, builds the SPA, and zip-deploys to Azure App Service (PS-WEBAPPS). The health gate accepts 200/401/403 per the intended auth posture.
The Azure App Service and the SPA Entra app registration are provisioned at first deploy; the workflow reads the app name and auth IDs from repo variables.
Go-live (2026-06-29, issue #2)
Provisioned to the compliance standard: App Service psi-service on the shared asp-erp-migration-tool plan (B3 Linux, NODE|24-lts, startup npm start), system-assigned MI, VNet integration on PS-WebApps, private endpoint 10.160.140.24 (PS-ProdData), publicNetworkAccess=Disabled, SCM/FTP basic creds disabled, HTTPS-only, wildcard *.progressivesurface.com cert bound (SNI), connected to shared psi-webapps-insights.
- Entra: SPA app reg Customer Service Manager (
34d0df1c-57b1-455f-a7fe-782361d714e0) — SPA redirect URIs for the custom domain +psi-service.azurewebsites.net, delegateduser_impersonationon PSI.UniData.API, admin-consented; tenant-wide consent for basic sign-in scopes so users sign in silently. - DNS: Azure private zone
psi-service+.scm→.24(PE zone group); publicserviceA →.24+asuid.serviceTXT. DC privatelink records (psi-service/.scm→.24) added on PS-AZ-DC01. - CORS:
api.progressivesurface.commust allowlisthttps://service.progressivesurface.com(+https://psi-service.azurewebsites.net). Notedeploy-ps-proxy.ymlpreserves the liveappsettings.Production.json(merges only new sections), so the live PS-PROXY config is updated in place + service restarted; the repo (deploy/appsettings.Production.json) is the source of truth for fresh installs. - Repo variables:
AZURE_CLIENT_ID,AZURE_TENANT_ID,API_SCOPE,API_URL(https://api.progressivesurface.com/api),AZURE_WEBAPP_NAME(psi-service),HEALTH_URL.
SERVICE.1287 read model (detail) — hard-won, read before touching the detail
The board/list read is fine via VB_GETLIST.REV1 + BSELECT (evaluates dict fields; short
columns). The single-ticket detail is different and was rebuilt 2026-06-30 to faithfully mirror
the legacy VB6 clsService.OpenService (C:\GIT\PTI_Classes_2.1\clsService.cls). Two dead ends to
avoid:
VB_GETLISTfor detail truncates long multi-line text (PROBLEMDESC/SOLUTION/NOTES) and its fixed-width column split desyncs when the embedded contact’s sub-valued phone is present — shifting PHONES/EMAIL/PART (ticket 6229 showed the phone’s1 / 210 / 412-4319scattered across them).- Reading by dict name via
file.Read(D-type only) drops derived fields.ACCOUNT.NAME, the assignee, the contact phone and the project title are not stored — they’re I-type translates / live lookups. A rawfile.Readreturns the stored numbers but no names.
The working approach: read the raw record (DataQueryService.GetRecord* → RawAttributes,
1-based) and map by attribute number, then resolve the derived fields with the same lookups VB6
uses. Dates are stored internal (epoch 1967-12-31) — convert to ISO.
SERVICE.1287 attribute map (key CO!TICKET, e.g. 1!6221)
| Attr | Field | Attr | Field |
|---|---|---|---|
| 1 | ACCOUNT # (→ name lookup) | 24 | PARTNO (multi) |
| 2 | OPENED BY (employee # → name) | 25 | PARTDESC (multi) |
| 3 | CREATED (internal date) | 26 | PRODUCTS |
| 4 | CURRSTATUS | 29 | WARRANTY TYPE (= Support level) |
| 5 | PROBLEMDESC | 30 | PROJECTS (assoc. multi) |
| 6 | FIRST.TYPE (service type) | 31 | CURRENT SITUATION |
| 8 | ASSIGNED (employee # → name) | 32–41 | Contact: first, last, company, dept, addr1, addr2, city, state, zip, country |
| 10 | SOLUTION | 42 | PHONES (sub-valued): country┊area┊number┊ext┊type |
| 11 | COMPLETED BY | 43 | EMAIL (multi) |
| 12 | COMPLETED (internal date) | 44 | PRIORITY (base 0–10) |
| 13 | PROJECT # (→ title lookup) | 47 | POS.ARROW (C=Customer, P=Progressive) |
| 15/17/18 | VENDORS / MANUFACTURES / FAILUREMODE | 48 | HARD.DATE / target (internal) |
| 21/22 | LAST/PLANNED ACTIVITY | 54 | SAFETY (0/1 → N/Y) |
| 23 | NOTES / log |
Derived-field lookups (mirror clsService / clsUniText)
| Detail field | Source |
|---|---|
| Account name | ACCOUNT.1287 key 1!<acct#> → NAME + LOCATION (→ “Name, Location”) |
| Project title | PROJECT.1287 key 1!<proj# padded to 6> → attribute 1 |
| Opened-by / Assigned name | EMPLOYEE.PUBLIC.1287 key <emp#> → FIRST.NAME + LAST.NAME |
| Phone | parse attr 42 sub-values → (area) number xext; type code → PHONETYPE.1287 description |
Department and Urgency are intentionally not surfaced — clsService doesn’t map an assigned
department or an urgency on this screen (service type is the scalar FIRST.TYPE @6).
Code: ServiceService.GetTicketAsync / MapDetailFromAttributes (PSI.UniData.API); rendered by
TicketDetail.tsx (psi-service). The legacy source of truth is clsService.cls +
clsUniText.cls in C:\GIT\PTI_Classes_2.1.
Related
- PSI.UniData.API — data backend (
/api/service) - Redbook Web — sibling UniData-backed SPA
- Web App Compliance Standard
- *)