{
  "format": "opex-flow-package/1",
  "payload": {
    "contents": {
      "apps": [
        {
          "description": "Printer connection status and the coding job log, filterable by printer, for line supervisors.",
          "icon": "printer",
          "id": "line-jobs",
          "pages": [
            {
              "components": [
                {
                  "format": "integer",
                  "id": "stat-total",
                  "refresh_s": 10,
                  "source": "printers",
                  "span": 4,
                  "title": "Printers",
                  "type": "stat",
                  "value": "total"
                },
                {
                  "format": "integer",
                  "id": "stat-up",
                  "refresh_s": 10,
                  "source": "printers",
                  "span": 4,
                  "title": "Connected",
                  "tone": "ok",
                  "type": "stat",
                  "value": "connected"
                },
                {
                  "format": "integer",
                  "id": "stat-down",
                  "refresh_s": 10,
                  "source": "printers",
                  "span": 4,
                  "title": "Not connected",
                  "tone": "error",
                  "type": "stat",
                  "value": "disconnected"
                },
                {
                  "fields": [
                    {
                      "label": "Printer",
                      "options_from": {
                        "label": "id",
                        "source": "printers",
                        "value": "id"
                      },
                      "param": "printer",
                      "placeholder": "All printers",
                      "type": "select"
                    }
                  ],
                  "id": "job-filters",
                  "span": 12,
                  "type": "filters"
                },
                {
                  "columns": [
                    {
                      "field": "ts",
                      "format": "datetime",
                      "label": "Time",
                      "sortable": true,
                      "width": "170px"
                    },
                    {
                      "field": "printer_id",
                      "label": "Printer",
                      "sortable": true
                    },
                    {
                      "field": "source",
                      "label": "Source"
                    },
                    {
                      "field": "message",
                      "label": "Message"
                    },
                    {
                      "field": "status",
                      "format": "badge",
                      "label": "Status",
                      "sortable": true,
                      "tones": {
                        "ok": "ok"
                      }
                    },
                    {
                      "align": "right",
                      "field": "duration_ms",
                      "format": "integer",
                      "label": "Duration (ms)",
                      "sortable": true
                    },
                    {
                      "field": "detail",
                      "label": "Detail"
                    }
                  ],
                  "empty": "No jobs logged yet.",
                  "export_csv": true,
                  "id": "job-table",
                  "page_size": 25,
                  "params": {
                    "printer": "${params.printer}"
                  },
                  "refresh_s": 15,
                  "search": true,
                  "source": "jobs",
                  "span": 12,
                  "subtitle": "Newest first, last 1000 jobs",
                  "title": "Printer jobs",
                  "type": "table"
                }
              ],
              "icon": "list-checks",
              "id": "jobs",
              "layout": {
                "columns": 12,
                "gap": "md",
                "type": "grid"
              },
              "params": {
                "printer": {
                  "type": "string"
                }
              },
              "path": "/",
              "title": "Job log"
            }
          ],
          "roles": [
            "viewer"
          ],
          "sources": {
            "jobs": {
              "printer": "${input.printer}",
              "type": "printers",
              "view": "jobs"
            },
            "printers": {
              "type": "printers",
              "view": "status"
            }
          },
          "theme": {
            "accent": "#b45309",
            "density": "comfortable",
            "mode": "auto",
            "primary": "#0f766e",
            "radius": "md"
          },
          "title": "Line job log"
        }
      ],
      "events": [
        {
          "description": "A production order was released to a packaging line: what to code on the pack (format, batch, production and expiry date) and which line runs it. The Printer line package maps it to the print fields of every coding printer on that line.",
          "example": {
            "batch": "B2610A",
            "expiry_date": "2027-10-29",
            "format_id": 2,
            "line": "LINE-A",
            "order_id": "PO-1042",
            "production_date": "2026-10-30"
          },
          "name": "production.order_released",
          "object": "production_order",
          "owner": "Production / Packaging",
          "schema": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "properties": {
              "batch": {
                "minLength": 1,
                "type": "string"
              },
              "expiry_date": {
                "description": "YYYY-MM-DD",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}",
                "type": "string"
              },
              "format_id": {
                "description": "1 = carton (outer), 2 = sachet, 3 = box (tertiary)",
                "enum": [
                  1,
                  2,
                  3
                ]
              },
              "line": {
                "description": "Production line; matches the `line:` of the coding printers",
                "minLength": 1,
                "type": "string"
              },
              "order_id": {
                "minLength": 1,
                "type": "string"
              },
              "production_date": {
                "description": "YYYY-MM-DD; required for formats 1 and 2",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}",
                "type": "string"
              },
              "reset_counter": {
                "type": "boolean"
              }
            },
            "required": [
              "order_id",
              "line",
              "format_id",
              "batch",
              "expiry_date"
            ],
            "type": "object"
          },
          "title": "Production order released to a line",
          "version": 1
        }
      ],
      "examples": [
        {
          "audience": "plant",
          "description": "Orders arrive at POST /api/ingest/orders/<id>. Two sub-flows of the Printer line package do the work: Order to print fields validates the order against production.order_released and maps it to the message and fields of the format; Print to line sends them to every enabled printer whose `line:` equals the order's line. Add a printer with that line in config and it is reached at once. Test shows, per printer, the message, fields and counter it would send, without printing.",
          "flow": {
            "edges": [
              {
                "from": "src",
                "id": "src-fields",
                "to": "fields"
              },
              {
                "from": "fields",
                "id": "fields-print",
                "to": "print"
              },
              {
                "from": "print",
                "id": "print-done",
                "to": "done"
              }
            ],
            "flows": [
              {
                "id": "main",
                "name": "Orders to printers"
              }
            ],
            "nodes": [
              {
                "h": 64,
                "id": "note",
                "text": "Order -> validated and mapped to print fields -> every printer of the order's line. Both Sub-flow steps come from the Printer line package.",
                "type": "comment",
                "w": 720,
                "x": 40,
                "y": 0
              },
              {
                "id": "src",
                "label": "Production orders",
                "match": "{{SOURCE}}",
                "type": "source",
                "x": 40,
                "y": 120
              },
              {
                "calls": "pkg:com.opexcg.printer-line/order-to-print-fields",
                "id": "fields",
                "label": "Order to print fields",
                "type": "subflow",
                "x": 300,
                "y": 120
              },
              {
                "calls": "pkg:com.opexcg.printer-line/print-to-line",
                "id": "print",
                "label": "Print to line",
                "type": "subflow",
                "x": 560,
                "y": 120
              },
              {
                "id": "done",
                "label": "Printed",
                "sink": "hub",
                "type": "sink",
                "x": 820,
                "y": 120
              }
            ]
          },
          "id": "order-to-printers-subflows",
          "inputs": [
            {
              "default": "orders:*",
              "help": "Where orders arrive: a source key such as orders:* (POST /api/ingest/orders/<id>).",
              "key": "SOURCE",
              "kind": "text",
              "label": "Order source",
              "pattern": "^[a-z0-9_-]+:(\\*|[A-Za-z0-9._-]+)$"
            },
            {
              "default": "LINE-A",
              "help": "The sample order goes to printers with this `line:`. Real orders carry their own line.",
              "key": "LINE",
              "kind": "text",
              "label": "Production line (for the sample order)",
              "pattern": "^[A-Za-z0-9._-]{1,32}$"
            }
          ],
          "needs": {
            "config": "printers:\n  - id: coder-a1\n    line: {{LINE}}\n    protocol: markem_imaje\n    transport: { kind: tcp, host: 192.168.10.11, port: 2101 }\n    messages: { FMT-1: \"1\", FMT-2: \"2\", FMT-3: \"3\" }\n    fields: { LINE1: \"1\", LINE2: \"2\" }",
            "env": [],
            "mocks": [],
            "sources": [
              "{{SOURCE}}: orders posted to POST /api/ingest/<source>/<id>"
            ]
          },
          "purpose": "A production order becomes print fields and reaches every coding printer configured for the order's line, with no flow edit when a printer or a line is added.",
          "runs": {
            "mode": "dry-run",
            "note": "Test shows what would be sent. Add the printers shown under Needs to print for real: a line with no printer fails the order with the line named."
          },
          "title": "Order to every printer of its line (package sub-flows)",
          "try": {
            "node": "src",
            "record": {
              "batch": "B2610A",
              "expiry_date": "2027-10-29",
              "format_id": 2,
              "line": "{{LINE}}",
              "order_id": "PO-1042",
              "production_date": "2026-10-30"
            }
          }
        }
      ],
      "openapi": [],
      "subflows": [
        {
          "description": "Checks a production order against the production.order_released event type (a bad one is quarantined with the reasons), then maps it to what a coding printer needs: metadata.line, metadata.message (FMT-<format_id>), metadata.fields (BATCH, ED, PD, LINE1, LINE2) and metadata.counter. Formats: 1 carton, 2 sachet, 3 box.",
          "edges": [
            {
              "from": "in",
              "id": "in-valid",
              "to": "valid"
            },
            {
              "from": "valid",
              "id": "valid-fields",
              "to": "fields"
            },
            {
              "from": "fields",
              "id": "fields-out",
              "to": "out"
            }
          ],
          "id": "order-to-print-fields",
          "nodes": [
            {
              "id": "in",
              "type": "subflow_in",
              "x": 40,
              "y": 80
            },
            {
              "id": "valid",
              "label": "Must be production.order_released",
              "on_invalid": "quarantine",
              "schema": "production.order_released",
              "type": "validate",
              "x": 300,
              "y": 80
            },
            {
              "backend": "js",
              "id": "fields",
              "label": "Order -> print fields",
              "on_error": "fail",
              "script": "// Production order -> what to print. No printer ids here: the Print to line step reaches every\n// printer configured with `line: <metadata.line>`.\n// Input: { order_id, line, format_id, batch, production_date: \"YYYY-MM-DD\", expiry_date: \"YYYY-MM-DD\", reset_counter? }\nconst FORMATS = {\n  1: { name: \"Carton, outer\", pd: true },\n  2: { name: \"Sachet\", pd: true, line1: \"PROD:\", line2: \"EXP:\" },\n  3: { name: \"Box, tertiary\", pd: false },\n};\n\nfunction day(value, what) {\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(String(value || \"\"));\n  if (!m) throw new Error(`${what} must be a date YYYY-MM-DD, got ${JSON.stringify(value)}`);\n  return `${m[3]} ${m[2]} ${m[1]}`;\n}\n\nfunction transform(order, ctx) {\n  const fmt = FORMATS[order.format_id];\n  if (!fmt) throw new Error(`unknown format_id ${JSON.stringify(order.format_id)}`);\n  const line = String(order.line || \"\").trim();\n  if (!line) throw new Error(\"order has no line\");\n  const batch = String(order.batch || \"\").trim();\n  if (!batch) throw new Error(\"order has no batch\");\n  const pd = fmt.pd ? day(order.production_date, \"production_date\") : \"\";\n  const ed = day(order.expiry_date, \"expiry_date\");\n  const text = (...p) => p.filter(Boolean).join(\" \");\n  return {\n    source: ctx.source,\n    timestamp: ctx.timestamp,\n    machine: { id: line },\n    status: { state: \"running\" },\n    axes: [],\n    metadata: {\n      line,\n      order_id: order.order_id == null ? \"\" : String(order.order_id),\n      message: `FMT-${order.format_id}`,\n      fields: { BATCH: batch, ED: ed, PD: pd, LINE1: text(fmt.line1, pd, batch), LINE2: text(fmt.line2, ed) },\n      counter: order.reset_counter === true ? \"reset\" : \"\",\n    },\n  };\n}\n",
              "type": "transform",
              "x": 560,
              "y": 80
            },
            {
              "id": "out",
              "type": "subflow_out",
              "x": 820,
              "y": 80
            }
          ],
          "title": "Order to print fields"
        },
        {
          "description": "Sends metadata.message, metadata.fields and metadata.counter to every enabled coding printer whose `line:` equals metadata.line (resolved when the record runs). A line with no printer fails the record with the line named, so a typo never prints nothing silently. Expects the record shape of Order to print fields.",
          "edges": [
            {
              "from": "in",
              "id": "in-print",
              "to": "print"
            },
            {
              "from": "print",
              "id": "print-out",
              "to": "out"
            }
          ],
          "id": "print-to-line",
          "nodes": [
            {
              "id": "in",
              "type": "subflow_in",
              "x": 40,
              "y": 80
            },
            {
              "counter": "${metadata.counter}",
              "fields_from": "metadata.fields",
              "id": "print",
              "label": "Printers of the line",
              "line": "${metadata.line}",
              "message": "${metadata.message}",
              "type": "printer",
              "x": 300,
              "y": 80
            },
            {
              "id": "out",
              "type": "subflow_out",
              "x": 560,
              "y": 80
            }
          ],
          "title": "Print to line"
        }
      ]
    },
    "manifest": {
      "author": "OpexCG",
      "description": "Production orders to coding printers by line: a validated production.order_released event, the order-to-print-fields mapping (formats, dates, batch, counter reset) and a Print to line step that reaches every printer configured for the order's line. Ships an example flow and a job-log app template for supervisors.",
      "homepage": "https://flow.opexcg.com/packages",
      "license": "Free for use with opex-flow",
      "name": "com.opexcg.printer-line",
      "requires": "2.1.0",
      "title": "Printer line",
      "version": "1.0.0"
    }
  },
  "signature": "qS2s4eWGOgMsWDV0vBmfM9O7BoSFlIm/jeCg6U/0+e54XbJfFl6ekFfCMwYMiVIrG1vTgvUTyV/vdXpLDUruDw==",
  "key_id": "opexcg-1"
}
