Skip to main content

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 eventNetSuite action
Raw milk separated into cream + skimAssembly unbuild (source → cream + skim items)
Bulk milk truck unloadedInventory receipt
Blend tank filled and namedWork order create, issues, completions
Surge tank filled from UHTWork order create, issues, completions (finished goods)
Milk arriving vs. issued differsWaste / shrinkage inventory adjustments
Tanks at rest drifting on sensor noiseDe-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. rest reads the live plant, historical replays a past window through a simulated clock for testing and back-fill, and mock runs 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. ENVIRONMENT defaults to dev, so an unconfigured instance cannot write into the production dataset.
  • MES_DRY_RUN computes 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

JobDefault cadenceResponsibility
separationJob10 sDrives the separator state machine → cream/skim unbuilds
bulkMilkJob1 minDetects bulk milk truck intakes
blendTankJob10 sDetects blend-tank batches (start, fill, name)
surgeTankJob10 sDetects surge-tank finished-goods batches
equipmentJob5 sPolls equipment state, integrates the UHT software totalizer
issueCompletionJob5 minEmits periodic work order issues and completions
inventoryAdjustmentJob5 minAt-rest tank reconciliation and waste sweeps

Every cadence is overridable via a *_CRON environment variable.

API

Mounted under /api, plus a health endpoint.

MountPurpose
GET /healthLiveness 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.

ModelHolds
MesSeparationBatchSeparator batches
MesBulkMilkRunBulk milk truck intakes
MesBlendTankBatch / MesBlendTankInflowBlend tank batches and their inflows
MesSurgeTankBatch / MesSurgeTankIssueStateSurge tank batches and issue state
MesFgInputPairingFinished-goods to raw-input pairings
MesInventoryAdjustmentWaste and reconciliation adjustments
MesPipeIssueStatePipe issue tracking
MesProcessingLogProcessing audit trail
MesRuntimeConfigHot-swappable runtime configuration

Tech Stack

TechnologyVersionPurpose
Node.js22Runtime
Express5.1.0HTTP API
node-cron3.0.3Job scheduling
Sequelize6.37.7ORM
tedious19.2.1SQL Server driver
httpntlm1.8.13NTLM auth to the AVEVA Historian
body-parser1.20.3Request parsing
dotenv16.4.7Configuration

Interfaces

SystemDirectionPurpose
AVEVA HistorianinboundPolls 23,000+ SCADA tags over the REST/OData v2 API using NTLM
SQL ServerbothBatch and tick state
Core API → NetSuiteoutboundWork orders, issues, completions, unbuilds, adjustments
SunApps GatewayinboundProxies /api/mes and /api/historian to this service
Microsoft TeamsoutboundAlerting 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:

GroupVariables
EnvironmentENVIRONMENT (dev/prod/local, drives table prefixing), PORT (default 4005)
HistorianHISTORIAN_PROVIDER (mock/rest/historical), HISTORIAN_BASE_URL, HISTORIAN_DOMAIN, HISTORIAN_USERNAME, HISTORIAN_PASSWORD
DatabaseSQL_SERVER, SQL_DATABASE, SQL_USER, SQL_PASSWORD
ERPCORE_API_URL, MES_DRY_RUN
Scheduling*_CRON per job
ReplaySIM_START, SPEEDUP
AlertingTEAMS_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

RepositoryIdaMilk/SunApps_MES
Version1.0.7
Primary languageJavaScript (Node, CommonJS)
Files178
Last activity2026-07-24
StatusActive

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.