@hotmeshio/long-tail 0.7.4 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/roles.d.ts +4 -0
- package/build/api/roles.js +7 -1
- package/build/lib/db/schemas/019_role_priority.sql +28 -0
- package/build/routes/roles.js +1 -1
- package/build/scripts/ortho-populate.d.ts +7 -1
- package/build/scripts/ortho-populate.js +21 -6
- package/build/services/escalation/facet-sql.d.ts +1 -0
- package/build/services/escalation/facet-sql.js +7 -6
- package/build/services/escalation/queries.js +27 -5
- package/build/services/escalation/sql.d.ts +19 -2
- package/build/services/escalation/sql.js +32 -9
- package/build/services/escalation/types.d.ts +7 -1
- package/build/services/role/index.js +2 -0
- package/build/services/role/sql.d.ts +4 -4
- package/build/services/role/sql.js +12 -6
- package/build/services/role/types.d.ts +14 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +6 -0
- package/build/system/mcp-servers/admin/schemas.js +3 -0
- package/build/system/mcp-servers/admin/users.js +4 -1
- package/build/system/seed/tool-manifests-admin.d.ts +152 -0
- package/build/system/seed/tool-manifests-admin.js +1 -1
- package/dashboard/dist/assets/{AdminDashboard-C9eE1VEp.js → AdminDashboard-Bgomc_jH.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-C9eE1VEp.js.map → AdminDashboard-Bgomc_jH.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-Ba8QfXM2.js → AgentConfigPage-C6gDgz21.js} +6 -6
- package/dashboard/dist/assets/{AgentConfigPage-Ba8QfXM2.js.map → AgentConfigPage-C6gDgz21.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-DVmec0CP.js → AgentDetailPage-0rrH5FmV.js} +4 -4
- package/dashboard/dist/assets/{AgentDetailPage-DVmec0CP.js.map → AgentDetailPage-0rrH5FmV.js.map} +1 -1
- package/dashboard/dist/assets/{AgentsPage-DNiCYMh3.js → AgentsPage-DkIU2zed.js} +2 -2
- package/dashboard/dist/assets/{AgentsPage-DNiCYMh3.js.map → AgentsPage-DkIU2zed.js.map} +1 -1
- package/dashboard/dist/assets/{AvailableEscalationsPage-gDIF8ocC.js → AvailableEscalationsPage-C9DAtNGp.js} +2 -2
- package/dashboard/dist/assets/{AvailableEscalationsPage-gDIF8ocC.js.map → AvailableEscalationsPage-C9DAtNGp.js.map} +1 -1
- package/dashboard/dist/assets/{BotPicker-3wBcBavT.js → BotPicker-MnI3OyE7.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-3wBcBavT.js.map → BotPicker-MnI3OyE7.js.map} +1 -1
- package/dashboard/dist/assets/{CapabilitiesPage-WoPkND2y.js → CapabilitiesPage-Cv_AD_5i.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-WoPkND2y.js.map → CapabilitiesPage-Cv_AD_5i.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-Cx99mqNN.js → CollapsibleSection-Ne-Q3QFM.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-Cx99mqNN.js.map → CollapsibleSection-Ne-Q3QFM.js.map} +1 -1
- package/dashboard/dist/assets/{CredentialsPage-DbNaoPbF.js → CredentialsPage-B3Mfwl22.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-DbNaoPbF.js.map → CredentialsPage-B3Mfwl22.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-DLW4_Zg4.js → CronLabel-CtjUBRPo.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-DLW4_Zg4.js.map → CronLabel-CtjUBRPo.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-BRUFjkiu.js → CustomDurationPicker-Cp1fgzPX.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-BRUFjkiu.js.map → CustomDurationPicker-Cp1fgzPX.js.map} +1 -1
- package/dashboard/dist/assets/{DropZone-DEX0BBqt.js → DropZone-CZDmNBn-.js} +2 -2
- package/dashboard/dist/assets/{DropZone-DEX0BBqt.js.map → DropZone-CZDmNBn-.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-BtmDQaym.js → ElapsedCell-Gr5UlUbn.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-BtmDQaym.js.map → ElapsedCell-Gr5UlUbn.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationSchemaPage-Bm4yBEGN.js → EscalationSchemaPage-C5tuV6SJ.js} +2 -2
- package/dashboard/dist/assets/{EscalationSchemaPage-Bm4yBEGN.js.map → EscalationSchemaPage-C5tuV6SJ.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-B3xZwTQA.js → EscalationsOverview-BIZVnSBF.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-B3xZwTQA.js.map → EscalationsOverview-BIZVnSBF.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-Bh9FKXLI.js → EventTable-90aUfsNr.js} +2 -2
- package/dashboard/dist/assets/{EventTable-Bh9FKXLI.js.map → EventTable-90aUfsNr.js.map} +1 -1
- package/dashboard/dist/assets/{GraphInvokePage-ov_ANlaE.js → GraphInvokePage-Io5ZucT8.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-ov_ANlaE.js.map → GraphInvokePage-Io5ZucT8.js.map} +1 -1
- package/dashboard/dist/assets/{HomePage-D0DV6izb.js → HomePage-Cu0vUzTF.js} +2 -2
- package/dashboard/dist/assets/{HomePage-D0DV6izb.js.map → HomePage-Cu0vUzTF.js.map} +1 -1
- package/dashboard/dist/assets/{ListToolbar-FpcvqNkN.js → ListToolbar-CdmIvEIF.js} +2 -2
- package/dashboard/dist/assets/{ListToolbar-FpcvqNkN.js.map → ListToolbar-CdmIvEIF.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-DcuRiAff.js → McpOverview-C-Ypezuv.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-DcuRiAff.js.map → McpOverview-C-Ypezuv.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-BcYl67KP.js → McpQueryDetailPage-DdontKcj.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-BcYl67KP.js.map → McpQueryDetailPage-DdontKcj.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-C3GDzqZM.js → McpQueryPage-D-1TaOGG.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-C3GDzqZM.js.map → McpQueryPage-D-1TaOGG.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-SzOyhDBW.js → McpRunDetailPage-B1sCxKLD.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-SzOyhDBW.js.map → McpRunDetailPage-B1sCxKLD.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunsPage-BwgB6mRV.js → McpRunsPage-eWJ7cDa3.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-BwgB6mRV.js.map → McpRunsPage-eWJ7cDa3.js.map} +1 -1
- package/dashboard/dist/assets/{NamespacePill-CwKdoyRM.js → NamespacePill-DlqBTvFR.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill-CwKdoyRM.js.map → NamespacePill-DlqBTvFR.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-C5phWL0O.js +2 -0
- package/dashboard/dist/assets/OperationsPage-C5phWL0O.js.map +1 -0
- package/dashboard/dist/assets/{OperatorDashboard-DSXh-yYw.js → OperatorDashboard-YP6kP-f_.js} +2 -2
- package/dashboard/dist/assets/{OperatorDashboard-DSXh-yYw.js.map → OperatorDashboard-YP6kP-f_.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeader-BhaWTWcM.js → PageHeader-CFta2VaS.js} +2 -2
- package/dashboard/dist/assets/{PageHeader-BhaWTWcM.js.map → PageHeader-CFta2VaS.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeaderWithStats-yex5WmHB.js → PageHeaderWithStats-CcVAji-7.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-yex5WmHB.js.map → PageHeaderWithStats-CcVAji-7.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-iIB1rwEt.js → ProcessDetailPage-C_BXQvBs.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-iIB1rwEt.js.map → ProcessDetailPage-C_BXQvBs.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-BrqWR2zr.js → ProcessesListPage-BNhFwTLl.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-BrqWR2zr.js.map → ProcessesListPage-BNhFwTLl.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-8GLNjj9I.js +11 -0
- package/dashboard/dist/assets/RoleDetailPage-8GLNjj9I.js.map +1 -0
- package/dashboard/dist/assets/{RolePill-Cl71eDUA.js → RolePill-CXEyPMqb.js} +2 -2
- package/dashboard/dist/assets/{RolePill-Cl71eDUA.js.map → RolePill-CXEyPMqb.js.map} +1 -1
- package/dashboard/dist/assets/{RolesPage-Bzp9FhAl.js → RolesPage-B59tE2lZ.js} +2 -2
- package/dashboard/dist/assets/{RolesPage-Bzp9FhAl.js.map → RolesPage-B59tE2lZ.js.map} +1 -1
- package/dashboard/dist/assets/{RunAsSelector-DXbHRxQs.js → RunAsSelector-CokM3Ifp.js} +2 -2
- package/dashboard/dist/assets/{RunAsSelector-DXbHRxQs.js.map → RunAsSelector-CokM3Ifp.js.map} +1 -1
- package/dashboard/dist/assets/{StickyPagination-D8XuMSWD.js → StickyPagination-_H2j2CAH.js} +2 -2
- package/dashboard/dist/assets/{StickyPagination-D8XuMSWD.js.map → StickyPagination-_H2j2CAH.js.map} +1 -1
- package/dashboard/dist/assets/{StreamMessageDetail-D5v95pmz.js → StreamMessageDetail-sSfHDYIa.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-D5v95pmz.js.map → StreamMessageDetail-sSfHDYIa.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-CUEf2R5R.js → SwimlaneTimeline-LZWtfBK3.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-CUEf2R5R.js.map → SwimlaneTimeline-LZWtfBK3.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-B_WQiuer.js → TagInput-CQg-TrV9.js} +2 -2
- package/dashboard/dist/assets/{TagInput-B_WQiuer.js.map → TagInput-CQg-TrV9.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-BJvKNwEe.js → TaskDetailPage-CHBV2V6R.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-BJvKNwEe.js.map → TaskDetailPage-CHBV2V6R.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-Lgn2obNU.js → TaskQueuePill-DyvtL8X4.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-Lgn2obNU.js.map → TaskQueuePill-DyvtL8X4.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-CM7BWh_h.js → TasksListPage-B71FxvrY.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-CM7BWh_h.js.map → TasksListPage-B71FxvrY.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-D1K7xKq5.js → TimeAgo-JgT0U_sd.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-D1K7xKq5.js.map → TimeAgo-JgT0U_sd.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-7G0echgP.js → TimestampCell-gEreK8Nn.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-7G0echgP.js.map → TimestampCell-gEreK8Nn.js.map} +1 -1
- package/dashboard/dist/assets/{ToolPill-uVqcbHPE.js → ToolPill-miuRcj1Q.js} +2 -2
- package/dashboard/dist/assets/{ToolPill-uVqcbHPE.js.map → ToolPill-miuRcj1Q.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-LxuJvURK.js → ToolTestPanel-ChfHw9Aq.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-LxuJvURK.js.map → ToolTestPanel-ChfHw9Aq.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-JvAX6rrR.js → TopicDetailPage-DigI0HGt.js} +2 -2
- package/dashboard/dist/assets/{TopicDetailPage-JvAX6rrR.js.map → TopicDetailPage-DigI0HGt.js.map} +1 -1
- package/dashboard/dist/assets/{TopicsPage-tFT2AcTz.js → TopicsPage-BHjlFpPN.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-tFT2AcTz.js.map → TopicsPage-BHjlFpPN.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-Djz0nmaZ.js → UserName-uncbaKYf.js} +2 -2
- package/dashboard/dist/assets/{UserName-Djz0nmaZ.js.map → UserName-uncbaKYf.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-f53hilbI.js → WorkflowExecutionPage-DwzLKTkt.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-f53hilbI.js.map → WorkflowExecutionPage-DwzLKTkt.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowPill-IDjTRfQ8.js → WorkflowPill-BegV-iRF.js} +2 -2
- package/dashboard/dist/assets/{WorkflowPill-IDjTRfQ8.js.map → WorkflowPill-BegV-iRF.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-DM9Xe665.js → WorkflowsDashboard-rpnGhGnl.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-DM9Xe665.js.map → WorkflowsDashboard-rpnGhGnl.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-CUGSrx9N.js → WorkflowsOverview-ihhW8DiS.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-CUGSrx9N.js.map → WorkflowsOverview-ihhW8DiS.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-B1_yqxAw.js → YamlWorkflowDetailPage-C61-8y-E.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-B1_yqxAw.js.map → YamlWorkflowDetailPage-C61-8y-E.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-NtGvqiu7.js → YamlWorkflowsPage-CMpgsLEo.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-NtGvqiu7.js.map → YamlWorkflowsPage-CMpgsLEo.js.map} +1 -1
- package/dashboard/dist/assets/{agents-tML6AjDj.js → agents-27sErp5R.js} +2 -2
- package/dashboard/dist/assets/{agents-tML6AjDj.js.map → agents-27sErp5R.js.map} +1 -1
- package/dashboard/dist/assets/{bots-Cb0WTwhY.js → bots-CPHuAvjW.js} +2 -2
- package/dashboard/dist/assets/{bots-Cb0WTwhY.js.map → bots-CPHuAvjW.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-BGZogsi_.js → capabilities-BBTPqF-V.js} +2 -2
- package/dashboard/dist/assets/{capabilities-BGZogsi_.js.map → capabilities-BBTPqF-V.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-B7VJTlZD.js → controlplane-C9j2mwX8.js} +2 -2
- package/dashboard/dist/assets/{controlplane-B7VJTlZD.js.map → controlplane-C9j2mwX8.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-Bl-SQV_u.js → escalation-CyJ_wz-f.js} +2 -2
- package/dashboard/dist/assets/{escalation-Bl-SQV_u.js.map → escalation-CyJ_wz-f.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-columns-BlJOswRx.js → escalation-columns-DU4Jyxxo.js} +2 -2
- package/dashboard/dist/assets/{escalation-columns-BlJOswRx.js.map → escalation-columns-DU4Jyxxo.js.map} +1 -1
- package/dashboard/dist/assets/{index-e97M6GoI.js → index-BBQggGcf.js} +2 -2
- package/dashboard/dist/assets/{index-e97M6GoI.js.map → index-BBQggGcf.js.map} +1 -1
- package/dashboard/dist/assets/{index-D0-8byNu.js → index-BHS-ubrT.js} +2 -2
- package/dashboard/dist/assets/{index-D0-8byNu.js.map → index-BHS-ubrT.js.map} +1 -1
- package/dashboard/dist/assets/{index-DcbEIXit.js → index-BVHXREYZ.js} +2 -2
- package/dashboard/dist/assets/{index-DcbEIXit.js.map → index-BVHXREYZ.js.map} +1 -1
- package/dashboard/dist/assets/{index-Il_g7kw5.js → index-BiLZJ43R.js} +2 -2
- package/dashboard/dist/assets/{index-Il_g7kw5.js.map → index-BiLZJ43R.js.map} +1 -1
- package/dashboard/dist/assets/{index-Dgw1KKpq.js → index-BoZydcaW.js} +4 -4
- package/dashboard/dist/assets/{index-Dgw1KKpq.js.map → index-BoZydcaW.js.map} +1 -1
- package/dashboard/dist/assets/{index-CBv3UxF2.js → index-CCHGKErT.js} +2 -2
- package/dashboard/dist/assets/{index-CBv3UxF2.js.map → index-CCHGKErT.js.map} +1 -1
- package/dashboard/dist/assets/{index-0o71lRuY.js → index-CNprOGuS.js} +2 -2
- package/dashboard/dist/assets/{index-0o71lRuY.js.map → index-CNprOGuS.js.map} +1 -1
- package/dashboard/dist/assets/{index-Wny0bRgq.css → index-CZ6-l8_0.css} +1 -1
- package/dashboard/dist/assets/{index-3A_5CL81.js → index-CgeFUhUr.js} +2 -2
- package/dashboard/dist/assets/{index-3A_5CL81.js.map → index-CgeFUhUr.js.map} +1 -1
- package/dashboard/dist/assets/{index-TiKh2Ctj.js → index-ChniBGqD.js} +2 -2
- package/dashboard/dist/assets/{index-TiKh2Ctj.js.map → index-ChniBGqD.js.map} +1 -1
- package/dashboard/dist/assets/{index--WeZDfeN.js → index-CjXzZ19T.js} +2 -2
- package/dashboard/dist/assets/{index--WeZDfeN.js.map → index-CjXzZ19T.js.map} +1 -1
- package/dashboard/dist/assets/{index-DfDfJivY.js → index-D9pi0fs6.js} +2 -2
- package/dashboard/dist/assets/{index-DfDfJivY.js.map → index-D9pi0fs6.js.map} +1 -1
- package/dashboard/dist/assets/{index-SCrD3l-K.js → index-DoivEaUZ.js} +2 -2
- package/dashboard/dist/assets/{index-SCrD3l-K.js.map → index-DoivEaUZ.js.map} +1 -1
- package/dashboard/dist/assets/{index-BorpZbZA.js → index-Dzqt3g-K.js} +2 -2
- package/dashboard/dist/assets/{index-BorpZbZA.js.map → index-Dzqt3g-K.js.map} +1 -1
- package/dashboard/dist/assets/{knowledge-CDzw4E6q.js → knowledge-BErJA3Je.js} +2 -2
- package/dashboard/dist/assets/{knowledge-CDzw4E6q.js.map → knowledge-BErJA3Je.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-D_-NQtnG.js → mcp-Cx9L42EN.js} +2 -2
- package/dashboard/dist/assets/{mcp-D_-NQtnG.js.map → mcp-Cx9L42EN.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-BqZXdf9A.js → mcp-query-Cf6kcBoW.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-BqZXdf9A.js.map → mcp-query-Cf6kcBoW.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-CNLhgj8b.js → pipelines-CFctrv71.js} +2 -2
- package/dashboard/dist/assets/{pipelines-CNLhgj8b.js.map → pipelines-CFctrv71.js.map} +1 -1
- package/dashboard/dist/assets/{roles-CFD2eYap.js → roles-CCXZtqDr.js} +2 -2
- package/dashboard/dist/assets/roles-CCXZtqDr.js.map +1 -0
- package/dashboard/dist/assets/{tasks-DcSvF636.js → tasks-BPNj8ZWL.js} +2 -2
- package/dashboard/dist/assets/{tasks-DcSvF636.js.map → tasks-BPNj8ZWL.js.map} +1 -1
- package/dashboard/dist/assets/{topics-DI8TFSZv.js → topics-CR4UBuSV.js} +2 -2
- package/dashboard/dist/assets/{topics-DI8TFSZv.js.map → topics-CR4UBuSV.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-DcIgMDh3.js → useEventHooks-BgdnN8Rj.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-DcIgMDh3.js.map → useEventHooks-BgdnN8Rj.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-C88ZxGoX.js → useNamespace-BDsKfT0f.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-C88ZxGoX.js.map → useNamespace-BDsKfT0f.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-D2-yVjJ7.js → useYamlActivityEvents-CWZwHIlR.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-D2-yVjJ7.js.map → useYamlActivityEvents-CWZwHIlR.js.map} +1 -1
- package/dashboard/dist/assets/{users-C0WV36X8.js → users-BqmKVXR1.js} +2 -2
- package/dashboard/dist/assets/{users-C0WV36X8.js.map → users-BqmKVXR1.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons-CEWCGPeJ.js → vendor-icons-BaQqUm-e.js} +2 -2
- package/dashboard/dist/assets/{vendor-icons-CEWCGPeJ.js.map → vendor-icons-BaQqUm-e.js.map} +1 -1
- package/dashboard/dist/assets/{workflows-CkPv7xk-.js → workflows-Dps45zjk.js} +2 -2
- package/dashboard/dist/assets/{workflows-CkPv7xk-.js.map → workflows-Dps45zjk.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-BjrMQLTK.js → yaml-workflows-DhlTkbvY.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-BjrMQLTK.js.map → yaml-workflows-DhlTkbvY.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +3 -3
- package/docs/api/http/roles.md +6 -0
- package/docs/api/mcp/admin.md +2 -0
- package/docs/api/sdk/escalations.md +2 -2
- package/docs/api/sdk/roles.md +7 -1
- package/docs/dashboard.md +5 -4
- package/docs/data.md +2 -0
- package/docs/operations.md +16 -3
- package/package.json +1 -1
- package/dashboard/dist/assets/OperationsPage-BCr21p8I.js +0 -2
- package/dashboard/dist/assets/OperationsPage-BCr21p8I.js.map +0 -1
- package/dashboard/dist/assets/RoleDetailPage-DS37tJhq.js +0 -11
- package/dashboard/dist/assets/RoleDetailPage-DS37tJhq.js.map +0 -1
- package/dashboard/dist/assets/roles-CFD2eYap.js.map +0 -1
package/build/api/roles.d.ts
CHANGED
|
@@ -91,6 +91,10 @@ export declare function updateRole(input: {
|
|
|
91
91
|
sla_minutes?: number | null;
|
|
92
92
|
target_per_hour?: number | null;
|
|
93
93
|
worker_count?: number | null;
|
|
94
|
+
/** Max age (minutes) before a pending unclaimed item counts as priority on the Pace Board. Falls back to sla_minutes. */
|
|
95
|
+
priority_threshold_minutes?: number | null;
|
|
96
|
+
/** Escalation metadata key holding the age origin (ISO 8601 UTC timestamp). Falls back to created_at. */
|
|
97
|
+
priority_facet?: string | null;
|
|
94
98
|
/** Replace the upstream-input set (omitted = preserve; null or [] = clear). */
|
|
95
99
|
upstream_roles?: string[] | null;
|
|
96
100
|
/** Recorded on the schema snapshot when this update changes a schema field. */
|
package/build/api/roles.js
CHANGED
|
@@ -46,6 +46,7 @@ exports.getRoleSchema = getRoleSchema;
|
|
|
46
46
|
exports.listRoleSchemaVersions = listRoleSchemaVersions;
|
|
47
47
|
exports.deleteRole = deleteRole;
|
|
48
48
|
const roleService = __importStar(require("../services/role"));
|
|
49
|
+
const facet_sql_1 = require("../services/escalation/facet-sql");
|
|
49
50
|
/**
|
|
50
51
|
* List all distinct role names in the system.
|
|
51
52
|
*
|
|
@@ -207,7 +208,7 @@ async function updateRole(input) {
|
|
|
207
208
|
if (!input.role) {
|
|
208
209
|
return { status: 400, error: 'role is required' };
|
|
209
210
|
}
|
|
210
|
-
for (const field of ['sla_minutes', 'target_per_hour', 'worker_count']) {
|
|
211
|
+
for (const field of ['sla_minutes', 'target_per_hour', 'worker_count', 'priority_threshold_minutes']) {
|
|
211
212
|
const value = input[field];
|
|
212
213
|
if (value !== undefined && value !== null && (typeof value !== 'number' || !Number.isFinite(value) || value < 0)) {
|
|
213
214
|
return { status: 400, error: `${field} must be a non-negative number or null` };
|
|
@@ -216,6 +217,9 @@ async function updateRole(input) {
|
|
|
216
217
|
if (input.worker_count != null && !Number.isInteger(input.worker_count)) {
|
|
217
218
|
return { status: 400, error: 'worker_count must be a whole number' };
|
|
218
219
|
}
|
|
220
|
+
if (input.priority_facet != null && (typeof input.priority_facet !== 'string' || !facet_sql_1.FACET_KEY.test(input.priority_facet))) {
|
|
221
|
+
return { status: 400, error: 'priority_facet must be a metadata key (letters, numbers, underscores) or null' };
|
|
222
|
+
}
|
|
219
223
|
if (input.ops_visible !== undefined && typeof input.ops_visible !== 'boolean') {
|
|
220
224
|
return { status: 400, error: 'ops_visible must be a boolean' };
|
|
221
225
|
}
|
|
@@ -246,6 +250,8 @@ async function updateRole(input) {
|
|
|
246
250
|
sla_minutes: input.sla_minutes,
|
|
247
251
|
target_per_hour: input.target_per_hour,
|
|
248
252
|
worker_count: input.worker_count,
|
|
253
|
+
priority_threshold_minutes: input.priority_threshold_minutes,
|
|
254
|
+
priority_facet: input.priority_facet,
|
|
249
255
|
upstream_roles: input.upstream_roles,
|
|
250
256
|
change_summary: input.change_summary,
|
|
251
257
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Migration 019: Role priority threshold
|
|
2
|
+
--
|
|
3
|
+
-- Per-role tuning for the Pace Board priority count: pending, unclaimed
|
|
4
|
+
-- escalations whose age exceeds the threshold appear as a count the floor
|
|
5
|
+
-- pulls to the front of the rack. The signal is deliberately boolean per
|
|
6
|
+
-- item (past threshold or not) — a count to rebalance by, not a queue to
|
|
7
|
+
-- keep re-sorting.
|
|
8
|
+
--
|
|
9
|
+
-- priority_threshold_minutes — max age before an item counts as priority.
|
|
10
|
+
-- Falls back to sla_minutes when NULL.
|
|
11
|
+
-- priority_facet — lt_escalations.metadata key holding the age
|
|
12
|
+
-- origin as an ISO 8601 UTC timestamp (e.g.
|
|
13
|
+
-- the order's authorized date). Falls back to
|
|
14
|
+
-- created_at when NULL. When set, items
|
|
15
|
+
-- missing the key or holding an unparseable
|
|
16
|
+
-- value are not counted.
|
|
17
|
+
--
|
|
18
|
+
-- With both NULL the count still works for any role with sla_minutes set
|
|
19
|
+
-- (age from created_at) — one signal, no parallel SLA pattern.
|
|
20
|
+
--
|
|
21
|
+
-- No new lt_escalations index: the count rides the bounded pending-only scan
|
|
22
|
+
-- in STATION_LIVE_COUNTS_SQL. The GIN metadata index only serves containment
|
|
23
|
+
-- (@>), not age comparisons, and the pending working set is bounded by real
|
|
24
|
+
-- backlog depth.
|
|
25
|
+
|
|
26
|
+
ALTER TABLE lt_roles
|
|
27
|
+
ADD COLUMN IF NOT EXISTS priority_threshold_minutes NUMERIC,
|
|
28
|
+
ADD COLUMN IF NOT EXISTS priority_facet TEXT;
|
package/build/routes/roles.js
CHANGED
|
@@ -95,7 +95,7 @@ router.delete('/escalation-chains', auth_1.requireRoleManager, async (req, res)
|
|
|
95
95
|
/**
|
|
96
96
|
* PATCH /api/roles/:role
|
|
97
97
|
* Update role metadata. Requires admin.
|
|
98
|
-
* Body: { title?, description?, form_schema?, metadata_schema?, properties?, ops_visible?, parent_role?, sla_minutes?, target_per_hour?, worker_count? }
|
|
98
|
+
* Body: { title?, description?, form_schema?, metadata_schema?, properties?, ops_visible?, parent_role?, sla_minutes?, target_per_hour?, worker_count?, priority_threshold_minutes?, priority_facet? }
|
|
99
99
|
*/
|
|
100
100
|
router.patch('/:role', auth_1.requireRoleManager, async (req, res) => {
|
|
101
101
|
// URL is the resource identity — a `role` key in the body must not redirect
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
* down gradually, and resolves feed the next station a trickle instead of a
|
|
40
40
|
* wave.
|
|
41
41
|
*
|
|
42
|
+
* Every escalation this run creates carries `metadata.authorized_at` — an ISO
|
|
43
|
+
* 8601 UTC timestamp stamped when the order arrives, identical across all of
|
|
44
|
+
* that order's stages (main line and shoe side-quest). Point a role's
|
|
45
|
+
* `priority_facet` at `authorized_at` with a threshold (e.g. 1 minute) and the
|
|
46
|
+
* Pace Board priority count lights up as orders age through the run.
|
|
47
|
+
*
|
|
42
48
|
* ── The shoe side-quest ──────────────────────────────────────────────────────
|
|
43
49
|
* A shoe order exercises the cross-sequence model (ordering → inserting feeds
|
|
44
50
|
* ship via upstream_roles — its own Operations sequence, not a bend in the
|
|
@@ -94,7 +100,7 @@ declare function holdFor(esc: {
|
|
|
94
100
|
declare function api(method: string, path: string, body?: unknown): Promise<any>;
|
|
95
101
|
declare function login(): Promise<void>;
|
|
96
102
|
declare function ensureSideQuestRoles(): Promise<void>;
|
|
97
|
-
declare function createSideEscalation(role: string, workflowId: string, orderId: string, runId: string, description: string): Promise<boolean>;
|
|
103
|
+
declare function createSideEscalation(role: string, workflowId: string, orderId: string, runId: string, description: string, authorizedAt: string): Promise<boolean>;
|
|
98
104
|
declare function claimBatch(batchTag: string): Promise<number>;
|
|
99
105
|
declare function resolveBatch(batchTag: string, runId: string): Promise<number>;
|
|
100
106
|
interface PlannedOrder {
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
* down gradually, and resolves feed the next station a trickle instead of a
|
|
41
41
|
* wave.
|
|
42
42
|
*
|
|
43
|
+
* Every escalation this run creates carries `metadata.authorized_at` — an ISO
|
|
44
|
+
* 8601 UTC timestamp stamped when the order arrives, identical across all of
|
|
45
|
+
* that order's stages (main line and shoe side-quest). Point a role's
|
|
46
|
+
* `priority_facet` at `authorized_at` with a threshold (e.g. 1 minute) and the
|
|
47
|
+
* Pace Board priority count lights up as orders age through the run.
|
|
48
|
+
*
|
|
43
49
|
* ── The shoe side-quest ──────────────────────────────────────────────────────
|
|
44
50
|
* A shoe order exercises the cross-sequence model (ordering → inserting feeds
|
|
45
51
|
* ship via upstream_roles — its own Operations sequence, not a bend in the
|
|
@@ -158,7 +164,7 @@ async function ensureSideQuestRoles() {
|
|
|
158
164
|
console.log(`[${ts()}] Side-quest roles ready (ordering → inserting ⇒ ship)`);
|
|
159
165
|
}
|
|
160
166
|
// ── Side-quest escalations (standalone rows — no workflow behind them) ────────
|
|
161
|
-
async function createSideEscalation(role, workflowId, orderId, runId, description) {
|
|
167
|
+
async function createSideEscalation(role, workflowId, orderId, runId, description, authorizedAt) {
|
|
162
168
|
try {
|
|
163
169
|
await api('POST', '/api/escalations', {
|
|
164
170
|
type: 'ortho-stage',
|
|
@@ -167,7 +173,7 @@ async function createSideEscalation(role, workflowId, orderId, runId, descriptio
|
|
|
167
173
|
description,
|
|
168
174
|
workflow_id: workflowId,
|
|
169
175
|
envelope: JSON.stringify({ order_id: orderId }),
|
|
170
|
-
metadata: { order_id: orderId, source: 'populate', run_id: runId },
|
|
176
|
+
metadata: { order_id: orderId, source: 'populate', run_id: runId, authorized_at: authorizedAt },
|
|
171
177
|
});
|
|
172
178
|
return true;
|
|
173
179
|
}
|
|
@@ -237,10 +243,11 @@ async function resolveBatch(batchTag, runId) {
|
|
|
237
243
|
resolved++;
|
|
238
244
|
console.log(`[${ts()}] [resolve] ${esc.role.padEnd(9)} ${esc.id.slice(0, 8)}… held ${heldS.toFixed(1)}s [${totalResolved} total]`);
|
|
239
245
|
// Shoes arrived — the side quest advances to inserting, which sits
|
|
240
|
-
// available until the manufactured inserts catch up with it.
|
|
246
|
+
// available until the manufactured inserts catch up with it. The
|
|
247
|
+
// order's authorized_at rides along from the ordering escalation.
|
|
241
248
|
if (esc.role === 'ordering') {
|
|
242
249
|
const orderId = esc.metadata?.order_id ?? '';
|
|
243
|
-
await createSideEscalation('inserting', esc.workflow_id, orderId, runId, `Stuff inserts into shoes for ${orderId}
|
|
250
|
+
await createSideEscalation('inserting', esc.workflow_id, orderId, runId, `Stuff inserts into shoes for ${orderId}`, esc.metadata?.authorized_at ?? new Date().toISOString());
|
|
244
251
|
}
|
|
245
252
|
}
|
|
246
253
|
catch (err) {
|
|
@@ -264,10 +271,18 @@ async function runOrders(batchTag, runId, plan, target) {
|
|
|
264
271
|
for (const order of plan) {
|
|
265
272
|
if (done)
|
|
266
273
|
break;
|
|
274
|
+
// The order's authorization moment — stamped once at arrival and carried
|
|
275
|
+
// on every stage escalation via the workflow's metadata passthrough, so
|
|
276
|
+
// an `authorized_at` priority facet measures journey age.
|
|
277
|
+
const authorizedAt = new Date().toISOString();
|
|
267
278
|
try {
|
|
268
279
|
await api('POST', '/api/workflows/orthoPipeline/invoke', {
|
|
269
280
|
workflowId: order.wfId,
|
|
270
|
-
data: {
|
|
281
|
+
data: {
|
|
282
|
+
order_id: order.orderId,
|
|
283
|
+
item_type: 'insole-standard',
|
|
284
|
+
metadata: { source: 'populate', run_id: runId, authorized_at: authorizedAt },
|
|
285
|
+
},
|
|
271
286
|
metadata: { source: 'populate', run_id: runId, shoes: order.shoes },
|
|
272
287
|
});
|
|
273
288
|
workflowIds.push(order.wfId);
|
|
@@ -279,7 +294,7 @@ async function runOrders(batchTag, runId, plan, target) {
|
|
|
279
294
|
continue;
|
|
280
295
|
}
|
|
281
296
|
if (order.shoes) {
|
|
282
|
-
const ok = await createSideEscalation('ordering', `${order.wfId}-shoes`, order.orderId, runId, `Order shoes for ${order.orderId}
|
|
297
|
+
const ok = await createSideEscalation('ordering', `${order.wfId}-shoes`, order.orderId, runId, `Order shoes for ${order.orderId}`, authorizedAt);
|
|
283
298
|
if (!ok)
|
|
284
299
|
target.value -= 2;
|
|
285
300
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FacetOrder, FacetQuery } from '../../types';
|
|
2
|
+
export declare const FACET_KEY: RegExp;
|
|
2
3
|
/**
|
|
3
4
|
* Build the WHERE clause for a faceted query, pushing bound params into `params`.
|
|
4
5
|
* Every value is parameterized; only validated column names / metadata keys and
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// as services/escalation/sql.ts. Metadata facets use the GIN index (`@>`).
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.ENSURE_ORIGIN_INDEX = void 0;
|
|
10
|
+
exports.ENSURE_ORIGIN_INDEX = exports.FACET_KEY = void 0;
|
|
11
11
|
exports.buildFacetWhere = buildFacetWhere;
|
|
12
12
|
exports.buildFacetOrder = buildFacetOrder;
|
|
13
13
|
exports.buildGroupOrder = buildGroupOrder;
|
|
@@ -16,7 +16,8 @@ exports.buildGroupOrder = buildGroupOrder;
|
|
|
16
16
|
const ORDER_COLUMNS = new Set(['priority', 'created_at', 'updated_at', 'status', 'role']);
|
|
17
17
|
const RANGE_OPS = new Set(['<', '<=', '>', '>=', '=']);
|
|
18
18
|
// Metadata keys are interpolated (as a JSON path), so they are strictly validated.
|
|
19
|
-
|
|
19
|
+
// Exported so role config (priority_facet) validates keys against the same rule.
|
|
20
|
+
exports.FACET_KEY = /^[a-zA-Z0-9_]+$/;
|
|
20
21
|
/** A metadata extraction expression for a validated key (text or numeric). */
|
|
21
22
|
function metaExpr(key, numeric = false) {
|
|
22
23
|
return numeric ? `(metadata->>'${key}')::numeric` : `(metadata->>'${key}')`;
|
|
@@ -49,13 +50,13 @@ function buildFacetWhere(q, params) {
|
|
|
49
50
|
clauses.push(`NOT (metadata @> ANY($${params.length}::jsonb[]))`);
|
|
50
51
|
}
|
|
51
52
|
for (const r of q.range ?? []) {
|
|
52
|
-
if (!FACET_KEY.test(r.facet) || !RANGE_OPS.has(r.op))
|
|
53
|
+
if (!exports.FACET_KEY.test(r.facet) || !RANGE_OPS.has(r.op))
|
|
53
54
|
continue;
|
|
54
55
|
params.push(r.value);
|
|
55
56
|
clauses.push(`${metaExpr(r.facet, true)} ${r.op} $${params.length}`);
|
|
56
57
|
}
|
|
57
58
|
for (const key of q.exists ?? []) {
|
|
58
|
-
if (!FACET_KEY.test(key))
|
|
59
|
+
if (!exports.FACET_KEY.test(key))
|
|
59
60
|
continue;
|
|
60
61
|
clauses.push(`metadata ? '${key}'`);
|
|
61
62
|
}
|
|
@@ -80,7 +81,7 @@ function buildFacetOrder(orderBy) {
|
|
|
80
81
|
}
|
|
81
82
|
else if (o.field.startsWith('metadata.')) {
|
|
82
83
|
const key = o.field.slice('metadata.'.length);
|
|
83
|
-
if (!FACET_KEY.test(key))
|
|
84
|
+
if (!exports.FACET_KEY.test(key))
|
|
84
85
|
continue;
|
|
85
86
|
parts.push(`${metaExpr(key, o.numeric)} ${dir} NULLS LAST`);
|
|
86
87
|
}
|
|
@@ -106,7 +107,7 @@ function buildGroupOrder(orderBy) {
|
|
|
106
107
|
}
|
|
107
108
|
else if (o.field.startsWith('metadata.')) {
|
|
108
109
|
const key = o.field.slice('metadata.'.length);
|
|
109
|
-
if (!FACET_KEY.test(key))
|
|
110
|
+
if (!exports.FACET_KEY.test(key))
|
|
110
111
|
continue;
|
|
111
112
|
parts.push(`${agg}(${metaExpr(key, o.numeric)}) ${dir} NULLS LAST`);
|
|
112
113
|
}
|
|
@@ -212,6 +212,22 @@ async function listDistinctTypes() {
|
|
|
212
212
|
const client = await (0, client_1.escalations)();
|
|
213
213
|
return client.listDistinctTypes();
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Narrow the caller's explicit role filter WITHIN their read scope. The SDK
|
|
217
|
+
* store gives `roles[]` precedence over `role` when both are set, so passing
|
|
218
|
+
* both would silently DROP the explicit filter for scoped (non-global)
|
|
219
|
+
* callers — a role-filtered list would return the scope's rows instead.
|
|
220
|
+
* Intersecting first keeps the plain path's semantics identical to the
|
|
221
|
+
* faceted path's: a role filter can only narrow scope, never widen past it,
|
|
222
|
+
* and an out-of-scope role yields an empty result, not someone else's rows.
|
|
223
|
+
*/
|
|
224
|
+
function narrowRoleWithinScope(role, visibleRoles) {
|
|
225
|
+
if (!role || !visibleRoles)
|
|
226
|
+
return { role, roles: visibleRoles, empty: false };
|
|
227
|
+
if (!visibleRoles.includes(role))
|
|
228
|
+
return { empty: true };
|
|
229
|
+
return { role, roles: undefined, empty: false };
|
|
230
|
+
}
|
|
215
231
|
async function listEscalations(filters) {
|
|
216
232
|
// Active claim semantics: assigned_to-active and `claimed` both mean "held now".
|
|
217
233
|
const heldNow = !!(filters.assigned_to || filters.claimed);
|
|
@@ -240,15 +256,18 @@ async function listEscalations(filters) {
|
|
|
240
256
|
order: filters.order,
|
|
241
257
|
});
|
|
242
258
|
}
|
|
259
|
+
const scoped = narrowRoleWithinScope(filters.role, filters.visibleRoles);
|
|
260
|
+
if (scoped.empty)
|
|
261
|
+
return { escalations: [], total: 0 };
|
|
243
262
|
const client = await (0, client_1.escalations)();
|
|
244
263
|
// Shared filter — passed to both list() and count() so totals stay in sync.
|
|
245
264
|
const where = {
|
|
246
265
|
status: filters.status,
|
|
247
|
-
role:
|
|
266
|
+
role: scoped.role,
|
|
248
267
|
type: filters.type,
|
|
249
268
|
subtype: filters.subtype,
|
|
250
269
|
priority: filters.priority,
|
|
251
|
-
roles:
|
|
270
|
+
roles: scoped.roles,
|
|
252
271
|
};
|
|
253
272
|
if (filters.assigned_to)
|
|
254
273
|
where.assignedTo = filters.assigned_to;
|
|
@@ -292,15 +311,18 @@ async function listAvailableEscalations(filters) {
|
|
|
292
311
|
order: filters.order,
|
|
293
312
|
});
|
|
294
313
|
}
|
|
314
|
+
const scoped = narrowRoleWithinScope(filters.role, filters.visibleRoles);
|
|
315
|
+
if (scoped.empty)
|
|
316
|
+
return { escalations: [], total: 0 };
|
|
295
317
|
const client = await (0, client_1.escalations)();
|
|
296
318
|
const where = {
|
|
297
319
|
status: 'pending',
|
|
298
320
|
available: true,
|
|
299
|
-
role:
|
|
321
|
+
role: scoped.role,
|
|
300
322
|
type: filters.type,
|
|
301
323
|
subtype: filters.subtype,
|
|
302
324
|
priority: filters.priority,
|
|
303
|
-
roles:
|
|
325
|
+
roles: scoped.roles,
|
|
304
326
|
};
|
|
305
327
|
const [rows, total] = await Promise.all([
|
|
306
328
|
client.list({
|
|
@@ -359,7 +381,7 @@ async function getStationMetrics(visibleRoles, period) {
|
|
|
359
381
|
pending: Number(c.pending ?? 0),
|
|
360
382
|
claimed: Number(c.claimed ?? 0),
|
|
361
383
|
resolved: Number(p?.resolved ?? 0),
|
|
362
|
-
|
|
384
|
+
priority_count: Number(c.priority_count ?? 0),
|
|
363
385
|
throughput_pct: p?.throughput_pct != null ? Number(p.throughput_pct) : null,
|
|
364
386
|
wait: {
|
|
365
387
|
p99: toNum(p?.p99_wait_min),
|
|
@@ -45,15 +45,32 @@ export declare function searchEscalationsQuery(orderBy: string): string;
|
|
|
45
45
|
*/
|
|
46
46
|
export declare const RESOLVE_BY_METADATA_ATOMIC = "WITH target AS MATERIALIZED (\n SELECT *\n FROM public.hmsh_escalations\n WHERE metadata @> $1::jsonb\n AND status = 'pending'\n AND (\n $5::text[] IS NULL\n OR role = ANY($5)\n OR (role = ANY($6::text[]) AND assigned_to = $2)\n )\n ORDER BY priority ASC, created_at ASC\n LIMIT 1\n FOR UPDATE\n),\nclaimed AS (\n UPDATE public.hmsh_escalations e\n SET assigned_to = COALESCE(e.assigned_to, $2),\n claimed_at = COALESCE(e.claimed_at, NOW()),\n assigned_until = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.assigned_until\n ELSE NOW() + INTERVAL '5 minutes' END,\n claim_expires_at = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.claim_expires_at\n ELSE NOW() + INTERVAL '5 minutes' END,\n metadata = CASE WHEN $4::jsonb IS NOT NULL\n THEN COALESCE(e.metadata, '{}'::jsonb) || $4::jsonb\n ELSE e.metadata END,\n updated_at = NOW()\n FROM target\n WHERE e.id = target.id\n AND target.signal_key IS NULL\n RETURNING e.*\n),\nresolved AS (\n UPDATE public.hmsh_escalations e\n SET status = 'resolved',\n resolved_at = NOW(),\n resolver_payload = $3,\n updated_at = NOW()\n FROM claimed\n WHERE e.id = claimed.id\n AND (claimed.metadata->>'signal_id') IS NULL\n RETURNING e.*\n)\nSELECT\n resolved.*,\n target.id AS target_id,\n target.metadata->>'signal_id' AS signal_id,\n target.signal_key AS signal_key,\n target.workflow_id AS target_workflow_id,\n target.workflow_type AS target_workflow_type,\n target.task_queue AS target_task_queue,\n (target.assigned_to IS NOT NULL\n AND target.assigned_until IS NOT NULL\n AND target.assigned_until > NOW()) AS signal_already_claimed,\n CASE WHEN resolved.id IS NOT NULL THEN 'resolved' ELSE 'signal_required' END AS outcome\nFROM target\nLEFT JOIN resolved ON resolved.id = target.id";
|
|
47
47
|
/**
|
|
48
|
-
* Live counts — pending, active claims, and past-
|
|
48
|
+
* Live counts — pending, active claims, and priority (past-threshold) per role.
|
|
49
49
|
*
|
|
50
50
|
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
51
51
|
*
|
|
52
52
|
* Hits idx_hmsh_esc_pending_role_created (role, created_at DESC) WHERE
|
|
53
53
|
* status='pending'. The pending working set is bounded by real backlog depth,
|
|
54
54
|
* so this stays cheap as historical volume grows — safe on the hot refresh path.
|
|
55
|
+
*
|
|
56
|
+
* priority_count is the Pace Board rebalance signal: pending AND unclaimed
|
|
57
|
+
* items older than the role's threshold. One signal, two per-role dials with
|
|
58
|
+
* fallbacks so it works from sla_minutes alone:
|
|
59
|
+
*
|
|
60
|
+
* age origin — priority_facet metadata key (an ISO 8601 UTC timestamp, e.g.
|
|
61
|
+
* the order's authorized date) when set, else created_at.
|
|
62
|
+
* threshold — priority_threshold_minutes when set, else sla_minutes.
|
|
63
|
+
*
|
|
64
|
+
* Claimed items are excluded — they are already in someone's hands; the count
|
|
65
|
+
* is what the floor must still pull forward. The GIN metadata index only
|
|
66
|
+
* serves containment, not age comparisons, so the facet read rides the same
|
|
67
|
+
* bounded pending scan as the other counts. The cast is guarded by
|
|
68
|
+
* pg_input_is_valid inside a CASE (Postgres does not promise AND evaluation
|
|
69
|
+
* order) so one malformed metadata value can never break the stats query;
|
|
70
|
+
* with a facet configured, items missing the key or holding an unparseable
|
|
71
|
+
* value do not count (NULL age never passes the comparison).
|
|
55
72
|
*/
|
|
56
|
-
export declare const STATION_LIVE_COUNTS_SQL = "\nWITH role_targets AS (\n SELECT role, sla_minutes\n FROM lt_roles\n WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))\n),\nlive_counts AS (\n SELECT\n e.role,\n COUNT(*) AS pending,\n COUNT(*) FILTER (WHERE e.assigned_to IS NOT NULL AND e.assigned_until > NOW()) AS claimed,\n COUNT(*) FILTER (\n WHERE rt.sla_minutes IS NOT NULL\n AND e.created_at < NOW() - (rt.sla_minutes * INTERVAL '1 minute')\n ) AS
|
|
73
|
+
export declare const STATION_LIVE_COUNTS_SQL = "\nWITH role_targets AS (\n SELECT role, sla_minutes, priority_threshold_minutes, priority_facet\n FROM lt_roles\n WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))\n),\nlive_counts AS (\n SELECT\n e.role,\n COUNT(*) AS pending,\n COUNT(*) FILTER (WHERE e.assigned_to IS NOT NULL AND e.assigned_until > NOW()) AS claimed,\n COUNT(*) FILTER (\n WHERE COALESCE(rt.priority_threshold_minutes, rt.sla_minutes) IS NOT NULL\n AND (e.assigned_to IS NULL OR e.assigned_until IS NULL OR e.assigned_until <= NOW())\n AND CASE\n WHEN rt.priority_facet IS NULL THEN e.created_at\n WHEN pg_input_is_valid(e.metadata->>rt.priority_facet, 'timestamptz')\n THEN (e.metadata->>rt.priority_facet)::timestamptz\n ELSE NULL\n END < NOW() - (COALESCE(rt.priority_threshold_minutes, rt.sla_minutes) * INTERVAL '1 minute')\n ) AS priority_count\n FROM hmsh_escalations e\n JOIN role_targets rt ON rt.role = e.role\n WHERE e.status = 'pending'\n GROUP BY e.role\n)\nSELECT\n rt.role,\n COALESCE(lc.pending, 0) AS pending,\n COALESCE(lc.claimed, 0) AS claimed,\n COALESCE(lc.priority_count, 0) AS priority_count\nFROM role_targets rt\nLEFT JOIN live_counts lc ON lc.role = rt.role\nORDER BY rt.role\n";
|
|
57
74
|
/**
|
|
58
75
|
* Period metrics — resolved count, throughput efficiency, and P99/P50/avg/max
|
|
59
76
|
* latency for wait (queue time) and work (processing) per role.
|
|
@@ -192,7 +192,7 @@ LEFT JOIN resolved ON resolved.id = target.id`;
|
|
|
192
192
|
// path is wasteful: percentile aggregates sort every qualifying resolved row,
|
|
193
193
|
// while the live counts are cheap and bounded. So the two halves are separated:
|
|
194
194
|
//
|
|
195
|
-
// STATION_LIVE_COUNTS_SQL — pending / claimed /
|
|
195
|
+
// STATION_LIVE_COUNTS_SQL — pending / claimed / priority_count. Cheap, always
|
|
196
196
|
// run fresh. Scans only pending rows (backlog is
|
|
197
197
|
// bounded no matter how much history accrues).
|
|
198
198
|
// STATION_PERIOD_METRICS_SQL — resolved / throughput / latency percentiles.
|
|
@@ -204,17 +204,34 @@ LEFT JOIN resolved ON resolved.id = target.id`;
|
|
|
204
204
|
// and the service merges them by role into one StationMetric per station.
|
|
205
205
|
// ---------------------------------------------------------------------------
|
|
206
206
|
/**
|
|
207
|
-
* Live counts — pending, active claims, and past-
|
|
207
|
+
* Live counts — pending, active claims, and priority (past-threshold) per role.
|
|
208
208
|
*
|
|
209
209
|
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
210
210
|
*
|
|
211
211
|
* Hits idx_hmsh_esc_pending_role_created (role, created_at DESC) WHERE
|
|
212
212
|
* status='pending'. The pending working set is bounded by real backlog depth,
|
|
213
213
|
* so this stays cheap as historical volume grows — safe on the hot refresh path.
|
|
214
|
+
*
|
|
215
|
+
* priority_count is the Pace Board rebalance signal: pending AND unclaimed
|
|
216
|
+
* items older than the role's threshold. One signal, two per-role dials with
|
|
217
|
+
* fallbacks so it works from sla_minutes alone:
|
|
218
|
+
*
|
|
219
|
+
* age origin — priority_facet metadata key (an ISO 8601 UTC timestamp, e.g.
|
|
220
|
+
* the order's authorized date) when set, else created_at.
|
|
221
|
+
* threshold — priority_threshold_minutes when set, else sla_minutes.
|
|
222
|
+
*
|
|
223
|
+
* Claimed items are excluded — they are already in someone's hands; the count
|
|
224
|
+
* is what the floor must still pull forward. The GIN metadata index only
|
|
225
|
+
* serves containment, not age comparisons, so the facet read rides the same
|
|
226
|
+
* bounded pending scan as the other counts. The cast is guarded by
|
|
227
|
+
* pg_input_is_valid inside a CASE (Postgres does not promise AND evaluation
|
|
228
|
+
* order) so one malformed metadata value can never break the stats query;
|
|
229
|
+
* with a facet configured, items missing the key or holding an unparseable
|
|
230
|
+
* value do not count (NULL age never passes the comparison).
|
|
214
231
|
*/
|
|
215
232
|
exports.STATION_LIVE_COUNTS_SQL = `
|
|
216
233
|
WITH role_targets AS (
|
|
217
|
-
SELECT role, sla_minutes
|
|
234
|
+
SELECT role, sla_minutes, priority_threshold_minutes, priority_facet
|
|
218
235
|
FROM lt_roles
|
|
219
236
|
WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))
|
|
220
237
|
),
|
|
@@ -224,9 +241,15 @@ live_counts AS (
|
|
|
224
241
|
COUNT(*) AS pending,
|
|
225
242
|
COUNT(*) FILTER (WHERE e.assigned_to IS NOT NULL AND e.assigned_until > NOW()) AS claimed,
|
|
226
243
|
COUNT(*) FILTER (
|
|
227
|
-
WHERE rt.sla_minutes IS NOT NULL
|
|
228
|
-
AND e.
|
|
229
|
-
|
|
244
|
+
WHERE COALESCE(rt.priority_threshold_minutes, rt.sla_minutes) IS NOT NULL
|
|
245
|
+
AND (e.assigned_to IS NULL OR e.assigned_until IS NULL OR e.assigned_until <= NOW())
|
|
246
|
+
AND CASE
|
|
247
|
+
WHEN rt.priority_facet IS NULL THEN e.created_at
|
|
248
|
+
WHEN pg_input_is_valid(e.metadata->>rt.priority_facet, 'timestamptz')
|
|
249
|
+
THEN (e.metadata->>rt.priority_facet)::timestamptz
|
|
250
|
+
ELSE NULL
|
|
251
|
+
END < NOW() - (COALESCE(rt.priority_threshold_minutes, rt.sla_minutes) * INTERVAL '1 minute')
|
|
252
|
+
) AS priority_count
|
|
230
253
|
FROM hmsh_escalations e
|
|
231
254
|
JOIN role_targets rt ON rt.role = e.role
|
|
232
255
|
WHERE e.status = 'pending'
|
|
@@ -234,9 +257,9 @@ live_counts AS (
|
|
|
234
257
|
)
|
|
235
258
|
SELECT
|
|
236
259
|
rt.role,
|
|
237
|
-
COALESCE(lc.pending,
|
|
238
|
-
COALESCE(lc.claimed,
|
|
239
|
-
COALESCE(lc.
|
|
260
|
+
COALESCE(lc.pending, 0) AS pending,
|
|
261
|
+
COALESCE(lc.claimed, 0) AS claimed,
|
|
262
|
+
COALESCE(lc.priority_count, 0) AS priority_count
|
|
240
263
|
FROM role_targets rt
|
|
241
264
|
LEFT JOIN live_counts lc ON lc.role = rt.role
|
|
242
265
|
ORDER BY rt.role
|
|
@@ -50,7 +50,13 @@ export interface StationMetric {
|
|
|
50
50
|
pending: number;
|
|
51
51
|
claimed: number;
|
|
52
52
|
resolved: number;
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Pending, unclaimed items past the role's threshold — the Pace Board
|
|
55
|
+
* rebalance signal. Age is measured from the role's priority_facet metadata
|
|
56
|
+
* timestamp (created_at when unset) against priority_threshold_minutes
|
|
57
|
+
* (sla_minutes when unset). 0 when neither threshold is configured.
|
|
58
|
+
*/
|
|
59
|
+
priority_count: number;
|
|
54
60
|
/** resolved_in_period / (target_per_hour × period_hours) × 100. Null when no target set. */
|
|
55
61
|
throughput_pct: number | null;
|
|
56
62
|
wait: StationMetricPeriod;
|
|
@@ -139,6 +139,8 @@ async function updateRoleMetadata(role, input) {
|
|
|
139
139
|
provided('sla_minutes'), input.sla_minutes ?? null,
|
|
140
140
|
provided('target_per_hour'), input.target_per_hour ?? null,
|
|
141
141
|
provided('worker_count'), input.worker_count ?? null,
|
|
142
|
+
provided('priority_threshold_minutes'), input.priority_threshold_minutes ?? null,
|
|
143
|
+
provided('priority_facet'), input.priority_facet ?? null,
|
|
142
144
|
input.change_summary ?? null,
|
|
143
145
|
provided('upstream_roles'), upstreams,
|
|
144
146
|
]);
|
|
@@ -27,15 +27,15 @@ export declare const DELETE_ROLE = "\n DELETE FROM lt_roles WHERE role = $1";
|
|
|
27
27
|
* evaluated against the pre-update row), current_schema_version advances and
|
|
28
28
|
* the post-update schema pair is snapshotted into lt_role_schemas. A no-change
|
|
29
29
|
* save leaves the version alone and the snapshot INSERT conflicts away.
|
|
30
|
-
* $
|
|
30
|
+
* $26 = optional change summary recorded on the snapshot.
|
|
31
31
|
*
|
|
32
|
-
* Upstream inputs ($
|
|
32
|
+
* Upstream inputs ($27 = provided sentinel, $28 = replacement set) sync in the
|
|
33
33
|
* same statement with replace semantics. The two CTEs deliberately avoid
|
|
34
34
|
* touching the same row: the DELETE only removes rows leaving the set, and the
|
|
35
35
|
* INSERT's ON CONFLICT DO NOTHING skips rows that stay (delete+insert of the
|
|
36
36
|
* SAME key in one statement would silently drop it — CTEs share a snapshot).
|
|
37
37
|
*/
|
|
38
|
-
export declare const UPDATE_ROLE_METADATA = "\n WITH updated AS (\n UPDATE lt_roles SET\n title = CASE WHEN $2::boolean THEN $3 ELSE title END,\n description = CASE WHEN $4::boolean THEN $5 ELSE description END,\n form_schema = CASE WHEN $6::boolean THEN $7::jsonb ELSE form_schema END,\n metadata_schema = CASE WHEN $8::boolean THEN $9::jsonb ELSE metadata_schema END,\n properties = CASE WHEN $10::boolean THEN COALESCE($11::jsonb, '{}'::jsonb) ELSE properties END,\n ops_visible = CASE WHEN $12::boolean THEN $13::boolean ELSE ops_visible END,\n parent_role = CASE WHEN $14::boolean THEN $15 ELSE parent_role END,\n sla_minutes = CASE WHEN $16::boolean THEN $17::numeric ELSE sla_minutes END,\n target_per_hour = CASE WHEN $18::boolean THEN $19::numeric ELSE target_per_hour END,\n worker_count = CASE WHEN $20::boolean THEN $21::int ELSE worker_count END,\n current_schema_version = CASE\n WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)\n OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)\n THEN COALESCE(current_schema_version, 0) + 1\n ELSE current_schema_version END\n WHERE role = $1\n RETURNING\n role, title, description, form_schema, metadata_schema, properties,\n ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,\n current_schema_version\n ), snapshot AS (\n INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)\n SELECT role, current_schema_version, form_schema, metadata_schema, $
|
|
38
|
+
export declare const UPDATE_ROLE_METADATA = "\n WITH updated AS (\n UPDATE lt_roles SET\n title = CASE WHEN $2::boolean THEN $3 ELSE title END,\n description = CASE WHEN $4::boolean THEN $5 ELSE description END,\n form_schema = CASE WHEN $6::boolean THEN $7::jsonb ELSE form_schema END,\n metadata_schema = CASE WHEN $8::boolean THEN $9::jsonb ELSE metadata_schema END,\n properties = CASE WHEN $10::boolean THEN COALESCE($11::jsonb, '{}'::jsonb) ELSE properties END,\n ops_visible = CASE WHEN $12::boolean THEN $13::boolean ELSE ops_visible END,\n parent_role = CASE WHEN $14::boolean THEN $15 ELSE parent_role END,\n sla_minutes = CASE WHEN $16::boolean THEN $17::numeric ELSE sla_minutes END,\n target_per_hour = CASE WHEN $18::boolean THEN $19::numeric ELSE target_per_hour END,\n worker_count = CASE WHEN $20::boolean THEN $21::int ELSE worker_count END,\n priority_threshold_minutes\n = CASE WHEN $22::boolean THEN $23::numeric ELSE priority_threshold_minutes END,\n priority_facet = CASE WHEN $24::boolean THEN $25 ELSE priority_facet END,\n current_schema_version = CASE\n WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)\n OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)\n THEN COALESCE(current_schema_version, 0) + 1\n ELSE current_schema_version END\n WHERE role = $1\n RETURNING\n role, title, description, form_schema, metadata_schema, properties,\n ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,\n priority_threshold_minutes, priority_facet,\n current_schema_version\n ), snapshot AS (\n INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)\n SELECT role, current_schema_version, form_schema, metadata_schema, $26\n FROM updated\n WHERE ($6::boolean OR $8::boolean) AND current_schema_version IS NOT NULL\n ON CONFLICT (role, version) DO NOTHING\n ), upstream_prune AS (\n DELETE FROM lt_role_upstreams\n WHERE $27::boolean AND role = $1 AND upstream_role <> ALL($28::text[])\n ), upstream_add AS (\n INSERT INTO lt_role_upstreams (role, upstream_role)\n SELECT $1, u FROM unnest($28::text[]) AS u\n WHERE $27::boolean\n ON CONFLICT DO NOTHING\n )\n SELECT * FROM updated";
|
|
39
39
|
export declare const GET_ROLE_FORM_SCHEMA = "\n SELECT form_schema FROM lt_roles WHERE role = $1";
|
|
40
40
|
export declare const GET_ROLE_METADATA_SCHEMA = "\n SELECT metadata_schema FROM lt_roles WHERE role = $1";
|
|
41
41
|
export declare const GET_ROLE_UPSTREAMS = "\n SELECT upstream_role FROM lt_role_upstreams WHERE role = $1 ORDER BY upstream_role";
|
|
@@ -46,7 +46,7 @@ export declare const GET_ROLE_SCHEMA_VERSION = "\n SELECT s.role, s.version, s.
|
|
|
46
46
|
* versioned its schema still answers, with version NULL).
|
|
47
47
|
*/
|
|
48
48
|
export declare const GET_ROLE_SCHEMA_CURRENT = "\n SELECT r.role, r.current_schema_version AS version, r.form_schema,\n r.metadata_schema, s.change_summary, s.created_at,\n r.current_schema_version AS latest_version\n FROM lt_roles r\n LEFT JOIN lt_role_schemas s\n ON s.role = r.role AND s.version = r.current_schema_version\n WHERE r.role = $1";
|
|
49
|
-
export declare const LIST_ROLES_WITH_DETAILS = "\n WITH\n user_counts AS (\n SELECT role, COUNT(DISTINCT user_id)::int AS cnt\n FROM lt_user_roles\n GROUP BY role\n ),\n chain_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM (\n SELECT source_role AS role FROM lt_config_role_escalations\n UNION ALL\n SELECT target_role AS role FROM lt_config_role_escalations\n ) c\n GROUP BY role\n ),\n workflow_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM lt_config_roles\n GROUP BY role\n ),\n upstreams AS (\n SELECT role, array_agg(upstream_role ORDER BY upstream_role) AS ups\n FROM lt_role_upstreams\n GROUP BY role\n )\n SELECT\n r.role,\n r.title,\n r.description,\n r.form_schema,\n r.metadata_schema,\n r.properties,\n r.ops_visible,\n r.parent_role,\n r.sla_minutes,\n r.target_per_hour,\n r.worker_count,\n r.current_schema_version,\n COALESCE(up.ups, '{}') AS upstream_roles,\n COALESCE(uc.cnt, 0) AS user_count,\n COALESCE(cc.cnt, 0) AS chain_count,\n COALESCE(wc.cnt, 0) AS workflow_count\n FROM lt_roles r\n LEFT JOIN user_counts uc ON uc.role = r.role\n LEFT JOIN chain_counts cc ON cc.role = r.role\n LEFT JOIN workflow_counts wc ON wc.role = r.role\n LEFT JOIN upstreams up ON up.role = r.role\n ORDER BY r.role";
|
|
49
|
+
export declare const LIST_ROLES_WITH_DETAILS = "\n WITH\n user_counts AS (\n SELECT role, COUNT(DISTINCT user_id)::int AS cnt\n FROM lt_user_roles\n GROUP BY role\n ),\n chain_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM (\n SELECT source_role AS role FROM lt_config_role_escalations\n UNION ALL\n SELECT target_role AS role FROM lt_config_role_escalations\n ) c\n GROUP BY role\n ),\n workflow_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM lt_config_roles\n GROUP BY role\n ),\n upstreams AS (\n SELECT role, array_agg(upstream_role ORDER BY upstream_role) AS ups\n FROM lt_role_upstreams\n GROUP BY role\n )\n SELECT\n r.role,\n r.title,\n r.description,\n r.form_schema,\n r.metadata_schema,\n r.properties,\n r.ops_visible,\n r.parent_role,\n r.sla_minutes,\n r.target_per_hour,\n r.worker_count,\n r.priority_threshold_minutes,\n r.priority_facet,\n r.current_schema_version,\n COALESCE(up.ups, '{}') AS upstream_roles,\n COALESCE(uc.cnt, 0) AS user_count,\n COALESCE(cc.cnt, 0) AS chain_count,\n COALESCE(wc.cnt, 0) AS workflow_count\n FROM lt_roles r\n LEFT JOIN user_counts uc ON uc.role = r.role\n LEFT JOIN chain_counts cc ON cc.role = r.role\n LEFT JOIN workflow_counts wc ON wc.role = r.role\n LEFT JOIN upstreams up ON up.role = r.role\n ORDER BY r.role";
|
|
50
50
|
export declare const COUNT_USER_ROLE_REFS = "\n SELECT COUNT(*)::int AS cnt FROM lt_user_roles WHERE role = $1";
|
|
51
51
|
export declare const COUNT_CHAIN_REFS = "\n SELECT COUNT(*)::int AS cnt\n FROM lt_config_role_escalations\n WHERE source_role = $1 OR target_role = $1";
|
|
52
52
|
export declare const COUNT_WORKFLOW_REFS = "\n SELECT COUNT(*)::int AS cnt FROM lt_config_roles WHERE role = $1";
|
|
@@ -61,9 +61,9 @@ exports.DELETE_ROLE = `
|
|
|
61
61
|
* evaluated against the pre-update row), current_schema_version advances and
|
|
62
62
|
* the post-update schema pair is snapshotted into lt_role_schemas. A no-change
|
|
63
63
|
* save leaves the version alone and the snapshot INSERT conflicts away.
|
|
64
|
-
* $
|
|
64
|
+
* $26 = optional change summary recorded on the snapshot.
|
|
65
65
|
*
|
|
66
|
-
* Upstream inputs ($
|
|
66
|
+
* Upstream inputs ($27 = provided sentinel, $28 = replacement set) sync in the
|
|
67
67
|
* same statement with replace semantics. The two CTEs deliberately avoid
|
|
68
68
|
* touching the same row: the DELETE only removes rows leaving the set, and the
|
|
69
69
|
* INSERT's ON CONFLICT DO NOTHING skips rows that stay (delete+insert of the
|
|
@@ -82,6 +82,9 @@ exports.UPDATE_ROLE_METADATA = `
|
|
|
82
82
|
sla_minutes = CASE WHEN $16::boolean THEN $17::numeric ELSE sla_minutes END,
|
|
83
83
|
target_per_hour = CASE WHEN $18::boolean THEN $19::numeric ELSE target_per_hour END,
|
|
84
84
|
worker_count = CASE WHEN $20::boolean THEN $21::int ELSE worker_count END,
|
|
85
|
+
priority_threshold_minutes
|
|
86
|
+
= CASE WHEN $22::boolean THEN $23::numeric ELSE priority_threshold_minutes END,
|
|
87
|
+
priority_facet = CASE WHEN $24::boolean THEN $25 ELSE priority_facet END,
|
|
85
88
|
current_schema_version = CASE
|
|
86
89
|
WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)
|
|
87
90
|
OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)
|
|
@@ -91,20 +94,21 @@ exports.UPDATE_ROLE_METADATA = `
|
|
|
91
94
|
RETURNING
|
|
92
95
|
role, title, description, form_schema, metadata_schema, properties,
|
|
93
96
|
ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,
|
|
97
|
+
priority_threshold_minutes, priority_facet,
|
|
94
98
|
current_schema_version
|
|
95
99
|
), snapshot AS (
|
|
96
100
|
INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)
|
|
97
|
-
SELECT role, current_schema_version, form_schema, metadata_schema, $
|
|
101
|
+
SELECT role, current_schema_version, form_schema, metadata_schema, $26
|
|
98
102
|
FROM updated
|
|
99
103
|
WHERE ($6::boolean OR $8::boolean) AND current_schema_version IS NOT NULL
|
|
100
104
|
ON CONFLICT (role, version) DO NOTHING
|
|
101
105
|
), upstream_prune AS (
|
|
102
106
|
DELETE FROM lt_role_upstreams
|
|
103
|
-
WHERE $
|
|
107
|
+
WHERE $27::boolean AND role = $1 AND upstream_role <> ALL($28::text[])
|
|
104
108
|
), upstream_add AS (
|
|
105
109
|
INSERT INTO lt_role_upstreams (role, upstream_role)
|
|
106
|
-
SELECT $1, u FROM unnest($
|
|
107
|
-
WHERE $
|
|
110
|
+
SELECT $1, u FROM unnest($28::text[]) AS u
|
|
111
|
+
WHERE $27::boolean
|
|
108
112
|
ON CONFLICT DO NOTHING
|
|
109
113
|
)
|
|
110
114
|
SELECT * FROM updated`;
|
|
@@ -184,6 +188,8 @@ exports.LIST_ROLES_WITH_DETAILS = `
|
|
|
184
188
|
r.sla_minutes,
|
|
185
189
|
r.target_per_hour,
|
|
186
190
|
r.worker_count,
|
|
191
|
+
r.priority_threshold_minutes,
|
|
192
|
+
r.priority_facet,
|
|
187
193
|
r.current_schema_version,
|
|
188
194
|
COALESCE(up.ups, '{}') AS upstream_roles,
|
|
189
195
|
COALESCE(uc.cnt, 0) AS user_count,
|
|
@@ -25,6 +25,18 @@ export interface RoleDetail {
|
|
|
25
25
|
target_per_hour: number | null;
|
|
26
26
|
/** Capacity at this station (people or machines). Part of the ops triangle. */
|
|
27
27
|
worker_count: number | null;
|
|
28
|
+
/**
|
|
29
|
+
* Max age (minutes) before a pending, unclaimed escalation counts toward the
|
|
30
|
+
* Pace Board priority count. Falls back to sla_minutes when null.
|
|
31
|
+
*/
|
|
32
|
+
priority_threshold_minutes: number | null;
|
|
33
|
+
/**
|
|
34
|
+
* lt_escalations.metadata key holding the age origin for the priority count
|
|
35
|
+
* as an ISO 8601 UTC timestamp (e.g. the order's authorized date). Falls
|
|
36
|
+
* back to created_at when null. When set, items missing the key or holding
|
|
37
|
+
* an unparseable value are not counted.
|
|
38
|
+
*/
|
|
39
|
+
priority_facet: string | null;
|
|
28
40
|
/**
|
|
29
41
|
* Version of the live form_schema/metadata_schema pair. Advances whenever a
|
|
30
42
|
* schema field changes; each version is snapshotted in lt_role_schemas so
|
|
@@ -74,6 +86,8 @@ export interface UpdateRoleInput {
|
|
|
74
86
|
sla_minutes?: number | null;
|
|
75
87
|
target_per_hour?: number | null;
|
|
76
88
|
worker_count?: number | null;
|
|
89
|
+
priority_threshold_minutes?: number | null;
|
|
90
|
+
priority_facet?: string | null;
|
|
77
91
|
/**
|
|
78
92
|
* Replace the upstream-input set (omitted = preserve; null or [] = clear).
|
|
79
93
|
* Every entry must name an existing role other than this one.
|
|
@@ -562,6 +562,8 @@ export declare const updateRoleSchema: z.ZodObject<{
|
|
|
562
562
|
sla_minutes: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
563
563
|
target_per_hour: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
564
564
|
worker_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
565
|
+
priority_threshold_minutes: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
566
|
+
priority_facet: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
565
567
|
upstream_roles: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
566
568
|
change_summary: z.ZodOptional<z.ZodString>;
|
|
567
569
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -576,6 +578,8 @@ export declare const updateRoleSchema: z.ZodObject<{
|
|
|
576
578
|
sla_minutes?: number | null | undefined;
|
|
577
579
|
target_per_hour?: number | null | undefined;
|
|
578
580
|
worker_count?: number | null | undefined;
|
|
581
|
+
priority_threshold_minutes?: number | null | undefined;
|
|
582
|
+
priority_facet?: string | null | undefined;
|
|
579
583
|
upstream_roles?: string[] | null | undefined;
|
|
580
584
|
change_summary?: string | undefined;
|
|
581
585
|
}, {
|
|
@@ -590,6 +594,8 @@ export declare const updateRoleSchema: z.ZodObject<{
|
|
|
590
594
|
sla_minutes?: number | null | undefined;
|
|
591
595
|
target_per_hour?: number | null | undefined;
|
|
592
596
|
worker_count?: number | null | undefined;
|
|
597
|
+
priority_threshold_minutes?: number | null | undefined;
|
|
598
|
+
priority_facet?: string | null | undefined;
|
|
593
599
|
upstream_roles?: string[] | null | undefined;
|
|
594
600
|
change_summary?: string | undefined;
|
|
595
601
|
}>;
|