Generated page. Written from IdaMilk/SunApps_MES at commit 10d6000 on 2026-07-28, version 1.0.7. This repository has the best documentation in the organisation — README.md, docs/PROJECT_OVERVIEW.md and docs/historian-api.md are worth reading directly.
SunApps MES
What is it?
A Manufacturing Execution System service that sits between the plant floor and the ERP. It continuously reads real-time process data from the AVEVA Historian (the plant SCADA system), reconstructs what physically happened at each production stage, and posts the corresponding inventory and cost transactions into NetSuite — with no operator data entry.
Purpose
The plant moves millions of pounds of milk a week through tanks, separators and fillers. None of that in-plant movement was recorded in NetSuite at all. Inventory and costing only saw what arrived and what shipped; everything in between was invisible.
This service closes that gap. It watches production through the historian and keeps NetSuite's inventory, work orders and costing in step with physical reality, keyed to lot numbers and bins so the result is fully traceable and costed.
How it Works
Production is a chain of stages:
Bulk intake → Raw silos → Separator → Silos → Blend tanks → UHT → Surge tanks → Fillers
The service polls the historian on cron schedules, detects the start and end of a batch at each stage, measures the volumes involved from tank levels, totalizers and flow meters, and emits the matching NetSuite transactions.
| Plant event | NetSuite action |
|---|---|
| Raw milk separated into cream + skim | Assembly unbuild (source → cream + skim items) |
| Bulk milk truck unloaded | Inventory receipt |
| Blend tank filled and named | Work order create, issues, completions |
| Surge tank filled from UHT | Work order create, issues, completions (finished goods) |
| Milk arriving vs. issued differs | Waste / shrinkage inventory adjustments |
| Tanks at rest drifting on sensor noise | De-noised inventory reconciliation sweeps |
Design decisions worth understanding
These are the parts that make the system trustworthy, and they are unusually deliberate:
- The historian is behind a swappable provider.
restreads the live plant,historicalreplays a past window through a simulated clock for testing and back-fill, andmockruns with no plant connection at all. You can develop against this system without touching production SCADA. - Environment-isolated tables. Dev, prod and local share one physical database, but every table is prefixed (
prod_…,dev_…) so datasets never commingle.ENVIRONMENTdefaults todev, so an unconfigured instance cannot write into the production dataset. MES_DRY_RUNcomputes everything and posts nothing. You can validate behaviour end to end with zero ERP side effects.- Concurrency-guarded ticks. Each cron job is guarded so a slow poll can never stack overlapping runs — important when a job fires every 5 seconds.
- Restart-safe. Batch and tick state hydrates from the database on boot, so a restart mid-production resumes cleanly rather than losing or double-posting a batch.
- Self-healing schema.
ensureRuntimeSchema()adds columns and indexes idempotently on every boot, so models evolve without manual migrations. - Hot-swappable runtime config. Replay mode, sim clock, speed-up and cron cadence can be changed at runtime through the dev API without a restart.
Cron jobs
| Job | Default cadence | Responsibility |
|---|---|---|
separationJob | 10 s | Drives the separator state machine → cream/skim unbuilds |
bulkMilkJob | 1 min | Detects bulk milk truck intakes |
blendTankJob | 10 s | Detects blend-tank batches (start, fill, name) |
surgeTankJob | 10 s | Detects surge-tank finished-goods batches |
equipmentJob | 5 s | Polls equipment state, integrates the UHT software totalizer |
issueCompletionJob | 5 min | Emits periodic work order issues and completions |
inventoryAdjustmentJob | 5 min | At-rest tank reconciliation and waste sweeps |
Every cadence is overridable via a *_CRON environment variable.
API
Mounted under /api, plus a health endpoint.
| Mount | Purpose |
|---|---|
GET /health | Liveness plus last-poll timestamp per job |
/api/historian/* | Raw historian queries — tags, values, summaries |
/api/mes/milk/* | Milk MES operations |
/api/mes/live-status/* | Live plant and batch status |
/api/mes/fg-pairings/* | Finished-goods to raw-input pairing approvals |
/api/mes/analytics/* | Batch history, trends, inventory-adjustment correlations |
/api/mes/dev/replay/* | Runtime config — replay toggle, sim clock, cron cadence |
/api/milk/* | Legacy milk endpoints |
/health is the most useful diagnostic here: per-job last-poll timestamps tell you immediately which stage has stopped seeing the plant.
Data model
Sequelize models over SQL Server, all environment-prefixed.
| Model | Holds |
|---|---|
MesSeparationBatch | Separator batches |
MesBulkMilkRun | Bulk milk truck intakes |
MesBlendTankBatch / MesBlendTankInflow | Blend tank batches and their inflows |
MesSurgeTankBatch / MesSurgeTankIssueState | Surge tank batches and issue state |
MesFgInputPairing | Finished-goods to raw-input pairings |
MesInventoryAdjustment | Waste and reconciliation adjustments |
MesPipeIssueState | Pipe issue tracking |
MesProcessingLog | Processing audit trail |
MesRuntimeConfig | Hot-swappable runtime configuration |
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Node.js | 22 | Runtime |
| Express | 5.1.0 | HTTP API |
| node-cron | 3.0.3 | Job scheduling |
| Sequelize | 6.37.7 | ORM |
| tedious | 19.2.1 | SQL Server driver |
| httpntlm | 1.8.13 | NTLM auth to the AVEVA Historian |
| body-parser | 1.20.3 | Request parsing |
| dotenv | 16.4.7 | Configuration |
Interfaces
| System | Direction | Purpose |
|---|---|---|
| AVEVA Historian | inbound | Polls 23,000+ SCADA tags over the REST/OData v2 API using NTLM |
| SQL Server | both | Batch and tick state |
| Core API → NetSuite | outbound | Work orders, issues, completions, unbuilds, adjustments |
| SunApps Gateway | inbound | Proxies /api/mes and /api/historian to this service |
| Microsoft Teams | outbound | Alerting webhook |
Operations
Deployed by GitHub Actions to internal CI runners: a development workflow, a production workflow triggered on release publish, and a production tagging workflow. The production workflow checks out the tag, writes .env from repository secrets, copies to an internal application server, runs npm ci, restarts the process under a supervisor, and posts a release-notes card to Teams.
Versioning follows a master-patch convention — a feature branch sets package.json to exactly the master patch plus one before its first push.
Configuration is environment variables; names only:
| Group | Variables |
|---|---|
| Environment | ENVIRONMENT (dev/prod/local, drives table prefixing), PORT (default 4005) |
| Historian | HISTORIAN_PROVIDER (mock/rest/historical), HISTORIAN_BASE_URL, HISTORIAN_DOMAIN, HISTORIAN_USERNAME, HISTORIAN_PASSWORD |
| Database | SQL_SERVER, SQL_DATABASE, SQL_USER, SQL_PASSWORD |
| ERP | CORE_API_URL, MES_DRY_RUN |
| Scheduling | *_CRON per job |
| Replay | SIM_START, SPEEDUP |
| Alerting | TEAMS_WEBHOOK_URL |
When diagnosing, check in this order: /health for per-job poll timestamps, then MesProcessingLog, then whether MES_DRY_RUN or a non-prod ENVIRONMENT is silently suppressing writes.
Repository
| Repository | IdaMilk/SunApps_MES |
| Version | 1.0.7 |
| Primary language | JavaScript (Node, CommonJS) |
| Files | 178 |
| Last activity | 2026-07-24 |
| Status | Active |
In-repo documentation beyond the README: docs/historian-api.md (historian REST reference and plant tag map), docs/PROJECT_OVERVIEW.md (non-technical business overview), plus BATCH_LIFECYCLE_PLAN.md, SURGE_TANK_PLAN.md, DEPLOY_CHECKLIST.md and PICKUP.md.