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.

FeatureDescription
Production URLservice.progressivesurface.comLive (2026-06-29)
API Backendhttps://api.progressivesurface.com/api/service
AuthenticationAzure AD / Entra (via @progressivesurface/auth); SPA app reg 34d0df1c-57b1-455f-a7fe-782361d714e0
DeploymentAzure App Service psi-service (Linux/Node 24, PS-WEBAPPS) + Private Endpoint 10.160.140.24, publicNetworkAccess=Disabled
RepositoryC:\GIT\psi-serviceProgressiveSurface/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.DATE overrides 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.1287 endpoints).
  • Phase 4 — reports (service/contact/activity), Excel export, email notifications.
  • Activity-log grid (ACTIVITY.LOG.1287); adopt @progressivesurface/ui CSS once on Tailwind v4.

Architecture

psi-service (React SPA) ──Entra access token (API scope)──▶ PSI.UniData.API ──U2──▶ UniData SERVICE.1287
ComponentTechnology
FrameworkReact 18 + TypeScript
BuildVite 5
StylingTailwindCSS 3 (brand green #027A54 via config)
Server stateTanStack React Query
Client stateZustand (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/auth encodes).
  • Requests an access token for the PSI.UniData.API scope (api://b3db69d9-…/user_impersonation); the API validates the token audience.
  • VITE_USE_DEV_ENDPOINTS=true bypasses auth and uses the API’s anonymous /dev routes 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, delegated user_impersonation on 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); public service A → .24 + asuid.service TXT. DC privatelink records (psi-service / .scm.24) added on PS-AZ-DC01.
  • CORS: api.progressivesurface.com must allowlist https://service.progressivesurface.com (+ https://psi-service.azurewebsites.net). Note deploy-ps-proxy.yml preserves the live appsettings.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:

  1. VB_GETLIST for 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’s 1 / 210 / 412-4319 scattered across them).
  2. 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 raw file.Read returns 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)

AttrFieldAttrField
1ACCOUNT # (→ name lookup)24PARTNO (multi)
2OPENED BY (employee # → name)25PARTDESC (multi)
3CREATED (internal date)26PRODUCTS
4CURRSTATUS29WARRANTY TYPE (= Support level)
5PROBLEMDESC30PROJECTS (assoc. multi)
6FIRST.TYPE (service type)31CURRENT SITUATION
8ASSIGNED (employee # → name)32–41Contact: first, last, company, dept, addr1, addr2, city, state, zip, country
10SOLUTION42PHONES (sub-valued): country┊area┊number┊ext┊type
11COMPLETED BY43EMAIL (multi)
12COMPLETED (internal date)44PRIORITY (base 0–10)
13PROJECT # (→ title lookup)47POS.ARROW (C=Customer, P=Progressive)
15/17/18VENDORS / MANUFACTURES / FAILUREMODE48HARD.DATE / target (internal)
21/22LAST/PLANNED ACTIVITY54SAFETY (0/1 → N/Y)
23NOTES / log

Derived-field lookups (mirror clsService / clsUniText)

Detail fieldSource
Account nameACCOUNT.1287 key 1!<acct#>NAME + LOCATION (→ “Name, Location”)
Project titlePROJECT.1287 key 1!<proj# padded to 6>attribute 1
Opened-by / Assigned nameEMPLOYEE.PUBLIC.1287 key <emp#>FIRST.NAME + LAST.NAME
Phoneparse 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.