PSI Explorer Web
Unified web portal for PSI manufacturing intelligence. A React application providing project management, BOM visualization, Gantt job analytics, PLC fleet analysis, lead time tracking, and AI-powered machine intelligence via real-time UniData connectivity.
Overview
PSI Explorer Web is a React single-page application that displays BOM (Bill of Materials) data from the AFTEC ERP system via the PSI.UniData.API gateway.
| Feature | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Tree View | Hierarchical expandable BOM display |
| Table View | Flat sortable/filterable table with CSV export |
| Part Details | Click any part to see inventory and specs |
| PLC Fleet | Fleet-wide PLC dashboard with project drill-down to per-project analysis, I/O verification, scorecards, intent, history, similar projects, and CSV export |
| PLC Repo | Browser for the L5X git history archive on PS-PLCRunner — three tabs: Recent Activity (fleet-wide commit feed), Failures (conversion failures grouped by job with SDK error + occurrence count), Browse (per-job version list with exploded file tree and click-to-diff routine XML) |
| Ask the Fleet | AI chat panel powered by Azure OpenAI + MCP tools for natural language machine queries |
| Machine Explorer | CS/field service focused machine intelligence portal — split-view layout with persistent AI chat panel + 5 tabs: Overview, Active Work, Service History, Parts & BOM, Controls. Photo gallery from MultiMedia + K: drive. Deep-linkable via #machine/{job}/{tab} |
| Drawings Viewer (Web ProViewer) | Part-number-driven multi-format viewer (Tools → Drawings, deep-linkable via #viewer/{pn}) — PDF, TIFF, SolidWorks 3D models and 2D drawings (eDrawings/HOOPS SCS with sheets/layers/measure), AutoCAD DWG parsed in-browser (R13+; legacy CimCAD R11/12 → PDF). Conversion runs on PS-PLCRunner (nightly pre-gen + on-demand queue), SCS cached in DFS\CAD\SCS. Fast transfer via zstd/gzip compression + binary endpoint + direct browser→PS-PROXY on-LAN (a full facility assembly loads in ~3.6 s). See the Drawings Viewer section below |
| Service Tickets (Ask the Fleet) | Ask the Fleet can query SERVICE.1287 service tickets (list/search/get) through the MCP server, run as the signed-in user (Entra ID token → trusted delegation → per-user AFTEC audit, never the service account) |
| PC Fleet | Dell PC fleet dashboard — 640+ PCs across machines, age/warranty tracking, Windows version badges, upgrade letter generation |
| Gantt Charts | Job-level Gantt analytics for 2,327 jobs — sortable table, drill-down heatmap (Year>Quarter>Month>Week), velocity chart, multi-job compare, single-job viewer with employee/handoff/redbook/ECN/inventory popups |
| Lead Time | Root cause analysis across 2,500+ projects (216 columns) with 8 analysis views, P2/P98/P50 dept milestones, idle time tracking |
| Performance | Full BOM loads in ~200-300ms |
| Authentication | Entra/Azure AD sign-in required (MSAL.js via @progressivesurface/auth — AuthGuard). The backend validates the ID token via @progressivesurface/auth/server (createUserTokenValidator) for per-user actions; the user’s identity flows to the MCP server as X-On-Behalf-Of for audited tools |
| Property | Value |
|---|---|
| Live Site | BomExplorer.progressivesurface.com |
| Repository | ProgressiveSurface/psi-explorer-web |
| API | api.progressivesurface.com |
| Hosting | Azure App Service (Express.js) |
| Access | VPN (Cisco AnyConnect) or clientless off-VPN / mobile via Azure App Proxy at explorer.progressivesurface.com (Entra sign-in + Conditional Access). The internal UniData API is never internet-exposed. |
Deployment
Production Configuration
| Setting | Value |
|---|---|
| App Service | bom-explorer-web |
| Resource Group | PS-WEBAPPS |
| Custom Domain | explorer.progressivesurface.com |
| Private Endpoint | bom-explorer-web-pe |
| Internal IP | 10.160.0.17 |
| Public Access | Disabled |
| HTTPS Only | Enabled |
| SSL Certificate | Wildcard from Key Vault (ps-certificates-kv) |
| VNet Integration | PS-WebApps subnet |
Network Architecture — private / VPN-only
Public network access on the App Service is Disabled; the app is reachable only via its private endpoint:
On-LAN / VPN → explorer.progressivesurface.com → A 10.160.0.17 (private endpoint) → App Service
explorer.progressivesurface.com resolves to 10.160.0.17 in both the Azure public DNS zone
(ps-rg-01/progressivesurface.com, A record) and the DC split-horizon zone. No public ingress.
Remote Access & Security — App Proxy removed (2026-07-28)
Azure App Proxy was trialed to give clientless/mobile remote access but removed: App Proxy
pre-authentication is fundamentally incompatible with a token-acquiring MSAL SPA — on any
interactive sign-in it 302-redirects the auth-code return to login.microsoftonline.com (no CORS),
the URL fragment is dropped, and MSAL loops (“We couldn’t sign you in”). This is Microsoft-documented
“expected behavior,” not a misconfiguration; no SPA-side change (PKCE dual redirect-URI,
navigateToLoginRequestUrl:false, popup) fixes it. See
App Proxy Implementation §8.
Current state:
- App Proxy publication in maintenance mode; public DNS flipped CNAME→A (
10.160.0.17),asuid.explorerTXT removed — nothing routes to App Proxy. - SPA back on standard MSAL redirect sign-in (the popup shim was removed).
- Auth: the SPA signs in with MSAL and sends the Entra ID-token bearer on
/api(api.ts);/dev/*endpoints are anonymous but only reachable on-VPN. Conditional AccessPSI Explorer — external accessstill applies at MSAL sign-in.
Planned direction: Microsoft Entra Private Access (EPA / Global Secure Access) — network-level ZTNA that never exposes the app to the internet and runs the SPA untouched (no App-Proxy redirect problem), with per-app Conditional Access. Needs the GSA client on devices + a Private Access license (~$5/user/mo on top of P1, or Entra Suite $12). A spike against Explorer is the next step.
Production Stack
| Component | Technology |
| --------- | ---------------------------------- | ------------------------------------------------- |
| Frontend | React 18 SPA (Vite build) |
| Server | Express.js (server.js) |
| Hosting | Azure App Service (Linux, Node 24) |
| API | PSI.UniData.API at api.progressivesurface.com |
CI/CD
The application auto-builds and deploys via GitHub Actions:
npm install- Install dependenciesnpm run build- Build React app (outputs todist/)- Deploy to Azure App Service
- Express server serves static files from
dist/
Environment Configuration
Development (.env or defaults):
VITE_API_URL=/api # Proxied through Vite dev server
VITE_USE_DEV_ENDPOINTS=trueProduction (.env.production):
VITE_API_URL=https://api.progressivesurface.com/api
VITE_USE_DEV_ENDPOINTS=trueCORS Configuration
The PSI.UniData.API must include this origin in its CORS allowed list (appsettings.json):
"Cors": {
"AllowedOrigins": [
"https://explorer.progressivesurface.com"
]
}Features
Tree View
- Expandable/collapsible hierarchy
- Color-coded GT codes (Purchased=green, Manufactured=blue)
- Auto-expands first two levels
- Click part to view details
Table View
- Sort by any column
- Filter by part number, description, or GT code
- CSV export with one click
- Shows all BOM items flat
Part Detail Card
- Part number and description
- GT code and UOM
- Lead times (Mfg, Purchase, Cumulative)
- Inventory (On Hand, On Order, Available, Allocated)
- BOM indicator
Statistics
- Total parts count
- Max BOM depth
- Load time (ms)
- Database calls made
Ask the Fleet (AI Chat)
Any PSI employee can ask questions about any machine in plain English — no AI tools, no configuration, no training. Click “Ask the Fleet” in the bottom-right corner (or press Ctrl+K) and type a question. This is the first production application consuming the PSI Machine Intelligence MCP Server.
What it does: The AI can call any combination of MCP tools — project lookup, BOM explosion, PLC analysis, fault diagnosis, inventory checks, drawing searches, vendor data, cost analysis — and returns streaming answers with rich result cards. Clicking a part number or job number in the response navigates directly to that item in PSI Explorer.
PLC analysis data: When users ask about PLC programs, the AI uses pre-converted L5X files and analysis JSON from the PLC Fleet Catalog — covering 507 Allen-Bradley Logix 5000 projects, plus 414 SLC 500, 248 CNC, and 252 robot programs across 1,391 projects fleet-wide. See L5X Tools for the conversion pipeline.
Example questions:
- “What is project 2399? Who’s on the team?”
- “Show me the BOM for job 2386”
- “Diagnose the low air pressure fault on 2399”
- “Check inventory for part 069995”
- “Compare projects 2399 and 2386”
- “What machines are similar to 2224?”
How it works:
User (browser) → ChatPanel (React)
→ POST /api/chat (SSE stream)
→ Express chat-engine (async generator agentic loop)
→ Azure OpenAI GPT 5.2 (with MCP tool definitions)
↔ MCP Client (@modelcontextprotocol/sdk)
→ PS-PROXY:3100 (MCP Server)
→ UniData API, file shares, PLC analyzers
← Streaming text + tool result events (SSE)
← Rich cards, markdown, clickable navigation
The Express.js backend connects to PS-PROXY:3100 as an MCP client at startup, discovers all available tools, and converts their schemas to the OpenAI tool format. When a user sends a message, the chat engine runs an agentic loop: it sends the conversation + tool definitions to GPT 5.2, streams text back as SSE events, and when the model calls tools, it executes them via MCP and feeds the results back to the model. This loop repeats until the model produces a final text response.
Key implementation details:
- SSE streaming with async generators for clean backpressure handling
- Rich tool result cards — project cards, BOM summaries, fault trace chains, inventory status with color coding
- Dual provider support — Azure OpenAI (primary) or Claude via Azure AI Foundry, auto-selected by env vars
- Conversation persistence in localStorage (client) and in-memory Map (server)
- Context-aware suggested questions when the chat is empty
Backend files: server/mcp-client.js, server/chat-engine.js, server/chat-routes.js
Frontend: src/components/chat/ (ChatPanel, ChatInput, ChatMessage, ToolResultCard, SuggestedQuestions)
Environment variables (set one provider):
| Variable | Description |
|---|---|
AZURE_OPENAI_ENDPOINT | Azure OpenAI endpoint URL |
AZURE_OPENAI_API_KEY | Azure OpenAI API key |
AZURE_OPENAI_API_VERSION | API version (default: 2025-04-01-preview) |
AZURE_OPENAI_MODEL | Model deployment name (default: gpt-5.2-chat) |
ANTHROPIC_FOUNDRY_API_KEY | Alternative: API key from Azure AI Foundry (for Claude) |
CLAUDE_MODEL | Claude model ID (default: claude-sonnet-4-6) |
MCP_SERVER_URL | MCP server URL — must use FQDN on Azure (default: http://ps-proxy:3100/mcp, production: http://ps-proxy.ad.ptihome.com:3100/mcp) |
PLC Fleet Dashboard
Top-level nav page for fleet-wide PLC analysis across all 88 archived Allen-Bradley projects. Provides fleet aggregates and per-project drill-down — click any job number or enter one directly to open the full project PLC view.
Fleet-level views:
- Fleet Stats — total projects, rungs, modules, tags
- Scorecard Summary — fleet averages across 4 dimensions (complexity, safety, reuse, documentation), distribution bar (good/fair/poor), sortable project table with pagination and CSV export
- Processor Distribution — horizontal bar chart of processor types across fleet
- Module Distribution — top 10 most-used I/O modules
- Fleet Reuse & Adoption — horizontal bar charts showing top routines, UDTs, and tag prefixes by adoption rate across fleet
- Recent Changes — nightly scan results with job-level change entries
- Fleet Search — search tags, modules, or programs across all projects (regex)
- Project Comparison — side-by-side controller/tag/module/program diff between any two projects
Per-project drill-down (8 sub-tabs):
| Sub-Tab | What It Shows |
|---|---|
| Modules | I/O module table with slot, catalog #, name, type badge, firmware version, RPI (color-coded if slow), IP address, status (FAULT/INHIBITED/OK). Risk highlighting on fault/inhibited rows. Filter by name, catalog, type, vendor, productType. Sortable by all columns. CSV export. |
| Tags | Controller tags with data type badge, constant flag (lock icon), dimensions, tag type, access. Type dropdown filter. Quick filter chips (All/Faults/Timers/Counters). “Used by” cross-reference chips showing which routines output each tag (from intent data). Paginated (50/page). CSV export. |
| Programs | Expandable program tree with routine details. Search filter (programs + routines by name). Intent analysis: routine descriptions, functional area badges, complexity indicators, key states, outputs, expandable safety dependencies. Program-level safety features. Click any routine to view its full source — ladder logic rung text with syntax highlighting (XIC/XIO, OTE/OTL, TON/CTU, JSR, MOV/EQU), line numbers, search within routine, and copy-to-clipboard. |
| History | Version timeline with date, engineer, confidence badge, summary. Inline change deltas: +/- tags (green/red), ~modified (amber), +/- modules, +/- rungs. Click to expand and see actual tag/module names. |
| Scorecard | 4-dimension quality score (Complexity, Safety, Reuse, Documentation) with progress bars, overall score, individual findings with confidence levels. Null-safe rendering — displays available dimensions even if MCP returns partial data. |
| I/O Verify | Cross-references PLC modules with project BOM. Two-pass matching (description substring + parts search fallback). Shows matched/unmatched modules, BOM items not in PLC, spare parts summary with stock status (None/Low/OK). CSV export. |
| Compare | Side-by-side comparison with another project: controller diff, tag/module/program counts, tags unique to each, summary. |
| Similar | Find projects with the same machine type or customer. Shows ranked list with job number, description, machine type, and status. Uses find_similar_machines MCP tool. |
Controller summary bar (shown on all sub-tabs): Processor type, firmware version, source file, creation date, last modified date, safety program badge (green if present, amber if missing), security badge (if secured).
Files: src/components/plc/ — PlcFleetPage.tsx, PlcTab.tsx, ModuleInventory.tsx, TagBrowser.tsx, ProgramTree.tsx, VersionTimeline.tsx, Scorecard.tsx, IoVerify.tsx, ProjectCompare.tsx
Data source: PLC Fleet Archive via MCP Server at PS-PROXY:3100. Analysis JSONs generated nightly from K: drive ACD files. See PLC Code Intelligence for the full vision.
Machine Explorer
Top-level nav page (#machine / #machine/{jobNumber}) designed for CS managers and field service engineers servicing machines post-ship. Split-view layout with persistent AI chat panel on the right.
Target users: CS managers on customer calls, field service engineers preparing for site visits, spare parts/aftermarket support. Not primarily for in-build project management.
Layout: Split view — machine content area (left, ~65%) with header + lifecycle timeline + 5 tabs. Persistent AI chat panel (right, ~35%) auto-scoped to the current machine, powered by Azure OpenAI + MCP tools.
Navigation: Enter a job number (e.g. 2399) for direct navigation, an S-prefixed serial number (e.g. S3501) to look up a machine by serial, or any free-text query (customer, description, quote) to search. The S prefix is required for serial lookup because job and serial numbers overlap in the 2400–3600 range — bare numerics are always treated as job numbers. Deep-link URLs: #machine/{job}/{tab} where tab is one of: overview, active-work, service-history, parts-bom, controls. Browser back/forward preserves tab state. (A stale .../drawings bookmark falls through the tab check and lands on the default tab rather than breaking — drawings are served by the viewer at Tools → Drawings.)
Header: Machine photo (from MultiMedia + K: drive, click to open fullscreen gallery), then two prominent identity blocks side-by-side — PROJECT {job} and MACHINE S{serial} — followed by customer, description, status badge, warranty status, robot badge, machine type, ship date (formatted), team members, quick links (copy K: drive path, copy video path, BOM link). The 4–5 digit number is the project number; the actual machine is identified by its serial (rendered with an S prefix). AI executive summary (“What is this machine?”) below the header.
Lifecycle timeline: Horizontal bar: Order → Engineering → Manufacturing → Ship → Field Service. Uses department P2/P98 dates from Azure SQL + project info dates. Current phase highlighted.
Photo gallery: Near-fullscreen lightbox with carousel navigation, keyboard arrows, photo counter, source labels. Photos aggregated from two sources: MultiMedia Lo Res Project Files (\\ad.ptihome.com\DFS\DATA\MultiMedia\Photo\Lo Res Project Files\{job}) and K: drive (K:\PROJECT\{job}\photos\).
AI Chat Panel: Persistent right panel reusing chat-engine.js backend. Machine context (job, customer, type, serial, team) injected as system message on first query. Suggested question chips for CS use cases. Rich responses with clickable links to parts, drawings, RFCs. Collapsible. Chat history persists across tab switches.
Tabs (5 total):
| Tab | Data Source | What It Shows |
|---|---|---|
| Overview | get_ai_assessment + get_rfcs (MCP) + Azure SQL handoff_extractions + machine_dna_features + lessons_learned + list_project_files | “What is this machine?” AI summary, Watch Out / known issues & risks, Resources grid (design review video, vendor manuals, drawings, K: drive), Quick Stats (RFCs, open issues, resolved, WOs), Lessons Learned |
| Active Work | get_rfcs (MCP, filtered to open) + Azure SQL purchase_orders | Open RFCs / service issues (cards with severity, age, expandable detail), Open Quotes (placeholder for future), Open Purchase Orders (table) |
| Service History | get_rfcs (MCP) + Azure SQL redbooks + ecn_detail + ncn_detail | Open items section (prominent red cards at top), Summary bar (total issues, quality cost, avg resolution), Searchable/filterable closed history timeline merging RFCs + Redbooks + ECNs + NCNs |
| Parts & BOM | get_bom + get_part_details + get_part_manufacturer + get_part_vendors + get_inventory_status + get_drawing_metadata + assess_obsolescence_risk (MCP) | Full hierarchical BOM viewer (the shared BomViewer — same tree + grid as Tools → BOM), part detail panel (manufacturer, vendor, inventory with serial numbers, engineering files, View Drawing into the viewer), obsolescence risk (real ERP INC/OBS flags), spare parts history |
| Controls | get_plc_analysis + get_ai_assessment (MCP) | Full PlcTab — modules, tags, programs, history, scorecard, I/O verify, compare, similar. Fault catalog and safety assessment moved here from former Quality tab |
Backend:
server/machine-routes.js— 39 Express routes proxying MCP tools (original 13 + Phase 5 diagnostics/tracing/photos)server/analytics-routes.js— 11 Azure SQL routes queryingPSI_Analytics(labor, departments, workforce, redbooks, ECNs, NCNs, Machine DNA, purchase orders, change orders, handoff, budget vs actual)server/analytics-db.js— Azure SQL connection via Azure AD managed identity (no credentials needed)server/chat-engine.js— Extended withmachineContextparameter for scoped conversationsserver/chat-routes.js—POST /api/chataccepts optionalmachineContextobject
Frontend files:
src/components/machine/— MachineExplorerPage, MachineSearch, MachineHeader, PhotoGallery, MachineChatPanel, OverviewTab, ActiveWorkTab, ServiceHistoryTab, ControlsTab, SupplyChainTab, LifecycleTimelinesrc/components/shared/ErrorBoundary.tsx— React error boundary wrapping each tabsrc/hooks/useMachine.ts— 32 React Query hooks (lazy-loaded per active tab)src/hooks/useAnalytics.ts— 11 React Query hooks for Azure SQL analyticssrc/services/machineApi.ts— typed API client (38 endpoints)src/services/analyticsApi.ts— typed API client (11 endpoints)src/services/chat.ts— SSE streaming chat client with machineContext supportsrc/types/machine.ts— TypeScript interfaces (MachineTab: 5 tabs)src/lib/dateUtils.ts— shared date formatting (“Dec 5, 2024” format)src/lib/navigation.ts— programmatic navigation utility
Testing:
src/components/machine/__tests__/null-safety.test.tsx— 24 tests verifying all components render without crashing when data is undefined/null/partial. Runs in CI on every push.
Removed tabs (files retained in codebase, accessible via AI chat panel):
- Quality → replaced by Service History + Controls (fault catalog/safety moved)
- Manufacturing → data available via AI panel
- Commissioning → data folded into Overview (risks, handoff) + AI panel
- Fleet Context → data available via AI panel
PC Fleet Dashboard
Top-level nav page (#pc-fleet) for tracking Dell PCs deployed inside PSI machines running PRIMS/CITS HMI software. Accessed via the Monitor icon in the left navigation bar.
What it tracks: 640+ PCs across the fleet (619 with Dell service tags), joined with project data to show customer and machine context alongside PC hardware status.
Two views:
- Machine-centric view (default) — groups PCs by project, shows PC replacement timeline (original → retrofit replacements), highlights current vs. replaced PCs
- Flat view — every PC as a sortable row for fleet-wide analysis
Key features:
- Age color coding (green <5yr, amber 5-8yr, red 8-10yr, dark red >10yr)
- Windows version badges (Win 11/10/7/XP with color severity)
- Dell warranty status with dates and service levels
- Text search, OS filter, warranty filter, age range filter
- Summary cards (current PC count, warranty breakdown, legacy OS count, average age)
- Generate Upgrade Letter — personalized customer template for PC replacement proposals
Data flow: PRIMS Installations spreadsheet → load_prims_standalone.py → psi_ingest.db → Dell Command Warranty enrichment → publish_ingest_tables.py → psi_analytics.db passthrough copy → sync_table_to_azure.py → Azure SQL → /api/analytics/pc-fleet endpoint → React page.
Production source of truth: PSI Explorer’s analytics routes query Azure SQL PSI_Analytics directly. The SQLite files on PS-PROXY (psi_ingest.db, psi_analytics.db) are upstream pipeline stages used to build and publish that Azure SQL data, not the app’s live database.
Files: src/components/pc-fleet/PcFleetPage.tsx, server/analytics-routes.js (pc-fleet endpoint)
Full documentation: PC Fleet Management
Drawings Viewer (Web ProViewer)
The browser replacement for the desktop PSI.ProViewer. A part-number-driven,
multi-format viewer at Tools → Drawings, deep-linkable via #viewer/{pn}.
Formats (left sidebar; first available is auto-selected in order PDF → Vendor
Manual → SW Drawing → SW 3D → DWG → …): PDF, Vendor Manual, TIFF, SolidWorks
3D models (.sldprt/.sldasm) and 2D drawings (.slddrw) via the
eDrawings/HOOPS SCS WebViewer (sheets, layers, measure), and AutoCAD DWG
parsed in-browser (R13+; self-hosted font pack; legacy CimCAD R11/12 falls back to
PDF). CAD files resolve from \\ad.ptihome.com\DFS\CAD including the legacy
cimcad\dwg archive.
Vendor manuals (2026-07-22). Purchased/vendor-manual parts (GT code PU/VM,
the green shopping-cart items in the BOM — e.g. 074722) are PDFs kept on a different
DFS root, \\ad.ptihome.com\DFS\Data\Dept\Eng\Vendor Manuals (S:), not the CAD tree.
The MCP indexes that folder (findVendorManual, 5-min cache; tolerates mixed-case
extensions and trailing vendor suffixes like 020764VT2196HO.pdf) and surfaces a
Vendor Manual format that the viewer renders inline as a fit-to-width PDF. Before
this, those parts showed in the BOM but “View Drawing” found nothing.
Conversion pipeline (on PS-PLCRunner, not PS-PROXY). eDrawings COM only runs in
an interactive desktop session, so conversion lives on PS-PLCRunner (eDrawings
2026 + SOLIDWORKS Document Manager). Two paths, both writing SCS to DFS\CAD\SCS
(model) / SCS\drw (drawing):
- Nightly pre-generation (
PSI_SCS_Pregen_Nightly, 20:00 with a 12-hour budget — it converts until 08:00, logsTime budget exhausted, and resumes where coverage left off the next night; the task runs asadministratorwithLogonType: Interactive, so it only starts while that account is logged on — disconnect from RDP, never sign out. Assemblies→drawings→parts, recent-first, skip-current, quarantine at 3 attempts). - On-demand via a DFS drop-file queue — the MCP writes
SCS/requests/{kind}_{pn}.req, theWatch-ScsRequests.ps1watcher on PLCRunner converts, the MCP long-polls the cache. - Both stage the SCS locally then copy to DFS (writing eDrawings’ many small stream files straight to the share throws intermittent “Error writing file / 502”).
Fast SCS transfer (2026-07-17). Baseline was 221 MB of base64 for a 165 MB model over the Azure VPN hairpin (~30 s, blank-spinner). Three compounding layers:
- Compression — the pipeline pre-generates
{pn}.scs.gz(.NET GZipStream) and{pn}.scs.zst(zstd -19;zstd.exeprovisioned by the exporter’s CI deploy) for any SCS ≥ 40 MB. The MCP’s binary endpointGET /files/drawing/:pn/scsnegotiatesContent-EncodingfromAccept-Encoding— zstd for modern Edge/Chrome, gzip fallback, raw otherwise. (~165 MB → ~65 MB zstd.) - Binary / skip-base64 — the client fetches the binary endpoint (browser decompresses
transparently) and re-encodes to base64 only for the eDrawings shell;
X-Uncompressed-Lengthdrives the progress bar. The Explorer proxy is a rawhttppipe (notfetch, which would auto-inflate and strip the compression on the Azure→browser leg). - Direct browser→PS-PROXY for on-LAN clients (97% of use) — bypasses the Azure hop.
The Explorer backend (
GET /api/drawing/:pn/scs-url) checks the client IP againstSCS_ONNET_CIDRS; on-net it returns a short-lived signed capability URL to the MCP’s HTTPS listener —sig = HMAC-SHA256(SCS_SIGN_KEY, "{pn}|{type}|{exp}"), TTL 5 min — whichlib/auth.jsaccepts in lieu of an Entra bearer. Off-net (or any direct failure) falls back to the proxied path. The secret never reaches the browser (trusted-issuer model, mirroring the MCP↔UniDataX-On-Behalf-Ofdelegation).
Result: a full facility assembly (339200, 165 MB SCS) loads in ~3.6 s end-to-end on-LAN, down from ~30 s (~8×).
Other viewer behaviour:
- “View Drawing” opens a new tab (from a BOM part card) — so jumping to a drawing keeps the BOM’s expanded tree + scroll position in the original tab instead of swapping it away.
- Progress + phases: checking → preparing (first-time conversion, elapsed timer) → downloading (%/MB) → building 3D scene → ready.
- GPU-reset recovery: a big HOOPS scene can exhaust the shared GPU process, blanking the
next WebGL viewer (drawing or DWG). On unmount the SolidWorks iframe navigates to
about:blank(frees the scene); a Reset control (bottom-right) + a build watchdog recover without a page refresh; WebGL is detected up front (RDP/VDI → “use PDF”). - Stats panel (“for nerds”, bottom-left) +
[SCS-PERF]console log /window.__scsPerf: per-load method (direct/proxied), host, encoding, wire vs raw MB, MB/s, download/render/total ms.
Config (direct-connect): MCP HTTPS listener on :3443 (*.progressivesurface.com wildcard
PFX injected via the SCS_TLS_PFX_B64 GH secret — PS-PROXY is on-prem and ps-certificates-kv is
firewalled, so it can’t self-fetch); shared SCS_SIGN_KEY (GH secret on psi-machine-mcp +
App Service setting on bom-explorer-web); SCS_ONNET_CIDRS, SCS_DIRECT_BASE; MCP_CORS_ORIGINS
includes the Explorer origin; firewall :3443 scoped to private ranges.
Related repos: PSI.UniData.API is unrelated here; the viewer’s backend is the
psi-machine MCP (ps-proxy:3100 HTTP / :3443 HTTPS) and the psi-edrawings-exporter
pipeline on PS-PLCRunner.
Files: src/components/viewer/{ViewerPage,SolidWorksViewer,DwgViewer}.tsx,
server/machine-routes.js (/api/drawing/*), public/viewer/index.html (eDrawings shell).
Tech Stack
| Technology | Purpose |
|---|---|
| React 18 | UI framework |
| Vite | Build tool and dev server |
| TypeScript | Type safety |
| TailwindCSS | Styling |
| React Query | Data fetching and caching |
| MSAL React | Azure AD authentication |
| Lucide React | Icons |
| Express.js | Production server |
| openai | Azure OpenAI GPT 5.2 (chat engine, primary) |
| @anthropic-ai/foundry-sdk | Claude API via Azure AI Foundry (chat engine, alternative) |
| @modelcontextprotocol/sdk | MCP client for PS-PROXY tool access |
Project Structure
psi-explorer-web/
├── package.json
├── server.js # Express production server (+ chat routes + MCP init)
├── vite.config.ts # Dev server, proxy config
├── tailwind.config.js # TailwindCSS theme
├── tsconfig.json
├── .env.example # Environment template
├── .env.production # Production API config
├── index.html
├── server/
│ ├── mcp-client.js # MCP connection to PS-PROXY:3100
│ ├── chat-engine.js # Agentic loop (Azure OpenAI / Claude + MCP)
│ ├── chat-routes.js # Express routes for /api/chat
│ ├── plc-routes.js # Express routes for /api/plc (MCP proxy)
│ ├── machine-routes.js # Express routes for /api/machine (26 MCP proxies)
│ ├── analytics-routes.js # Express routes for /api/analytics (10 Azure SQL routes)
│ └── analytics-db.js # Azure SQL connection pool (PSI_Analytics)
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Main application (+ ChatPanel)
│ ├── index.css # Tailwind imports
│ ├── vite-env.d.ts # Environment types
│ ├── components/
│ │ ├── BomTree.tsx # Hierarchical tree view
│ │ ├── BomTable.tsx # Flat table with sorting
│ │ ├── BomStats.tsx # Summary statistics
│ │ ├── JobSearch.tsx # Search input
│ │ ├── PartCard.tsx # Part detail panel
│ │ ├── chat/
│ │ │ ├── ChatPanel.tsx # Floating panel (toggle/Ctrl+K)
│ │ │ ├── ChatInput.tsx # Text input + send
│ │ │ ├── ChatMessage.tsx # Message bubbles + markdown
│ │ │ ├── ToolResultCard.tsx # Rich tool result cards
│ │ │ └── SuggestedQuestions.tsx # Context-aware question chips
│ │ ├── machine/
│ │ │ ├── MachineExplorerPage.tsx # Page shell (search + detail + tabs)
│ │ │ ├── MachineSearch.tsx # Search + recent machines
│ │ │ ├── MachineHeader.tsx # Identity banner
│ │ │ ├── OverviewTab.tsx # Dashboard cards + scorecard + Machine DNA
│ │ │ ├── ControlsTab.tsx # PlcTab reuse wrapper
│ │ │ ├── QualityTab.tsx # RFCs + AI assessment + Redbooks/ECNs/NCNs
│ │ │ ├── ManufacturingTab.tsx # Labor charts + dept summary + workforce
│ │ │ ├── SupplyChainTab.tsx # BOM + part details + obsolescence
│ │ │ ├── CommissioningTab.tsx # Readiness + AI checklist + handoff
│ │ │ └── FleetTab.tsx # Lineage + siblings + comparison + radar
│ │ └── plc/
│ │ ├── PlcFleetPage.tsx # Fleet dashboard + project drill-down
│ │ ├── PlcTab.tsx # Per-project sub-tab orchestrator
│ │ ├── ModuleInventory.tsx # Modules table (health, risk, FW)
│ │ ├── TagBrowser.tsx # Tag table (search, type filter, used-by)
│ │ ├── ProgramTree.tsx # Program tree (intent, search, safety)
│ │ ├── VersionTimeline.tsx # History timeline (change deltas)
│ │ ├── Scorecard.tsx # Quality scorecard display
│ │ ├── IoVerify.tsx # BOM ↔ PLC module matching
│ │ └── ProjectCompare.tsx # Cross-project comparison
│ ├── hooks/
│ │ ├── useBom.ts # BOM React Query hooks
│ │ ├── usePlc.ts # PLC React Query hooks (analysis, history, changes, compare, fleet)
│ │ ├── useMachine.ts # Machine Explorer React Query hooks (20 hooks)
│ │ ├── useAnalytics.ts # Analytics React Query hooks (10 hooks, Azure SQL)
│ │ ├── useBomModuleMatch.ts # BOM ↔ PLC module matching hook
│ │ └── useChat.ts # Chat state + localStorage persistence
│ ├── lib/
│ │ ├── bomModuleMatch.ts # Pure BOM ↔ module matching logic
│ │ ├── plcExportUtils.ts # CSV export for PLC tables (modules, tags, scorecards, I/O verify)
│ │ ├── plcScorecard.ts # Scorecard scoring algorithms
│ │ └── navigation.ts # Programmatic nav utility (navigateToMachine)
│ ├── services/
│ │ ├── api.ts # REST API client (BOM + parts)
│ │ ├── plcApi.ts # PLC API client (analysis, history, fleet, intent)
│ │ ├── machineApi.ts # Machine Explorer API client (26 endpoints)
│ │ ├── analyticsApi.ts # Analytics API client (10 endpoints, Azure SQL)
│ │ └── chat.ts # SSE streaming client for /api/chat
│ ├── auth/
│ │ └── msalConfig.ts # Azure AD config
│ └── types/
│ ├── bom.ts # BOM TypeScript interfaces
│ ├── plc.ts # PLC TypeScript interfaces (analysis, history, scorecard, intent)
│ ├── machine.ts # Machine Explorer interfaces (info, summary, assessment, rfcs, etc.)
│ ├── analytics.ts # Analytics interfaces (labor, redbooks, ECNs, NCNs, DNA, etc.)
│ └── chat.ts # Chat TypeScript interfaces
└── dist/ # Build output (served by Express)
Running Locally
Prerequisites
- Node.js 18+
- PSI.UniData.API running on port 5000 (or use production API)
Install and Run (Development)
cd C:\GIT\psi-explorer-web
# Install dependencies
npm install
# Start dev server (with Vite proxy)
npm run devAccess at http://localhost:5173
Run Production Build Locally
# Build the app
npm run build
# Start Express server
npm startAccess at http://localhost:8080
Network Access
To access from other machines in development:
- Set
host: truein vite.config.ts (already configured) - Add machine names to
allowedHosts - Run
npm run dev - Access via
http://PCNAME:5173
Key Components
server.js (Production Server)
Express server for Azure App Service deployment. Initializes MCP client at startup and registers chat API routes:
import express from "express"
import { connect as connectMcp } from "./server/mcp-client.js"
import chatRoutes from "./server/chat-routes.js"
const app = express()
app.use(express.json())
app.use(express.static("dist"))
app.use(chatRoutes) // /api/chat (POST), /api/chat/tools (GET)
// Health check, SPA fallback...
async function start() {
await connectMcp() // Connect to PS-PROXY:3100/mcp
app.listen(PORT)
}
start()BomTree.tsx
Recursive tree component with expand/collapse:
function BomTreeItem({ node, onSelectPart, depth }) {
const [expanded, setExpanded] = useState(node.expanded)
return (
<div style={{ paddingLeft: depth * 20 }}>
<div onClick={() => onSelectPart(node.partNumber)}>
{node.hasChildren && <ChevronIcon expanded={expanded} />}
{getGTCodeIcon(node.gtCode)}
<span>{node.partNumber}</span>
<span>{node.description}</span>
</div>
{expanded &&
node.children.map((child) => (
<BomTreeItem key={child.wbsNumber} node={child} depth={depth + 1} />
))}
</div>
)
}useBom.ts
React Query hooks for data fetching:
export function useBomByJob(jobNumber: string | null) {
return useQuery({
queryKey: ["bom", "job", jobNumber],
queryFn: async () => {
const response = await bomApi.getByJob(jobNumber)
return {
...response,
tree: buildBomTree(response.items), // Convert flat to tree
}
},
enabled: !!jobNumber,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
})
}api.ts
API client with dev/prod endpoint switching:
const API_BASE_URL = import.meta.env.VITE_API_URL || "/api"
const USE_DEV_ENDPOINTS = import.meta.env.VITE_USE_DEV_ENDPOINTS === "true" || true
export const bomApi = {
getByJob: async (jobNumber: string): Promise<BomResponse> => {
const endpoint = USE_DEV_ENDPOINTS ? `/bom/dev/job/${jobNumber}` : `/bom/job/${jobNumber}`
return fetchApi(endpoint)
},
}Data Types
BomItem
interface BomItem {
level: number
wbsNumber: string
parentPartNumber: string | null
partNumber: string
description: string
gtCode: string
quantity: number
onHand: number
onOrder: number
available: number
hasChildren: boolean
isPurchased: boolean
isManufactured: boolean
jobNumber: string
}BomTreeNode
interface BomTreeNode extends BomItem {
children: BomTreeNode[]
expanded: boolean
}Building for Production
npm run buildOutput in dist/ folder. The postinstall script automatically runs the build.
Package Scripts
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"start": "node server.js",
"postinstall": "npm run build"
}
}GT Code Colors
| GT Code | Type | Color |
|---|---|---|
| PU, VM | Purchased | Green |
| FA, MO | Manufactured | Blue |
| AS | Assembly | Purple |
| Other | - | Gray |
Screenshots
Tree View
▼ 841782 - Torit Dust Coll. Retrofit Qty: 1 [RETROFIT]
▼ 806038 - W/o 95188 Retrofit Electrical Qty: 1 [RETROFIT]
▸ 003419 - 800mr-pt16rs Pilot Light Qty: 1 [LT]
▸ 003054 - 700-p400-a1 Relay 4p 120vac Qty: 1 [RELAY]
▸ 005756 - V130la10a Varistor Qty: 1 [VARISTOR]
Statistics Bar
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 📦 40 │ │ 📊 2 │ │ ⏱️ 0.2s │ │ 🔗 16 │
│ Total Parts│ │ Max Depth │ │ Load Time │ │ DB Calls │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
Lead Time Analysis
Root cause analysis module for understanding why PSI lead times increased 50-68% since 2022. Accessible via the “Lead Time” tab. Analyzes 216+ data columns across 2,569 projects spanning 24 years (2001–2025), including P2/P98/P50 substantial engagement milestones, department idle time, engineering cascade effects, and ECN-driven complexity metrics.
Key Findings
The deep-dive analysis (March 2026) identified these root causes:
- Idle time is the real problem: Work hours grew 25% but calendar time grew 68%. The gap is idle time — departments waiting for upstream work, materials, or decisions. Engineering and Machine Shop are 89-91% idle.
- Engineering cascade proven: Engineering completion (P98) drives downstream department start (P2) with r=0.70–0.83 correlation. Late engineering finish cascades through the entire project.
- ECN as complexity proxy: ECN count is the strongest single predictor of engineering hours (r=0.72) and quality rework (r=0.82). High-ECN projects take 20% longer and consume 54% more labor.
- Vendor lead times doubled: Work order cycle times went from 65 to 117 days.
- Quote inflation masks the problem: Planned lead times grew +90 days, making OTD% unreliable as a root cause metric.
Eras
Projects are grouped into business eras for comparison:
| Era | Years | Description |
|---|---|---|
| Baseline | 2017–2021 | Steady-state operations |
| Overload | 2022–2023 | Too many concurrent projects |
| Current | 2024–present | Post-correction period |
Views (8 tabs)
| View | Description |
|---|---|
| Root Causes | Headline idle time analysis: work vs idle stacked bars per era, department queue ratio cards (8 depts), engineering cascade correlation badges, ECN trend summary, era comparison stats |
| Explorer | Sortable/filterable project table with human-readable column names, CSV export, click-to-detail popup |
| Correlation | Top 25 metrics correlated with Days Late (or Lead Time / On Time), with scatter plot + plain-English explanation panel and correlation strength badge |
| Era Comparison | Side-by-side era cards with delta-vs-baseline indicators for 13 metrics including idle %, work hours, P2P98 spans, WO cycle time, and ECN count |
| Dept Heatmap | Year-over-year heatmap for 8 departments with 6 metric options: Budget %, Hours, Span, P2P98 Span, Idle %, ECN Count. Color logic varies by metric (absolute thresholds, YoY delta, percentile). |
| Dept Flow | Engineering cascade visualization: upstream→downstream scatter plots (4 pairs), “Everything Starts Later” table, engineering span growth charts, ECN impact on cascade, capacity vs OTD (de-emphasized) |
| Findings | 11 data-driven findings ranked by evidence strength (Definitive / Strong / Contributing / Contradicted), with leadership-friendly expanded cards (What we think / How we tested / What the data shows / Uncertainty / Recommendations) |
| Timeline | Summary mode: median dept activity bars per ship year. Individual mode: Gantt-style per-project bars with P2/P98 substantial engagement (solid core) and First/Last tails (faded), P50 midpoint markers, idle % tooltips |
P2/P98/P50 Milestones
Department engagement dates computed from daily labor hours (tslabor2):
| Milestone | Definition |
|---|---|
| P2 (Substantial Start) | Date when 2% of cumulative labor hours reached — trims early check-ins |
| P50 (Work Midpoint) | Date when 50% of cumulative labor hours reached |
| P98 (Substantial End) | Date when 98% of cumulative labor hours reached — trims late punch-list entries |
| Queue Ratio | P2P98 calendar span ÷ (total hours / 8). Ratio of 1.0 = working every day; 10x = 90% idle |
Key UI Features
- Human-readable labels: All 216+ raw CSV column names mapped to plain English
- View descriptions: Each tab shows a plain-English explanation of what the view shows
- Expandable glossary: Key terms (P2/P98, Queue Ratio, Idle %, Cascade, OTD, ECN, etc.) defined inline
- Era year ranges: Every era chip, card, and label shows the year range
- Filter bar: Era toggle chips, search by job/customer, OTD filter, internal project toggle
- OTD de-emphasis: OTD % is shown but flagged as unreliable for root cause analysis due to quote inflation (Finding F11)
- Graceful degradation: Views fall back to First/Last dates when P2/P98 data is unavailable
Data Pipeline
Project data is built by build_comprehensive_dataset.py (Python, runs nightly at 1 AM ET on PS-PROXY) from UniData tables including tslabor2, purchaseorders, pohist, bom_receipts, gantt_job_revisions, ecn data, and redbook records. Output: \\ad.ptihome.com\DFS\Schedule\SS123\LEADTIME\comprehensive_dataset.csv (2,569 rows × 216+ columns). Served via PSI.UniData.API (/api/leadtime/dev/projects and /api/leadtime/dev/scorecard) with 5-minute cache and auto-refresh on file change.
Gantt Charts
Job-level Gantt analytics page accessible via the “Gantt” tab (#gantt). Replaces a monolithic 36K-line vanilla JS page with modular React components. Covers 2,327 jobs (1,329 new builds, 998 retrofits) with weekly labor data by work center, section-level aggregation, and Goldratt TOC velocity metrics.
Views
| View | Description |
|---|---|
| Job Table | 13-column sortable table (Job, Description, Actual/Quoted hours, Ship, BL Ship, Redbook, ECN, WOs, Revenue, Throughput, T/hr). Type filter (All/New Builds/Retrofits/Active), year filter tabs, search. Checkbox selection for multi-job compare. |
| Overview | Drill-down heatmap (Year > Quarter > Month > Week) with section/operation rows. RGBA color-scaled cells. Breadcrumb navigation + zoom buttons. Velocity strip (8 TOC KPI cards: WIP, Throughput Rate, Implied Lead Time, Avg Cycle Time, WIP Hours, Revenue, Throughput, Avg T/hr). Recharts velocity chart (shipped/WIP/new starts). Click any column header for period detail overlay with capacity bars and job breakdown. |
| Compare | Multi-job comparison heatmap. Select 2+ jobs via checkboxes, then compare. Bucket tabs (TOTAL + per-section). Two modes: by-job (one row per job) and by-op (hierarchical with work center sub-rows). 15-color cycling palette. Month span headers. |
Single Job Viewer
Click any job row to open the full Gantt viewer. Replaces the former standalone viewer.html.
| Component | Description |
|---|---|
| Stats Bar | Actual/Planned/Quoted hours, WO count, weeks, work centers, peak IP inventory, redbook count, ECN count, revenue, throughput, T/hr, date range, order/ship/BL ship dates |
| Handoff Pills | Section-to-section handoff indicators (overlap/gap/direct/incomplete) with colored dots |
| Gantt Grid | Weeks x operations table with RGBA heatmap cells, section headers, milestone markers, redbook week tinting, cutoff/overrun dimming. 4 sticky left columns (Work Center, Actual, Plan, Quoted). Inventory GL bucket rows, handoff rows, redbook/ECN rows, total row. |
| Popups (5 types) | Employee: expandable rows with WO breakdown + cross-job links + time off. Redbook: issues with ID, date, type, problem/cause/solution. ECN: parts with status badges, dates, hours. Inventory: parts list with BOM level badges (L0-L5) and dollar amounts. Handoff: from/to section comparison with overlap/gap status and per-week detail. |
Data Source
Pre-generated JSON files from batch_gantt.py (built nightly by Python scripts in C:\GIT\JobPlanningBom\scripts\gantt\). Data is uploaded to Azure Blob Storage (psiganttdata storage account, gantt-data container) via CI workflow (hourly Mon-Fri during business hours) and manual scripts/upload-gantt-data.ps1. The Express server reads from blob storage first (via @azure/storage-blob SDK, 5-minute cache), falling back to local filesystem. Jobs summary served via /api/gantt/jobs, per-job detail via /api/gantt/job/{number}. In development, Vite middleware serves files directly from ./gantt-data/ (populated by scripts/sync-gantt-data.mjs).
Files
src/components/gantt/— GanttPage, GanttStatsBar, GanttFilters, JobTable, OverviewPanel, OverviewBreadcrumb, OverviewHeatmap, VelocityStrip, VelocityChart, PeriodDetailOverlay, ComparePanel, CompareHeatmap, CompareLegend, JobViewer, JobViewerStats, JobViewerTable, JobViewerPopups (17 components)src/hooks/useGantt.ts— React Query hooks (useGanttJobs, useGanttJobDetail)src/lib/ganttUtils.ts— Pure utility functions (filtering, sorting, aggregation, velocity, compare)src/types/gantt.ts— TypeScript interfaces (GanttJobSummary, GanttJobDetail, 25+ sub-types)
Recent Changes
- 2026-08-17 - The web BOM was showing sub-assemblies at the root that the desktop viewer nests.
buildBomTreederived each row’s parent by slicing two characters off its WBS number and stripping a trailing zero, then pushed the row to the root whenever that guess produced a WBS that didn’t exist — so the breakage was invisible: the tree rendered, the row count matched, and whole sub-assemblies simply appeared at the top level. Three ordinary AFTEC cases break the slice. (1) The trailing-zero strip is wrong: a parent that is the 10th/20th/30th… child of its own parent has a WBS ending in0(001+10=00110), so slicing its child0011001gives00110, which the regex mangles to the nonexistent0011— every descendant of every tenth/twentieth/… child was orphaned. (2) Segments are not always two digits: past 99 children they overflow to three, so001010113100is001010113+100, not0010101131+00. (3)parentPartNumberoutranks WBS nesting — cut-length / detail lines (“X 48-1/2” LG”) sit one WBS level below the stock item they detail but report the grandparent as their parent, making them siblings of that stock item, which is how the desktop viewer draws them. Replaced the slice with a prefix walk that takes the nearest ancestor whose part number equalsparentPartNumber(nearest-match wins even when a part repeats up the chain); an unresolvable parent now falls back to the nearest ancestor by prefix instead of becoming a root. Production roots before → after: job 2399 41→1, 2386 18→1, 2337 130→1, 2224 166→1, with row counts unchanged. Auto-expand was broken by the same data quirk — it keyed offitem.level, which the API reports as 2 for every non-root row, so “expand the first two levels” expanded only the root; depth now comes from the assembled tree. Also added the View Drawing action to the Parts & BOM part detail panel, which previously showed the Engineering Files checklist (green/grey dots saying a drawing exists) with no way to open it — it now resolves the target exactly as the Tools → BOM part card does (drawingNumberwhen present, else the part number) so both surfaces land on the same document. (bom-explorer-web #105) - 2026-08-03 - Machine Explorer went blank fleet-wide — the MCP bearer was never refreshed.
server/mcp-client.jsminted the Entra token once at startup and baked it into the MCP transport’srequestInit.headers, which is read at construction. Entra tokens last ~24h and the App Service process runs for days, so every/api/machine/*call eventually returned 500 with{"error":"invalid_token","error_description":"jwt expired"}and every panel on the machine page rendered empty. The psi-machine MCP server was healthy throughout — it was correctly rejecting a stale token. Two things made it permanent and invisible: the auto-reconnect classifier matched onlysession/ECONNREFUSED/ECONNRESET, so an auth rejection never triggered a retry (the app stayed broken until manually restarted), and/healthderived MCP status from “does a client object exist”, so it advertised"mcp":"connected"for the entire outage. Fixed by minting the bearer per request via the transport’sfetchhook (the credential caches internally — same patternanalytics-db.jsalready used for the Azure SQL token), widening the classifier to coverinvalid_token/jwt expired/401, and making/healthdo a real 30s-cachedlistTools()that reportsstatus: "degraded"+mcpError. Operational note: if machine data goes blank again, check/healthfirst — it now tells the truth. Also fixed two PSI_Analytics routes querying non-existent columns (handoff_extractionshas noproject,change_ordershas nochange_date— both key onjob), and added the first-ever test coverage forserver/(18 tests). (bom-explorer-web #90) - 2026-08-03 - PSI_Analytics job numbers are not stored uniformly — know which helper to use. Tables loaded straight from AFTEC CSV exports (
labor_detail,purchase_orders) keep AFTEC’s zero-padded job (002396) → usepadJob(). Tables the pipeline derives from its ownprojectstable (handoff_extractions,change_orders, and probablydepartment_summary/workforce_features/machine_dna_features) key onprojects.job, whichpsi-data-pipeline/pipeline/build_analytics_db.pywrites with.lstrip('0')(2396) → useunpaddedJob(). Getting this wrong fails silently: the query is valid and simply matches zero rows, so the panel renders empty with no error, no 500, and nothing in the logs. Audit of the remaining routes tracked in bom-explorer-web#91. - 2026-07-21 - Remote access:
explorer.progressivesurface.comis now reachable off-VPN / mobile via Azure App Proxy — clientless, no full-VPN requirement to use the app. The internal UniData API is never internet-exposed: the browser calls the Explorer origin same-origin for every request andserver.jsrelays to the API over the private network (VNet), so App Proxy publishes only Explorer. App Proxy uses passthrough pre-auth — the SPA does its own Entra MSAL sign-in; App-Proxy pre-auth caused a redirect loop because the SPA and pre-auth share app registration13d6930aand both return to the site root. One URL on-net and off (public DNSexplorer→ CNAME → the App Proxy edge; the DC still resolves it on-LAN, and the drawing SCS direct fast-path is unaffected). Wildcard*.progressivesurface.comcert (fromps-certificates-kv) on the App Proxy custom domain. Conditional Access “PSI Explorer — external access” gates it — compliant device + phishing-resistant MFA, excludesAllTrusted, mirrors the VPN policy’s VPN Authorized Users group; currently report-only, enforce after passkey rollout. App-registration gotcha: the reply URL must live only under the SPA platform — a duplicate under Web (added when App Proxy pre-auth is enabled) triggersAADSTS9002326(cross-origin token redemption blocked) and must be removed. Every UniData call now carries the SPA’s Entra ID-token bearer, readying the authenticated-endpoint cutover as the API’s/dev/*routes move to the sandbox server (flipVITE_USE_DEV_ENDPOINTS=false+REQUIRE_API_AUTH=truetogether). MSAL init also self-heals a stale cache (purge + reload once) instead of dead-ending at “Sign-in couldn’t start”. Cisco AnyConnect stays for network-level / thick-client access; Entra Private Access is the strategic VPN replacement. (bom-explorer-web #48, #53, #54, #56) - 2026-07-14 - Serial-first asset model + Fleet Home + embedded BOM viewer + PSI design system. The tool is now organized around the asset (serial number), not the project number: an “asset” aggregates its original build project plus all retrofits into one lifecycle. Canonical machine URL is
#asset/{key}(key =S{serial}or a project number);#machine/{job}remains a legacy alias that resolves and canonicalizes to the serial. The asset page (Machine Explorer) gained a lifecycle selector that re-scopes the per-project tabs to the chosen build/retrofit. A new Fleet Home dashboard is the landing page at the site root (global serial/job/customer search, fleet KPIs, recent assets, launcher tiles into Machine Explorer, Accounts & Contacts, BOM, Drawings, Reporting, PLC Fleet). The Parts & BOM tab now shows the full hierarchical BOM viewer (tree + sortable/filterable grid with Excel export) instead of the old flat 200-row table. First app to adopt the@progressivesurface/uidesign system (PSIKpi/PSITile/ps.css) and@progressivesurface/grid(PSIDataGrid). Resolution is frontend-only (serial↔project resolver over the existingby-serial+ lineage endpoints; no AFTEC/API re-key). - 2026-07-14 - Viewer full width (dropped the 1600px cap — drawings/models use the whole monitor). SCS pre-generation pipeline LIVE on PS-PLCRunner (
PSI_SCS_Pregen_Nightly, daily 21:00, 6-hour budget: assemblies → drawings → parts, recent-first, skip-current, failure quarantine;psi-edrawings-exporter/pipeline/). Two hard requirements discovered: eDrawings COM only converts in an interactive desktop session (the MCP’s on-demand tasks on PS-PROXY never actually ran in production — no logon session for the service account; all pre-2026-07 cached SCS came from dev workstations), and SOLIDWORKS Document Manager must be registered or eDrawings blocks every conversion behind a modal warning (installed on PS-PLCRunner). On-demand conversion on PS-PROXY remains inactive; nightly cache coverage is the primary path (bom-explorer-web#24). - 2026-07-13 - Drawings viewer dialed in: SolidWorks 2D drawings (.slddrw) now render (type-aware SCS conversion + separate
CAD\SCS\drwcache; eDrawings shell sheets/layers/measure verified on live drawings). DWG viewer hardened: 86-font CAD pack self-hosted inpublic/cad-data/fonts/(removed mlightcad.gitlab.io CDN dependency), viewer-singleton remount fix (blank canvas on part/format switch), legacy CimCAD R11/12 DWGs detected by version stamp with a clear use-the-PDF message. Drawing metadata falls back to the legacycimcad\dwgarchive (~165k DWGs). MCP-side: SCS generation no longer blocks the event loop (was freezing all MCP sessions up to 45 s per conversion), concurrent-safe task naming, zero-byte SCS guards. Long conversions supported:generate-scslong-polls then returnsgenerating; the viewer polls up to 12 min with a live elapsed timer and a size-aware “large assembly — this can take a few minutes” message (conversions run up to 10 min server-side). Batch pre-generation pipeline still tracked in bom-explorer-web#24. - 2026-03-19 - Gantt Charts: Full React rebuild of the 36K-line vanilla JS gantt page. 17 modular components replacing monolithic index.html + viewer.html. Job table with type/year/search filters and checkbox selection. Drill-down overview heatmap (Year>Quarter>Month>Week) with section/operation rows and RGBA color scaling. 8-card Goldratt TOC velocity strip + Recharts velocity chart (shipped/WIP/starts). Period detail overlay with capacity bars and WIP risk. Multi-job compare panel (by-job and by-op modes, 15-color palette). Full single-job viewer with stats badges, handoff pills, Gantt grid (weeks x ops), and 5 popup types (employee, redbook, ECN, inventory, handoff). React Query hooks with staleTime caching. Vite middleware plugin for dev-time JSON serving.
- 2026-03-16 - Machine Explorer Redesign: Transformed from engineering debug panel to CS/field service portal. Split-view layout with persistent AI chat panel (machine-scoped conversations via Azure OpenAI + 61 MCP tools). 6 CS-focused tabs replacing 8 engineer-focused tabs: Overview (“What is this machine?”, Watch Out risks, Resources, Lessons Learned), Active Work (open RFCs, open POs), Service History (open items prominent, searchable timeline), Parts & BOM, Drawings, Controls. Photo gallery lightbox from MultiMedia + K: drive (29 photos for job 2337). Lifecycle timeline (Order→Ship). Error boundaries on all tabs. 24 null-safety tests. Azure SQL managed identity auth (no credentials needed). 39 MCP backend routes, 11 analytics routes. Tab data badges. Date formatting throughout.
- 2026-03-12 - Machine Explorer Phase 1: New top-level
#machine/{job}page with 360° machine intelligence view. 13 Express backend routes proxying MCP tools. Search with recent history, header banner (identity, team, PLC stats), Overview tab (health cards, I/O, programs, modules, scorecard dimensions, findings, available data), Controls tab (full PlcTab reuse), Quality tab (RFCs + AI fault catalog + safety assessment + design quality). Hash-based deep linking. 5 placeholder tabs for future phases. - 2026-03-02 - Lead Time Deep Dive Revamp: Dashboard→Root Causes (idle time bars, queue ratio cards, cascade badges, ECN trends), Capacity&OTD→Dept Flow (cascade scatter plots, “Everything Starts Later” table, span growth, ECN impact), Hypotheses→Findings (11 findings with evidence grouping), P2/P98/P50 substantial engagement milestones on Timeline (three-segment bars + midpoint markers), Dept Heatmap (3 new metrics: P2P98 Span, Idle %, ECN Count), Era Comparison expanded to 13 metrics, OTD de-emphasized per F11 finding, data pipeline expanded to 216+ columns.
- 2026-03-03 - PLC bug fixes + routine source viewer: Scorecard crash fix (null guards when MCP returns partial data), click-to-view routine source code on Programs tab (full ladder logic rung text with syntax highlighting, search, copy).
- 2026-03-02 - PLC Fleet 1.5 UX: CSV export on all PLC tables (modules, tags, scorecards, I/O verify), sortable fleet scorecard table with pagination, sortable module inventory, fault/timer/counter quick filter chips on tags, fleet reuse & adoption dashboard (routine/UDT/tag prefix adoption bars), Similar Projects sub-tab, E2E tests converted from mocks to real MCP endpoints.
- 2026-03-02 - PLC Fleet page: project drill-down (click any job or enter number), clickable jobs on scorecard/search/changes, per-project Compare sub-tab. Sub-tab enhancements: module health columns (FW, status, risk highlighting), history change deltas (tag/module/rung diffs), program search filter + key states/outputs/safety features, tag const/dims columns + used-by cross-reference, controller summary badges (dates, safety, security).
- 92af620 - PLC Code Intelligence Phases A-C: Scorecards (4-dimension scoring, fleet averages), Intent Deduction (100K rungs parsed, 2923 routines summarized), I/O Verify sub-tab (BOM ↔ PLC module matching with spare parts). 46 MCP tools.
- a549ce1 - Lead Time UI round 2: leadership polish — bigger text, correlation explanation panels, redesigned hypothesis cards, smart heatmap coloring (YoY delta + percentile), capacity chart fixes, timeline summary mode
- a4352af - Lead Time UI: rename Recovery→Current, human-readable labels everywhere, Machine Shop + Weld in dept heatmap, new Gantt timeline view, ViewHeader with glossary, fix dollar formatting on labor hour columns
- Ask the Fleet - AI chat panel: Claude + 42 MCP tools, streaming SSE, rich tool cards, navigation integration, Ctrl+K shortcut
- 1c01405 - Dashboard collapse (details hidden by default), timeline overhaul (zoom/pan/tooltips/keyboard nav), capacity & floor plan diagnostics
- 0f2ddf8 - Add monthly labor burn chart to project detail Hours tab (cumulative dept hours from WIPLEDGER.HRS with budget reference lines)
- e26affb - Fix status codes, array direction, add close/ship date exclusions
- 56bd1fe - Add recency gate to active project filter
- e82636a - Update API URL to
https://api.progressivesurface.com - e9c4cb6 - Add Azure App Service deployment configuration (Express server)
- d4316c4 - Initial commit: PSI Explorer Web React application
Related
- PSI.UniData.API - Backend REST API
- PSI Explorer (Desktop) - Original WPF application
- CLI Export Tool - Command-line BOM export
- BOM Data Dictionary - Field definitions