@hotmeshio/long-tail 0.10.13 → 0.11.0

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.
Files changed (373) hide show
  1. package/README.md +2 -4
  2. package/build/api/escalations/analytics.d.ts +6 -0
  3. package/build/api/escalations/analytics.js +123 -0
  4. package/build/api/escalations/index.d.ts +1 -0
  5. package/build/api/escalations/index.js +4 -1
  6. package/build/api/escalations/metadata.js +1 -1
  7. package/build/api/escalations/resolve.js +4 -4
  8. package/build/api/roles.d.ts +4 -0
  9. package/build/api/roles.js +8 -0
  10. package/build/api/scan-codes/choice.d.ts +11 -0
  11. package/build/api/scan-codes/choice.js +136 -0
  12. package/build/api/scan-codes/context.d.ts +24 -0
  13. package/build/api/scan-codes/context.js +86 -0
  14. package/build/api/scan-codes/execute.d.ts +1 -0
  15. package/build/api/scan-codes/execute.js +49 -244
  16. package/build/api/scan-codes/identity.d.ts +18 -0
  17. package/build/api/scan-codes/identity.js +116 -0
  18. package/build/api/scan-codes/index.d.ts +2 -0
  19. package/build/api/scan-codes/index.js +6 -1
  20. package/build/api/scan-codes/locate.d.ts +14 -0
  21. package/build/api/scan-codes/locate.js +127 -0
  22. package/build/api/scan-codes/present.d.ts +9 -0
  23. package/build/api/scan-codes/present.js +59 -0
  24. package/build/api/scan-codes/verbs.d.ts +15 -0
  25. package/build/api/scan-codes/verbs.js +205 -0
  26. package/build/bin/ltc.js +33 -0
  27. package/build/lib/cli/commands/escalations.d.ts +31 -0
  28. package/build/lib/cli/commands/escalations.js +88 -0
  29. package/build/lib/db/schemas/028_role_entity_facet.sql +18 -0
  30. package/build/lib/db/schemas/029_scan_scheme_kind.sql +31 -0
  31. package/build/modules/config.d.ts +5 -0
  32. package/build/modules/config.js +12 -0
  33. package/build/routes/escalations/acting.d.ts +12 -0
  34. package/build/routes/escalations/acting.js +25 -0
  35. package/build/routes/escalations/analytics.d.ts +8 -0
  36. package/build/routes/escalations/analytics.js +63 -0
  37. package/build/routes/escalations/index.js +3 -0
  38. package/build/routes/escalations/resolve.js +8 -1
  39. package/build/routes/escalations/single.js +13 -4
  40. package/build/routes/roles.js +1 -1
  41. package/build/routes/scan-codes.js +28 -2
  42. package/build/scripts/ortho-populate.js +14 -0
  43. package/build/scripts/printer-cycle.d.ts +25 -0
  44. package/build/scripts/printer-cycle.js +169 -0
  45. package/build/scripts/twin-farm.js +19 -5
  46. package/build/sdk/index.d.ts +2 -0
  47. package/build/sdk/index.js +3 -0
  48. package/build/services/escalation/aggregate-sql.d.ts +36 -0
  49. package/build/services/escalation/aggregate-sql.js +253 -0
  50. package/build/services/escalation/aggregate-validate.d.ts +57 -0
  51. package/build/services/escalation/aggregate-validate.js +218 -0
  52. package/build/services/escalation/aggregates.d.ts +26 -0
  53. package/build/services/escalation/aggregates.js +204 -0
  54. package/build/services/escalation/client.js +57 -21
  55. package/build/services/escalation/facet-sql.js +17 -0
  56. package/build/services/escalation/index.d.ts +2 -0
  57. package/build/services/escalation/index.js +8 -1
  58. package/build/services/escalation/metrics-cache.d.ts +18 -0
  59. package/build/services/escalation/metrics-cache.js +47 -1
  60. package/build/services/escalation/queries.js +2 -0
  61. package/build/services/escalation/resolver-validation.d.ts +1 -1
  62. package/build/services/escalation/resolver-validation.js +12 -5
  63. package/build/services/escalation/submit-guard.d.ts +29 -0
  64. package/build/services/escalation/submit-guard.js +57 -0
  65. package/build/services/iam/acting-identity.d.ts +16 -0
  66. package/build/services/iam/acting-identity.js +60 -0
  67. package/build/services/role/index.js +2 -0
  68. package/build/services/role/sql.d.ts +10 -2
  69. package/build/services/role/sql.js +14 -1
  70. package/build/services/role/types.d.ts +16 -0
  71. package/build/services/scan-code/read.js +4 -0
  72. package/build/services/scan-code/sql.d.ts +4 -4
  73. package/build/services/scan-code/sql.js +14 -8
  74. package/build/services/scan-code/validate.d.ts +2 -0
  75. package/build/services/scan-code/validate.js +86 -0
  76. package/build/services/scan-code/write.d.ts +4 -0
  77. package/build/services/scan-code/write.js +16 -2
  78. package/build/services/user/crud.d.ts +6 -0
  79. package/build/services/user/crud.js +16 -0
  80. package/build/services/user/index.d.ts +1 -1
  81. package/build/services/user/index.js +2 -1
  82. package/build/services/user/sql.d.ts +8 -0
  83. package/build/services/user/sql.js +9 -1
  84. package/build/shared/form-validation/index.d.ts +4 -0
  85. package/build/shared/form-validation/index.js +4 -0
  86. package/build/shared/form-validation/x-lt-labels.d.ts +28 -0
  87. package/build/shared/form-validation/x-lt-labels.js +42 -0
  88. package/build/shared/form-validation/x-lt-query.d.ts +56 -0
  89. package/build/shared/form-validation/x-lt-query.js +49 -0
  90. package/build/shared/form-validation/x-lt-submit-guard.d.ts +46 -0
  91. package/build/shared/form-validation/x-lt-submit-guard.js +25 -0
  92. package/build/shared/form-validation/x-lt-submit-on-claim.d.ts +22 -0
  93. package/build/shared/form-validation/x-lt-submit-on-claim.js +28 -0
  94. package/build/system/mcp-servers/admin/escalations.js +34 -0
  95. package/build/system/mcp-servers/admin/scan-codes.js +17 -1
  96. package/build/system/mcp-servers/admin/schemas.d.ts +643 -25
  97. package/build/system/mcp-servers/admin/schemas.js +127 -19
  98. package/build/system/mcp-servers/admin/users.js +3 -1
  99. package/build/system/mcp-servers/human-queue-schemas.d.ts +2 -2
  100. package/build/system/seed/tool-manifests-admin.d.ts +2283 -0
  101. package/build/system/seed/tool-manifests-admin.js +8 -5
  102. package/build/tsconfig.tsbuildinfo +1 -1
  103. package/build/types/analytics.d.ts +154 -0
  104. package/build/types/analytics.js +14 -0
  105. package/build/types/facets.d.ts +13 -0
  106. package/build/types/index.d.ts +5 -2
  107. package/build/types/index.js +5 -1
  108. package/build/types/scan-choice.d.ts +49 -0
  109. package/build/types/scan-choice.js +9 -0
  110. package/build/types/scan-code.d.ts +72 -1
  111. package/build/types/scan-code.js +30 -1
  112. package/dashboard/dist/assets/{AdminDashboard-Bv3pHBzn.js → AdminDashboard-DJ3LJQ09.js} +2 -2
  113. package/dashboard/dist/assets/{AdminDashboard-Bv3pHBzn.js.map → AdminDashboard-DJ3LJQ09.js.map} +1 -1
  114. package/dashboard/dist/assets/{AgentConfigPage-Dk2zlvDb.js → AgentConfigPage-BMMvaD0z.js} +2 -2
  115. package/dashboard/dist/assets/{AgentConfigPage-Dk2zlvDb.js.map → AgentConfigPage-BMMvaD0z.js.map} +1 -1
  116. package/dashboard/dist/assets/{AgentDetailPage-DUno6EGs.js → AgentDetailPage-C6wfsnRe.js} +2 -2
  117. package/dashboard/dist/assets/{AgentDetailPage-DUno6EGs.js.map → AgentDetailPage-C6wfsnRe.js.map} +1 -1
  118. package/dashboard/dist/assets/AgentsPage-Cwqka7nE.js +2 -0
  119. package/dashboard/dist/assets/{AgentsPage-CToVu1qj.js.map → AgentsPage-Cwqka7nE.js.map} +1 -1
  120. package/dashboard/dist/assets/AvailableEscalationsPage-CVlG_AVC.js +2 -0
  121. package/dashboard/dist/assets/AvailableEscalationsPage-CVlG_AVC.js.map +1 -0
  122. package/dashboard/dist/assets/{BotPicker-DQOpVB4M.js → BotPicker-BBkJBXZB.js} +2 -2
  123. package/dashboard/dist/assets/{BotPicker-DQOpVB4M.js.map → BotPicker-BBkJBXZB.js.map} +1 -1
  124. package/dashboard/dist/assets/{CapabilitiesPage-Dmf46PUJ.js → CapabilitiesPage-wxmE4917.js} +2 -2
  125. package/dashboard/dist/assets/{CapabilitiesPage-Dmf46PUJ.js.map → CapabilitiesPage-wxmE4917.js.map} +1 -1
  126. package/dashboard/dist/assets/{CollapsibleSection-CDADefUY.js → CollapsibleSection-B3FsrHwH.js} +2 -2
  127. package/dashboard/dist/assets/{CollapsibleSection-CDADefUY.js.map → CollapsibleSection-B3FsrHwH.js.map} +1 -1
  128. package/dashboard/dist/assets/CountdownTimer-B3nCqJdC.js +2 -0
  129. package/dashboard/dist/assets/CountdownTimer-B3nCqJdC.js.map +1 -0
  130. package/dashboard/dist/assets/{CredentialsPage-8LIEvomi.js → CredentialsPage-Bv875IHe.js} +2 -2
  131. package/dashboard/dist/assets/{CredentialsPage-8LIEvomi.js.map → CredentialsPage-Bv875IHe.js.map} +1 -1
  132. package/dashboard/dist/assets/{CronLabel-CK3WSsM5.js → CronLabel-CSzjWuwj.js} +2 -2
  133. package/dashboard/dist/assets/{CronLabel-CK3WSsM5.js.map → CronLabel-CSzjWuwj.js.map} +1 -1
  134. package/dashboard/dist/assets/{CustomDurationPicker-FPmlA6cE.js → CustomDurationPicker-CCpqf3e4.js} +2 -2
  135. package/dashboard/dist/assets/{CustomDurationPicker-FPmlA6cE.js.map → CustomDurationPicker-CCpqf3e4.js.map} +1 -1
  136. package/dashboard/dist/assets/{DropZone-CL9gA7pu.js → DropZone-CJkk-U3o.js} +2 -2
  137. package/dashboard/dist/assets/{DropZone-CL9gA7pu.js.map → DropZone-CJkk-U3o.js.map} +1 -1
  138. package/dashboard/dist/assets/{ElapsedCell-BZVkVxE0.js → ElapsedCell-WV6pUaKo.js} +2 -2
  139. package/dashboard/dist/assets/{ElapsedCell-BZVkVxE0.js.map → ElapsedCell-WV6pUaKo.js.map} +1 -1
  140. package/dashboard/dist/assets/EntityTimelinePanel-DrL86Yrf.js +2 -0
  141. package/dashboard/dist/assets/EntityTimelinePanel-DrL86Yrf.js.map +1 -0
  142. package/dashboard/dist/assets/{EscalationListSchemaPage-kYLdUtZX.js → EscalationListSchemaPage-CTQCALw4.js} +3 -3
  143. package/dashboard/dist/assets/{EscalationListSchemaPage-kYLdUtZX.js.map → EscalationListSchemaPage-CTQCALw4.js.map} +1 -1
  144. package/dashboard/dist/assets/{EscalationSchemaPage-kUmCDivo.js → EscalationSchemaPage-D_UgP0pv.js} +3 -3
  145. package/dashboard/dist/assets/{EscalationSchemaPage-kUmCDivo.js.map → EscalationSchemaPage-D_UgP0pv.js.map} +1 -1
  146. package/dashboard/dist/assets/{EscalationsOverview-D17auO-m.js → EscalationsOverview-DuIUyxr0.js} +2 -2
  147. package/dashboard/dist/assets/{EscalationsOverview-D17auO-m.js.map → EscalationsOverview-DuIUyxr0.js.map} +1 -1
  148. package/dashboard/dist/assets/EventTable-BV6unphX.js +2 -0
  149. package/dashboard/dist/assets/EventTable-BV6unphX.js.map +1 -0
  150. package/dashboard/dist/assets/FilterBar-DYFgzXNB.js +2 -0
  151. package/dashboard/dist/assets/FilterBar-DYFgzXNB.js.map +1 -0
  152. package/dashboard/dist/assets/{GraphInvokePage-D11uOnZi.js → GraphInvokePage-BGHVUipJ.js} +2 -2
  153. package/dashboard/dist/assets/{GraphInvokePage-D11uOnZi.js.map → GraphInvokePage-BGHVUipJ.js.map} +1 -1
  154. package/dashboard/dist/assets/HomePage-B-S2cK3E.js +2 -0
  155. package/dashboard/dist/assets/{HomePage-Bcq10Bxx.js.map → HomePage-B-S2cK3E.js.map} +1 -1
  156. package/dashboard/dist/assets/ListToolbar-Bl7F3A28.js +2 -0
  157. package/dashboard/dist/assets/{ListToolbar-IPXsTb3Q.js.map → ListToolbar-Bl7F3A28.js.map} +1 -1
  158. package/dashboard/dist/assets/{McpOverview-Dx2j0KkF.js → McpOverview-_T9IsCD_.js} +2 -2
  159. package/dashboard/dist/assets/{McpOverview-Dx2j0KkF.js.map → McpOverview-_T9IsCD_.js.map} +1 -1
  160. package/dashboard/dist/assets/{McpQueryDetailPage-CzF_bna8.js → McpQueryDetailPage-DtsVqKPw.js} +2 -2
  161. package/dashboard/dist/assets/{McpQueryDetailPage-CzF_bna8.js.map → McpQueryDetailPage-DtsVqKPw.js.map} +1 -1
  162. package/dashboard/dist/assets/McpQueryPage-DX5u-1bx.js +2 -0
  163. package/dashboard/dist/assets/{McpQueryPage-CNsuiw_q.js.map → McpQueryPage-DX5u-1bx.js.map} +1 -1
  164. package/dashboard/dist/assets/McpRunDetailPage-BhcqqtYS.js +2 -0
  165. package/dashboard/dist/assets/McpRunDetailPage-BhcqqtYS.js.map +1 -0
  166. package/dashboard/dist/assets/{McpRunsPage-CK96s3Nt.js → McpRunsPage-L-1y8wnX.js} +2 -2
  167. package/dashboard/dist/assets/{McpRunsPage-CK96s3Nt.js.map → McpRunsPage-L-1y8wnX.js.map} +1 -1
  168. package/dashboard/dist/assets/{NamespacePill-B9Vc_Pz2.js → NamespacePill-in1SLd0W.js} +2 -2
  169. package/dashboard/dist/assets/{NamespacePill-B9Vc_Pz2.js.map → NamespacePill-in1SLd0W.js.map} +1 -1
  170. package/dashboard/dist/assets/OperationsPage-i8KcDpKo.js +3 -0
  171. package/dashboard/dist/assets/OperationsPage-i8KcDpKo.js.map +1 -0
  172. package/dashboard/dist/assets/OperatorDashboard-CIbDzOlh.js +2 -0
  173. package/dashboard/dist/assets/{OperatorDashboard-C-jE0IRA.js.map → OperatorDashboard-CIbDzOlh.js.map} +1 -1
  174. package/dashboard/dist/assets/{PageHeader-Ce4GcIYo.js → PageHeader-BJgWMZtw.js} +2 -2
  175. package/dashboard/dist/assets/{PageHeader-Ce4GcIYo.js.map → PageHeader-BJgWMZtw.js.map} +1 -1
  176. package/dashboard/dist/assets/{PageHeaderWithStats-Ck0KZvXD.js → PageHeaderWithStats-Ccio4vyG.js} +2 -2
  177. package/dashboard/dist/assets/{PageHeaderWithStats-Ck0KZvXD.js.map → PageHeaderWithStats-Ccio4vyG.js.map} +1 -1
  178. package/dashboard/dist/assets/{PersonaDetailPage-CJ6jqo7h.js → PersonaDetailPage-BOaf0TW1.js} +2 -2
  179. package/dashboard/dist/assets/{PersonaDetailPage-CJ6jqo7h.js.map → PersonaDetailPage-BOaf0TW1.js.map} +1 -1
  180. package/dashboard/dist/assets/{PersonasPage-D8hL3kL5.js → PersonasPage-DSdRb90f.js} +2 -2
  181. package/dashboard/dist/assets/{PersonasPage-D8hL3kL5.js.map → PersonasPage-DSdRb90f.js.map} +1 -1
  182. package/dashboard/dist/assets/{ProcessDetailPage-Vnx8Z5_y.js → ProcessDetailPage-DN9NqfeP.js} +2 -2
  183. package/dashboard/dist/assets/{ProcessDetailPage-Vnx8Z5_y.js.map → ProcessDetailPage-DN9NqfeP.js.map} +1 -1
  184. package/dashboard/dist/assets/{ProcessesListPage-DRYtvS3N.js → ProcessesListPage-CfQEWaD4.js} +2 -2
  185. package/dashboard/dist/assets/{ProcessesListPage-DRYtvS3N.js.map → ProcessesListPage-CfQEWaD4.js.map} +1 -1
  186. package/dashboard/dist/assets/RoleDetailPage-BC5HL7ls.js +8 -0
  187. package/dashboard/dist/assets/RoleDetailPage-BC5HL7ls.js.map +1 -0
  188. package/dashboard/dist/assets/{RolePill-CnuBPhPw.js → RolePill-BNAP-uK6.js} +2 -2
  189. package/dashboard/dist/assets/{RolePill-CnuBPhPw.js.map → RolePill-BNAP-uK6.js.map} +1 -1
  190. package/dashboard/dist/assets/RolesPage-DCnW0VmD.js +2 -0
  191. package/dashboard/dist/assets/RolesPage-DCnW0VmD.js.map +1 -0
  192. package/dashboard/dist/assets/RunAsSelector-twbuX7fB.js +2 -0
  193. package/dashboard/dist/assets/{RunAsSelector-CS3KQ-BF.js.map → RunAsSelector-twbuX7fB.js.map} +1 -1
  194. package/dashboard/dist/assets/ScanCodesPage-CPNg6K--.js +2 -0
  195. package/dashboard/dist/assets/ScanCodesPage-CPNg6K--.js.map +1 -0
  196. package/dashboard/dist/assets/ScanSchemeDetailPage-DyLmCamO.js +2 -0
  197. package/dashboard/dist/assets/ScanSchemeDetailPage-DyLmCamO.js.map +1 -0
  198. package/dashboard/dist/assets/ScanStationPage-DoZccU5B.js +2 -0
  199. package/dashboard/dist/assets/ScanStationPage-DoZccU5B.js.map +1 -0
  200. package/dashboard/dist/assets/{ScopeBadge-BUpYpS3x.js → ScopeBadge-BG7KkZ8Q.js} +2 -2
  201. package/dashboard/dist/assets/{ScopeBadge-BUpYpS3x.js.map → ScopeBadge-BG7KkZ8Q.js.map} +1 -1
  202. package/dashboard/dist/assets/SegmentedTabs-DXNzY05q.js +2 -0
  203. package/dashboard/dist/assets/SegmentedTabs-DXNzY05q.js.map +1 -0
  204. package/dashboard/dist/assets/StickyPagination-DIeYMPPd.js +2 -0
  205. package/dashboard/dist/assets/StickyPagination-DIeYMPPd.js.map +1 -0
  206. package/dashboard/dist/assets/{StreamMessageDetail-DyJyZwBl.js → StreamMessageDetail-gU4CUeQI.js} +2 -2
  207. package/dashboard/dist/assets/{StreamMessageDetail-DyJyZwBl.js.map → StreamMessageDetail-gU4CUeQI.js.map} +1 -1
  208. package/dashboard/dist/assets/{SwimlaneTimeline-BlzV74V5.js → SwimlaneTimeline-Br-nBA4l.js} +2 -2
  209. package/dashboard/dist/assets/{SwimlaneTimeline-BlzV74V5.js.map → SwimlaneTimeline-Br-nBA4l.js.map} +1 -1
  210. package/dashboard/dist/assets/{TagInput-BbZ25nNe.js → TagInput-CvM5n6ww.js} +2 -2
  211. package/dashboard/dist/assets/{TagInput-BbZ25nNe.js.map → TagInput-CvM5n6ww.js.map} +1 -1
  212. package/dashboard/dist/assets/{TaskDetailPage-B0p_Z4J_.js → TaskDetailPage-_9cr_R0u.js} +2 -2
  213. package/dashboard/dist/assets/{TaskDetailPage-B0p_Z4J_.js.map → TaskDetailPage-_9cr_R0u.js.map} +1 -1
  214. package/dashboard/dist/assets/{TaskQueuePill-D0QKBrN3.js → TaskQueuePill-D4JPHXOq.js} +2 -2
  215. package/dashboard/dist/assets/{TaskQueuePill-D0QKBrN3.js.map → TaskQueuePill-D4JPHXOq.js.map} +1 -1
  216. package/dashboard/dist/assets/{TasksListPage-CiGR2QiQ.js → TasksListPage-DVENC9Jp.js} +2 -2
  217. package/dashboard/dist/assets/{TasksListPage-CiGR2QiQ.js.map → TasksListPage-DVENC9Jp.js.map} +1 -1
  218. package/dashboard/dist/assets/{TimeAgo-D_DA9ucC.js → TimeAgo-C9yYnOui.js} +2 -2
  219. package/dashboard/dist/assets/{TimeAgo-D_DA9ucC.js.map → TimeAgo-C9yYnOui.js.map} +1 -1
  220. package/dashboard/dist/assets/{TimestampCell-C7rkC9nh.js → TimestampCell-DoB8cYgw.js} +2 -2
  221. package/dashboard/dist/assets/{TimestampCell-C7rkC9nh.js.map → TimestampCell-DoB8cYgw.js.map} +1 -1
  222. package/dashboard/dist/assets/{ToolPill-8xY9rkoc.js → ToolPill-BUjbC-Tj.js} +2 -2
  223. package/dashboard/dist/assets/{ToolPill-8xY9rkoc.js.map → ToolPill-BUjbC-Tj.js.map} +1 -1
  224. package/dashboard/dist/assets/{ToolTestPanel-E8efYGYs.js → ToolTestPanel-CBtsIP8K.js} +2 -2
  225. package/dashboard/dist/assets/{ToolTestPanel-E8efYGYs.js.map → ToolTestPanel-CBtsIP8K.js.map} +1 -1
  226. package/dashboard/dist/assets/{TopicDetailPage-B455XrRc.js → TopicDetailPage-mR4Jaz83.js} +4 -4
  227. package/dashboard/dist/assets/{TopicDetailPage-B455XrRc.js.map → TopicDetailPage-mR4Jaz83.js.map} +1 -1
  228. package/dashboard/dist/assets/TopicsPage-CP7HFxIC.js +2 -0
  229. package/dashboard/dist/assets/{TopicsPage-1VAc29Qw.js.map → TopicsPage-CP7HFxIC.js.map} +1 -1
  230. package/dashboard/dist/assets/{UserName-DikzjQHJ.js → UserName-BnLF6yhz.js} +2 -2
  231. package/dashboard/dist/assets/{UserName-DikzjQHJ.js.map → UserName-BnLF6yhz.js.map} +1 -1
  232. package/dashboard/dist/assets/WorkflowExecutionPage-xL956RZi.js +2 -0
  233. package/dashboard/dist/assets/WorkflowExecutionPage-xL956RZi.js.map +1 -0
  234. package/dashboard/dist/assets/{WorkflowPill-Da406nEj.js → WorkflowPill-tXU0-eXu.js} +2 -2
  235. package/dashboard/dist/assets/{WorkflowPill-Da406nEj.js.map → WorkflowPill-tXU0-eXu.js.map} +1 -1
  236. package/dashboard/dist/assets/WorkflowsDashboard-B-u7zFKM.js +2 -0
  237. package/dashboard/dist/assets/{WorkflowsDashboard-BMQd_0tW.js.map → WorkflowsDashboard-B-u7zFKM.js.map} +1 -1
  238. package/dashboard/dist/assets/{WorkflowsOverview-CdxjVifp.js → WorkflowsOverview-CqZVarK-.js} +2 -2
  239. package/dashboard/dist/assets/{WorkflowsOverview-CdxjVifp.js.map → WorkflowsOverview-CqZVarK-.js.map} +1 -1
  240. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DNVffIXa.js → YamlWorkflowDetailPage-DiFLavW0.js} +10 -10
  241. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DNVffIXa.js.map → YamlWorkflowDetailPage-DiFLavW0.js.map} +1 -1
  242. package/dashboard/dist/assets/{YamlWorkflowsPage-BGP8wuos.js → YamlWorkflowsPage-BuQM6uZA.js} +2 -2
  243. package/dashboard/dist/assets/{YamlWorkflowsPage-BGP8wuos.js.map → YamlWorkflowsPage-BuQM6uZA.js.map} +1 -1
  244. package/dashboard/dist/assets/{agents-BZJLIJ4h.js → agents-DU9GPovp.js} +2 -2
  245. package/dashboard/dist/assets/{agents-BZJLIJ4h.js.map → agents-DU9GPovp.js.map} +1 -1
  246. package/dashboard/dist/assets/{bots-ClLcDGJb.js → bots-Cy-Hmg7w.js} +2 -2
  247. package/dashboard/dist/assets/{bots-ClLcDGJb.js.map → bots-Cy-Hmg7w.js.map} +1 -1
  248. package/dashboard/dist/assets/capabilities-sSJJqrp-.js +2 -0
  249. package/dashboard/dist/assets/{capabilities-IaCcS7ge.js.map → capabilities-sSJJqrp-.js.map} +1 -1
  250. package/dashboard/dist/assets/{controlplane-Zi4m_MSx.js → controlplane-TVCjDHQ-.js} +2 -2
  251. package/dashboard/dist/assets/{controlplane-Zi4m_MSx.js.map → controlplane-TVCjDHQ-.js.map} +1 -1
  252. package/dashboard/dist/assets/escalation-columns-BpkjKYGd.js +2 -0
  253. package/dashboard/dist/assets/escalation-columns-BpkjKYGd.js.map +1 -0
  254. package/dashboard/dist/assets/index-5TblaCXQ.css +1 -0
  255. package/dashboard/dist/assets/index-7BP6FaCZ.js +2 -0
  256. package/dashboard/dist/assets/{index-Dldi3Q3M.js.map → index-7BP6FaCZ.js.map} +1 -1
  257. package/dashboard/dist/assets/{index-BzkE48xJ.js → index-B7IaBPBg.js} +4 -4
  258. package/dashboard/dist/assets/{index-BzkE48xJ.js.map → index-B7IaBPBg.js.map} +1 -1
  259. package/dashboard/dist/assets/index-BE0phFw0.js +2 -0
  260. package/dashboard/dist/assets/{index-CLmZaNTK.js.map → index-BE0phFw0.js.map} +1 -1
  261. package/dashboard/dist/assets/{index-BqGnhkh0.js → index-BYNN9N1T.js} +2 -2
  262. package/dashboard/dist/assets/{index-BqGnhkh0.js.map → index-BYNN9N1T.js.map} +1 -1
  263. package/dashboard/dist/assets/{index-kiKZresb.js → index-CLPVqbVY.js} +2 -2
  264. package/dashboard/dist/assets/{index-kiKZresb.js.map → index-CLPVqbVY.js.map} +1 -1
  265. package/dashboard/dist/assets/{index-C3o4OisO.js → index-Cfv86_L9.js} +2 -2
  266. package/dashboard/dist/assets/{index-C3o4OisO.js.map → index-Cfv86_L9.js.map} +1 -1
  267. package/dashboard/dist/assets/{index-yGNt29V9.js → index-CtwbQ_-m.js} +2 -2
  268. package/dashboard/dist/assets/{index-yGNt29V9.js.map → index-CtwbQ_-m.js.map} +1 -1
  269. package/dashboard/dist/assets/index-DOai950R.js +3 -0
  270. package/dashboard/dist/assets/index-DOai950R.js.map +1 -0
  271. package/dashboard/dist/assets/index-DTtHNC-D.js +5 -0
  272. package/dashboard/dist/assets/{index-CvNBdkB1.js.map → index-DTtHNC-D.js.map} +1 -1
  273. package/dashboard/dist/assets/{index-CJu2PBNx.js → index-DsWnVyVN.js} +2 -2
  274. package/dashboard/dist/assets/{index-CJu2PBNx.js.map → index-DsWnVyVN.js.map} +1 -1
  275. package/dashboard/dist/assets/{index-D0zFReGL.js → index-Dy-MMYti.js} +2 -2
  276. package/dashboard/dist/assets/{index-D0zFReGL.js.map → index-Dy-MMYti.js.map} +1 -1
  277. package/dashboard/dist/assets/index-errgQT5x.js +65 -0
  278. package/dashboard/dist/assets/index-errgQT5x.js.map +1 -0
  279. package/dashboard/dist/assets/{index-CmnNqpWg.js → index-fgBDhnI6.js} +2 -2
  280. package/dashboard/dist/assets/{index-CmnNqpWg.js.map → index-fgBDhnI6.js.map} +1 -1
  281. package/dashboard/dist/assets/{knowledge-DWLxGTib.js → knowledge-arh4NfmL.js} +2 -2
  282. package/dashboard/dist/assets/{knowledge-DWLxGTib.js.map → knowledge-arh4NfmL.js.map} +1 -1
  283. package/dashboard/dist/assets/{mcp-B6AARZ2I.js → mcp-QGf5jHCo.js} +2 -2
  284. package/dashboard/dist/assets/{mcp-B6AARZ2I.js.map → mcp-QGf5jHCo.js.map} +1 -1
  285. package/dashboard/dist/assets/{mcp-query-DrkvBU3a.js → mcp-query-BEJN1nMZ.js} +2 -2
  286. package/dashboard/dist/assets/{mcp-query-DrkvBU3a.js.map → mcp-query-BEJN1nMZ.js.map} +1 -1
  287. package/dashboard/dist/assets/{personas-Cmz2SAEU.js → personas-DYtw1COf.js} +2 -2
  288. package/dashboard/dist/assets/{personas-Cmz2SAEU.js.map → personas-DYtw1COf.js.map} +1 -1
  289. package/dashboard/dist/assets/{pipelines-CSgfVCMy.js → pipelines-Byi6PnbE.js} +2 -2
  290. package/dashboard/dist/assets/{pipelines-CSgfVCMy.js.map → pipelines-Byi6PnbE.js.map} +1 -1
  291. package/dashboard/dist/assets/{tasks-UOtUkDk-.js → tasks-D0SQdENz.js} +2 -2
  292. package/dashboard/dist/assets/{tasks-UOtUkDk-.js.map → tasks-D0SQdENz.js.map} +1 -1
  293. package/dashboard/dist/assets/{topics-Df5hGh6o.js → topics-DgDv5ZXP.js} +2 -2
  294. package/dashboard/dist/assets/{topics-Df5hGh6o.js.map → topics-DgDv5ZXP.js.map} +1 -1
  295. package/dashboard/dist/assets/useEventHooks-DZaYzE7s.js +2 -0
  296. package/dashboard/dist/assets/{useEventHooks-PDbuSJ12.js.map → useEventHooks-DZaYzE7s.js.map} +1 -1
  297. package/dashboard/dist/assets/{useNamespace-tZSOlge3.js → useNamespace-C1OqiNXN.js} +2 -2
  298. package/dashboard/dist/assets/{useNamespace-tZSOlge3.js.map → useNamespace-C1OqiNXN.js.map} +1 -1
  299. package/dashboard/dist/assets/{useYamlActivityEvents-CINLzE32.js → useYamlActivityEvents-CkmXDm-Z.js} +2 -2
  300. package/dashboard/dist/assets/{useYamlActivityEvents-CINLzE32.js.map → useYamlActivityEvents-CkmXDm-Z.js.map} +1 -1
  301. package/dashboard/dist/assets/{users-G8qwyZKS.js → users-D4DCFPH1.js} +2 -2
  302. package/dashboard/dist/assets/{users-G8qwyZKS.js.map → users-D4DCFPH1.js.map} +1 -1
  303. package/dashboard/dist/assets/{vendor-icons-BpZiWH2f.js → vendor-icons-BxuyFxH2.js} +156 -136
  304. package/dashboard/dist/assets/vendor-icons-BxuyFxH2.js.map +1 -0
  305. package/dashboard/dist/assets/{workflows-DbPqi1TW.js → workflows-qyc0ku_5.js} +2 -2
  306. package/dashboard/dist/assets/{workflows-DbPqi1TW.js.map → workflows-qyc0ku_5.js.map} +1 -1
  307. package/dashboard/dist/assets/{yaml-workflows-DozD8hpR.js → yaml-workflows-DWFww9Xn.js} +2 -2
  308. package/dashboard/dist/assets/{yaml-workflows-DozD8hpR.js.map → yaml-workflows-DWFww9Xn.js.map} +1 -1
  309. package/dashboard/dist/index.html +3 -3
  310. package/docs/api/http/escalations.md +133 -0
  311. package/docs/api/http/roles.md +6 -0
  312. package/docs/api/mcp/admin.md +53 -0
  313. package/docs/api/sdk/escalations.md +58 -0
  314. package/docs/cli.md +11 -0
  315. package/docs/dashboard.md +22 -20
  316. package/docs/data.md +12 -4
  317. package/docs/design-principles.md +9 -7
  318. package/docs/escalation-analytics.md +251 -0
  319. package/docs/hitl/roles.md +1 -1
  320. package/docs/hitl/x-lt-embed.md +8 -4
  321. package/docs/hitl/x-lt-footer.md +67 -0
  322. package/docs/hitl/x-lt-list-schema.md +10 -3
  323. package/docs/hitl-guide.md +11 -2
  324. package/docs/iam.md +12 -0
  325. package/docs/operations.md +17 -0
  326. package/docs/scan-codes.md +112 -0
  327. package/docs/sdk.md +1 -1
  328. package/package.json +2 -1
  329. package/dashboard/dist/assets/AgentsPage-CToVu1qj.js +0 -2
  330. package/dashboard/dist/assets/AvailableEscalationsPage-DldboL4W.js +0 -2
  331. package/dashboard/dist/assets/AvailableEscalationsPage-DldboL4W.js.map +0 -1
  332. package/dashboard/dist/assets/CountdownTimer-DPjIkerB.js +0 -2
  333. package/dashboard/dist/assets/CountdownTimer-DPjIkerB.js.map +0 -1
  334. package/dashboard/dist/assets/EventTable-CwPWl163.js +0 -2
  335. package/dashboard/dist/assets/EventTable-CwPWl163.js.map +0 -1
  336. package/dashboard/dist/assets/FilterBar-D7GghUsU.js +0 -2
  337. package/dashboard/dist/assets/FilterBar-D7GghUsU.js.map +0 -1
  338. package/dashboard/dist/assets/HomePage-Bcq10Bxx.js +0 -2
  339. package/dashboard/dist/assets/ListToolbar-IPXsTb3Q.js +0 -2
  340. package/dashboard/dist/assets/McpQueryPage-CNsuiw_q.js +0 -2
  341. package/dashboard/dist/assets/McpRunDetailPage-BJguEMS0.js +0 -2
  342. package/dashboard/dist/assets/McpRunDetailPage-BJguEMS0.js.map +0 -1
  343. package/dashboard/dist/assets/OperationsPage-DvWrLuH6.js +0 -2
  344. package/dashboard/dist/assets/OperationsPage-DvWrLuH6.js.map +0 -1
  345. package/dashboard/dist/assets/OperatorDashboard-C-jE0IRA.js +0 -2
  346. package/dashboard/dist/assets/RoleDetailPage-BKkUvTO-.js +0 -8
  347. package/dashboard/dist/assets/RoleDetailPage-BKkUvTO-.js.map +0 -1
  348. package/dashboard/dist/assets/RolesPage-BHzWK9n6.js +0 -2
  349. package/dashboard/dist/assets/RolesPage-BHzWK9n6.js.map +0 -1
  350. package/dashboard/dist/assets/RunAsSelector-CS3KQ-BF.js +0 -2
  351. package/dashboard/dist/assets/ScanCodesPage-BW4P4zWf.js +0 -2
  352. package/dashboard/dist/assets/ScanCodesPage-BW4P4zWf.js.map +0 -1
  353. package/dashboard/dist/assets/ScanSchemeDetailPage-BLykA4j-.js +0 -2
  354. package/dashboard/dist/assets/ScanSchemeDetailPage-BLykA4j-.js.map +0 -1
  355. package/dashboard/dist/assets/StickyPagination-hMrnLXde.js +0 -2
  356. package/dashboard/dist/assets/StickyPagination-hMrnLXde.js.map +0 -1
  357. package/dashboard/dist/assets/TopicsPage-1VAc29Qw.js +0 -2
  358. package/dashboard/dist/assets/WorkflowExecutionPage-C15GSAoe.js +0 -2
  359. package/dashboard/dist/assets/WorkflowExecutionPage-C15GSAoe.js.map +0 -1
  360. package/dashboard/dist/assets/WorkflowsDashboard-BMQd_0tW.js +0 -2
  361. package/dashboard/dist/assets/capabilities-IaCcS7ge.js +0 -2
  362. package/dashboard/dist/assets/escalation-columns-BY2mg9DY.js +0 -2
  363. package/dashboard/dist/assets/escalation-columns-BY2mg9DY.js.map +0 -1
  364. package/dashboard/dist/assets/index-CLmZaNTK.js +0 -2
  365. package/dashboard/dist/assets/index-CfAi1OsI.js +0 -65
  366. package/dashboard/dist/assets/index-CfAi1OsI.js.map +0 -1
  367. package/dashboard/dist/assets/index-CvNBdkB1.js +0 -5
  368. package/dashboard/dist/assets/index-Dldi3Q3M.js +0 -2
  369. package/dashboard/dist/assets/index-mKcQGi2B.js +0 -3
  370. package/dashboard/dist/assets/index-mKcQGi2B.js.map +0 -1
  371. package/dashboard/dist/assets/index-zQiUPfxA.css +0 -1
  372. package/dashboard/dist/assets/useEventHooks-PDbuSJ12.js +0 -2
  373. package/dashboard/dist/assets/vendor-icons-BpZiWH2f.js.map +0 -1
@@ -1154,3 +1154,136 @@ Batch-claim individual rows matching a facet query (`FOR UPDATE SKIP LOCKED`), a
1154
1154
  | `allOrNone` | `boolean` | no | Commit only if the full set was acquired |
1155
1155
 
1156
1156
  **Response 200:** `{ "claimed": [...] }`.
1157
+
1158
+ ## Aggregate by facets
1159
+
1160
+ ```
1161
+ POST /api/escalations/aggregate-by-facets
1162
+ ```
1163
+
1164
+ Grouped analytics over the escalation intervals. Every escalation is one open interval `[created_at, ended_at)` — `ended_at` is the instant the row left the live set (resolved / cancelled / expired), `NULL` while live. The aggregate reads that time-series two ways: **membership** (rows — or, with `distinctBy`, distinct entities — whose interval is open at an instant; a past `asOf` reconstructs the live set at that moment) and **dwell** (open-seconds per group within a half-open `[from, to)` window, clipped to it on both ends). One call replaces N per-filter count round-trips. See [escalation-analytics.md](../../escalation-analytics.md) for the full guide.
1165
+
1166
+ **Request body:**
1167
+
1168
+ | Field | Type | Required | Description |
1169
+ |-------|------|----------|-------------|
1170
+ | `query` | `object` | yes | The filter — `role`/`roles` (or `entity`), `facets`, `anyOf` (rows carrying ANY of the given facet sets, max 200 — target an explicit entity set such as one table page), `block`, `range`, `exists`, and `prefix` (case-insensitive prefix match on facet values — the locate affordance, e.g. `{"serialNumber":"PRN-00"}`). `entity` names an entity facet key and resolves server-side to every role declaring it as its `entity_facet` — the entity's **system**; mutually exclusive with `role`/`roles`, and it implies `exists: [entity]` so stray rows without the key stay out |
1171
+ | `groupBy` | `object` | yes | Group keys — `columns` (whitelisted: `role`, `subtype`, `status`), `facets` (metadata keys, projected as text; `NULL` group key when absent), and `state` (group by the derived state label: each role contributes per its `entity_state_source` — its subtypes or itself; mutually exclusive with `states[]`). An empty object yields one total row |
1172
+ | `measure` | `object` | yes | Exactly one kind: `{ "kind": "membership", "asOf"? }` (default anchor: now) or `{ "kind": "dwell", "window": { "from", "to" } }` |
1173
+ | `distinctBy` | `string` | no | Membership only — count DISTINCT of this metadata facet per group (entities, not rows). Omit to count rows |
1174
+ | `states` | `array` | no | Pure labeling: tag each group with the FIRST matching `{ name, match }` entry, evaluated top-to-bottom; `match` compares grouped columns/facets. Grouping is unchanged |
1175
+ | `liveStatuses` | `string[]` | no | Statuses considered live (default `["pending"]`) |
1176
+ | `orderBy` | `array` | no | Order the RESULT groups: `{ field, direction? }` — `count`, `dwellSeconds`, `sampleCount`, a grouped column, or a grouped facet key |
1177
+ | `limit` / `offset` | `integer` | no | Result-group paging, capped at `LT_ANALYTICS_MAX_GROUPS` |
1178
+
1179
+ The filter takes the WHAT fields only. `status`, `available`, and `jeopardy` are rejected — liveness derives from the interval, `liveStatuses`, and the measure anchor. Query-level `orderBy`/`limit`/`offset` are rejected too — order and paginate the result groups with the top-level fields.
1180
+
1181
+ **Example** — how the printer fleet spent a day, one row per state. The three seeded roles declare `entity_facet: "serialNumber"`: `printer-fleet` uses `entity_state_source: "subtype"` (its `idle` / `printing` subtypes are states), while `printer-harvest` and `printer-service` use `"role"` (each role is a state):
1182
+
1183
+ ```json
1184
+ {
1185
+ "query": { "entity": "serialNumber" },
1186
+ "groupBy": { "state": true },
1187
+ "measure": {
1188
+ "kind": "dwell",
1189
+ "window": { "from": "2026-08-01T00:00:00Z", "to": "2026-08-02T00:00:00Z" }
1190
+ }
1191
+ }
1192
+ ```
1193
+
1194
+ **Response 200:**
1195
+
1196
+ ```json
1197
+ {
1198
+ "groups": [
1199
+ { "facets": {}, "state": "printing", "dwellSeconds": 214380, "sampleCount": 41 },
1200
+ { "facets": {}, "state": "idle", "dwellSeconds": 132600, "sampleCount": 38 },
1201
+ { "facets": {}, "state": "printer-harvest", "dwellSeconds": 21540, "sampleCount": 17 },
1202
+ { "facets": {}, "state": "printer-service", "dwellSeconds": 9060, "sampleCount": 3 }
1203
+ ],
1204
+ "overflow": false
1205
+ }
1206
+ ```
1207
+
1208
+ Adding `"facets": ["model"]` to `groupBy` slices the same bands per model value (`p1s` vs `h2s`).
1209
+
1210
+ **Response fields (per group):**
1211
+
1212
+ | Field | Type | Description |
1213
+ |-------|------|-------------|
1214
+ | `role` / `subtype` / `status` | `string` | Present iff requested in `groupBy.columns`. `status` is the row's status NOW — status history is not stored, so a past `asOf` still groups by current status |
1215
+ | `facets` | `object` | One entry per `groupBy.facets` key; `null` when the facet is absent on the underlying rows |
1216
+ | `state` | `string` | Present when `groupBy.state` derived it or a `states[]` entry matched |
1217
+ | `count` | `number` | Membership: entities (with `distinctBy`) or rows (without) |
1218
+ | `dwellSeconds` | `number` | Dwell: summed open-seconds within the window |
1219
+ | `sampleCount` | `number` | Underlying escalation rows contributing to the group (pre-distinct) |
1220
+
1221
+ `overflow: true` means the group cap was hit — more groups exist beyond this page.
1222
+
1223
+ **Response 400** — a caller-input problem: a liveness field (`status`/`available`/`jeopardy`) or `orderBy`/`limit`/`offset` on the filter; `entity` together with `role`/`roles`; `groupBy.state` together with `states[]`; an unknown status in `liveStatuses`; an empty window or one wider than `LT_ANALYTICS_MAX_WINDOW_DAYS`; a future `asOf`; a malformed facet key; a `states[]` match referencing a key the query never groups by.
1224
+
1225
+ **Response 403** — the caller must hold `read_all` on every role in scope (for `entity`, every role in the derived system); a filter with no role scope spans every pond and requires a global principal (superadmin/admin). While `features.publicPaceBoard` stands (default on), counts-only groupings — no `groupBy.facets` keys — are readable by any login, the same data class the Pace Board exposes; a facet-keyed grouping emits facet values (entity ids) as group keys and always takes the full gate.
1226
+
1227
+ ## Timeline by facet
1228
+
1229
+ ```
1230
+ POST /api/escalations/timeline-by-facet
1231
+ ```
1232
+
1233
+ One entity's ordered interval sequence — every escalation the entity facet appeared in, as `[startedAt, endedAt)` spans with durations, in `created_at` order. Open intervals report `endedAt: null`. Gaps between consecutive intervals are untracked time and are preserved, not filled. The facet is matched GIN-served (`metadata @> {key: value}`), so the stored value must be a JSON string.
1234
+
1235
+ **Request body:**
1236
+
1237
+ | Field | Type | Required | Description |
1238
+ |-------|------|----------|-------------|
1239
+ | `facet` | `{ key, value }` | yes | The entity facet to trace |
1240
+ | `query` | `object` | no | Optional extra filter / role scope (or `entity` — the derived system). Same field rules as the aggregate filter |
1241
+ | `window` | `{ from, to }` | no | Only intervals overlapping the window (overlap-filtered, not clipped) |
1242
+ | `select` | `object` | no | `columns` / `facets` to surface per interval (default: all three columns) |
1243
+ | `liveStatuses` | `string[]` | no | Statuses considered live (default `["pending"]`) |
1244
+ | `order` | `string` | no | Interval ordering by start instant — `asc` (default) or `desc`; `desc` + `before` pages a long history recent-first |
1245
+ | `before` | `string` | no | Strict upper bound on `startedAt` (ISO instant) — the "load earlier" cursor |
1246
+ | `limit` | `integer` | no | Max intervals |
1247
+
1248
+ **Example** — one printer's movement across the fleet's queues:
1249
+
1250
+ ```json
1251
+ {
1252
+ "facet": { "key": "serialNumber", "value": "PRN-001" },
1253
+ "query": { "entity": "serialNumber" }
1254
+ }
1255
+ ```
1256
+
1257
+ **Response 200:**
1258
+
1259
+ ```json
1260
+ {
1261
+ "intervals": [
1262
+ {
1263
+ "role": "printer-fleet",
1264
+ "subtype": "printing",
1265
+ "status": "resolved",
1266
+ "facets": {},
1267
+ "startedAt": "2026-08-01T08:00:00.000Z",
1268
+ "endedAt": "2026-08-01T09:30:00.000Z",
1269
+ "durationSeconds": 5400
1270
+ },
1271
+ {
1272
+ "role": "printer-harvest",
1273
+ "subtype": "harvest",
1274
+ "status": "pending",
1275
+ "facets": {},
1276
+ "startedAt": "2026-08-01T09:30:00.000Z",
1277
+ "endedAt": null,
1278
+ "durationSeconds": 1800
1279
+ }
1280
+ ],
1281
+ "overflow": false
1282
+ }
1283
+ ```
1284
+
1285
+ `durationSeconds` runs to `endedAt`, else to `window.to` clamped at now. `overflow: true` means the interval cap was hit.
1286
+
1287
+ **Response 400** — same validation class as the aggregate: a rejected filter field, an invalid window, a malformed facet key.
1288
+
1289
+ **Response 403** — timelines always take the full gate: an entity's movement history is item-level disclosure. The caller must hold `read_all` on every role in scope (for `entity`, every role in the derived system); a roleless query requires a global principal.
@@ -252,6 +252,8 @@ Returns all roles with metadata and usage counts.
252
252
  "worker_count": 4,
253
253
  "priority_threshold_minutes": null,
254
254
  "priority_facet": null,
255
+ "entity_facet": null,
256
+ "entity_state_source": "role",
255
257
  "user_count": 5,
256
258
  "chain_count": 2,
257
259
  "workflow_count": 3
@@ -275,6 +277,8 @@ Returns all roles with metadata and usage counts.
275
277
  | `worker_count` | `number \| null` | Capacity at this station — staff or machine count (capacity setting) |
276
278
  | `priority_threshold_minutes` | `number \| null` | Max age (minutes) before a pending unclaimed item counts toward `priority_count` in station metrics; falls back to `sla_minutes` |
277
279
  | `priority_facet` | `string \| null` | Escalation metadata key holding the age origin as an ISO 8601 UTC timestamp (e.g. `authorized_at`); falls back to `created_at` |
280
+ | `entity_facet` | `string \| null` | Escalation metadata key naming the **entity** that moves through this role (e.g. `serialNumber`, `orderId`). Roles sharing the key form that entity's system; the analytics surfaces (distinct-entity counts, per-entity dwell, entity timelines) derive from it. `null` = the role has no entity notion |
281
+ | `entity_state_source` | `'role' \| 'subtype'` | How the role names its contribution to the entity's state space: `'role'` (the station itself is the state) or `'subtype'` (the role's subtypes are its states). Default `'role'`; only meaningful while `entity_facet` is set |
278
282
  | `user_count` | `number` | Number of users assigned this role |
279
283
  | `chain_count` | `number` | Number of escalation chain entries referencing this role |
280
284
  | `workflow_count` | `number` | Number of workflow configs that reference this role |
@@ -374,6 +378,8 @@ When the update changes `form_schema` or `metadata_schema`, the new pair is snap
374
378
  | `worker_count` | `number \| null` | Station capacity |
375
379
  | `priority_threshold_minutes` | `number \| null` | Priority age threshold in minutes; falls back to `sla_minutes` |
376
380
  | `priority_facet` | `string \| null` | Metadata key for the priority age origin (letters, numbers, underscores); falls back to `created_at` |
381
+ | `entity_facet` | `string \| null` | Metadata key naming the entity that moves through this role (letters, numbers, underscores). Roles sharing the key form the entity's system for the analytics surfaces. `null` clears |
382
+ | `entity_state_source` | `'role' \| 'subtype' \| null` | How the role names the entity's states — the station itself (`'role'`) or its subtypes (`'subtype'`). `null` resets to the `'role'` default |
377
383
  | `upstream_roles` | `string[] \| null` | Replace the set of roles this station draws input from across other Operations sequences (omitted = preserve; `null` or `[]` = clear). Every entry must name an existing role other than this one. Distinct from `parent_role`, which places the role in its own sequence. |
378
384
  | `list_schema` | `object \| null` | Rich formatting (x-lt-* markup) for this role's escalation LIST page. Versions independently of `form_schema`; the list always renders the latest |
379
385
  | `default_pins` | `array \| null` | Pinned-view seeds for members: `[{ label, url, badge? }]` with dashboard-relative `url`s. Members promote, hide, or reorder them via their own preferences (`/api/me/preferences`). `null` clears |
@@ -377,6 +377,59 @@ Update the priority of multiple escalations.
377
377
  | ids | string[] | Yes | Escalation IDs |
378
378
  | priority | integer | Yes | New priority value |
379
379
 
380
+ ### aggregate_by_facets
381
+
382
+ Grouped analytics over the escalation intervals. Every escalation is one open
383
+ interval `[created_at, ended_at)`; this tool reads it two ways: **membership**
384
+ (rows or, with `distinctBy`, distinct entities open at an instant — a past
385
+ `asOf` reconstructs the live set then) and **dwell** (open-seconds per group
386
+ within a half-open `[from, to)` window). Group by role/subtype/status columns
387
+ plus metadata facet keys; optional `states[]` label each group. One call
388
+ replaces N per-filter count round-trips. Mirrors
389
+ `POST /api/escalations/aggregate-by-facets`.
390
+
391
+ | | |
392
+ |---|---|
393
+ | Read-safe | Yes |
394
+
395
+ **Parameters:**
396
+
397
+ | Field | Type | Required | Description |
398
+ |-------|------|----------|-------------|
399
+ | query | object | Yes | The filter (WHAT only — no status/available/jeopardy): `role`/`roles` or `entity` (the entity facet key, resolved to every role declaring it — the entity's system), plus `facets`, `block`, `range`, `exists` |
400
+ | groupBy | object | Yes | Group keys: `columns` (`role`, `subtype`, `status`), `facets` (metadata keys), `state` (derived state label per each role's `entity_state_source`; mutually exclusive with `states`). Empty object → one total row |
401
+ | measure | object | Yes | `{ kind: "membership", asOf? }` or `{ kind: "dwell", window: { from, to } }` |
402
+ | distinctBy | string | No | Membership only: count DISTINCT of this metadata facet (entities, not rows) |
403
+ | states | array | No | Pure labeling: tag each group with the FIRST matching state name |
404
+ | liveStatuses | string[] | No | Statuses considered live (default `["pending"]`) |
405
+ | orderBy | array | No | Order the RESULT groups: `{ field, direction? }` |
406
+ | limit | integer | No | Max result groups (server-capped; `overflow` flag when more exist) |
407
+ | offset | integer | No | Result-group offset |
408
+
409
+ ### timeline_by_facet
410
+
411
+ One entity's ordered interval sequence — every escalation the entity facet
412
+ (e.g. a machine id) appeared in, as `[startedAt, endedAt)` spans with durations,
413
+ in `created_at` order. Open intervals report `endedAt: null`. Gaps between
414
+ consecutive intervals are untracked time and are preserved, not filled. The
415
+ entity facet value must be stored as a JSON string (GIN containment match).
416
+ Mirrors `POST /api/escalations/timeline-by-facet`.
417
+
418
+ | | |
419
+ |---|---|
420
+ | Read-safe | Yes |
421
+
422
+ **Parameters:**
423
+
424
+ | Field | Type | Required | Description |
425
+ |-------|------|----------|-------------|
426
+ | facet | object | Yes | `{ key, value }` — the entity facet to trace |
427
+ | query | object | No | Optional extra filter / role scope (or `entity` — the derived system) |
428
+ | window | object | No | `{ from, to }` — only intervals overlapping this window (overlap-filtered, not clipped) |
429
+ | select | object | No | `columns` / `facets` to surface per interval (default: all three columns) |
430
+ | liveStatuses | string[] | No | Statuses considered live (default `["pending"]`) |
431
+ | limit | integer | No | Max intervals |
432
+
380
433
  ## Workflow Configuration
381
434
 
382
435
  ### list_workflow_configs
@@ -915,6 +915,64 @@ const result = await lt.escalations.claimByFacets({
915
915
 
916
916
  **Returns:** `LTApiResult<{ claimed }>`.
917
917
 
918
+ ## aggregateByFacets
919
+
920
+ Grouped analytics over the escalation intervals — every escalation is one open interval `[created_at, ended_at)`, `ended_at` `NULL` while live. Two measures: **membership** (rows or, with `distinctBy`, distinct entities open at an instant; a past `asOf` reconstructs the live set then) and **dwell** (open-seconds per group within a half-open `[from, to)` window, clipped to it). `query.entity` names an entity facet key and scopes to every role declaring it as its `entity_facet` — the entity's system (mutually exclusive with `role`/`roles`); `groupBy.state` groups by the derived state label, each role contributing per its `entity_state_source`.
921
+
922
+ ```typescript
923
+ // The printer fleet: how the entities spent the window, one row per state
924
+ // (idle, printing, printer-harvest, printer-service)
925
+ const result = await lt.escalations.aggregateByFacets({
926
+ query: { entity: 'serialNumber' },
927
+ groupBy: { state: true },
928
+ measure: { kind: 'dwell', window: { from: '2026-08-01T00:00:00Z', to: '2026-08-02T00:00:00Z' } },
929
+ });
930
+
931
+ // The slice: the same bands per model value (p1s vs h2s)
932
+ await lt.escalations.aggregateByFacets({
933
+ query: { entity: 'serialNumber' },
934
+ groupBy: { state: true, facets: ['model'] },
935
+ measure: { kind: 'dwell', window: { from: '2026-08-01T00:00:00Z', to: '2026-08-02T00:00:00Z' } },
936
+ });
937
+ ```
938
+
939
+ | Field | Type | Required | Description |
940
+ |-------|------|----------|-------------|
941
+ | `query` | `AnalyticsQuery` | Yes | The filter — `role`/`roles` or `entity`, plus `facets`, `anyOf` (rows carrying ANY of the given facet sets, max 200), `block`, `range`, `exists`, and `prefix` (case-insensitive prefix match on facet values — the locate affordance). `status`/`available`/`jeopardy` and query-level paging are rejected with 400: liveness derives from the interval, `liveStatuses`, and the measure anchor |
942
+ | `groupBy` | `FacetGroupBy` | Yes | `columns` (`role`, `subtype`, `status`), `facets` (metadata keys), `state` (derived state label; mutually exclusive with `states[]`). Empty object → one total row |
943
+ | `measure` | `AggregateMeasure` | Yes | `{ kind: 'membership', asOf? }` or `{ kind: 'dwell', window }` |
944
+ | `distinctBy` | `string` | No | Membership only — count DISTINCT of this facet (entities, not rows) |
945
+ | `states` | `Array<{ name, match }>` | No | Pure labeling: tag each group with the first matching state name |
946
+ | `liveStatuses` | `string[]` | No | Statuses considered live (default `['pending']`) |
947
+ | `orderBy` / `limit` / `offset` | — | No | Order and page the RESULT groups (capped at `LT_ANALYTICS_MAX_GROUPS`) |
948
+
949
+ **Returns:** `LTApiResult<{ groups: AggregateRow[]; overflow: boolean }>` — each group carries the grouped keys, `count` or `dwellSeconds`, and `sampleCount`. Gate: `read_all` on every role in scope (entity-derived roles included); a roleless filter requires a global principal; counts-only groupings (no `groupBy.facets`) are open to any login while `features.publicPaceBoard` stands. Mirrors `POST /api/escalations/aggregate-by-facets`.
950
+
951
+ ## timelineByFacet
952
+
953
+ One entity's ordered interval sequence — every escalation the facet appeared in, as `[startedAt, endedAt)` spans with durations, in `created_at` order. Open intervals report `endedAt: null`; gaps are untracked time, preserved. GIN-served containment match — the stored facet value must be a JSON string. Always takes the full read gate (an entity's movement history is item-level disclosure).
954
+
955
+ ```typescript
956
+ // One printer's movement across the fleet's queues
957
+ const result = await lt.escalations.timelineByFacet({
958
+ facet: { key: 'serialNumber', value: 'PRN-001' },
959
+ query: { entity: 'serialNumber' },
960
+ });
961
+ ```
962
+
963
+ | Field | Type | Required | Description |
964
+ |-------|------|----------|-------------|
965
+ | `facet` | `{ key, value }` | Yes | The entity facet to trace |
966
+ | `query` | `AnalyticsQuery` | No | Optional extra filter / role scope (or `entity` — the derived system) |
967
+ | `window` | `{ from, to }` | No | Only intervals overlapping the window (overlap-filtered, not clipped) |
968
+ | `select` | `{ columns?, facets? }` | No | Columns/facets to surface per interval (default: all three columns) |
969
+ | `liveStatuses` | `string[]` | No | Statuses considered live (default `['pending']`) |
970
+ | `order` | `'asc' \| 'desc'` | No | Interval ordering by start instant (default `asc`); `desc` + `before` pages a long history recent-first |
971
+ | `before` | `Date \| string` | No | Strict upper bound on `startedAt` — the "load earlier" cursor |
972
+ | `limit` | `integer` | No | Max intervals |
973
+
974
+ **Returns:** `LTApiResult<{ intervals: TimelineInterval[]; overflow: boolean }>`. Mirrors `POST /api/escalations/timeline-by-facet`.
975
+
918
976
  ---
919
977
 
920
978
  ## getStationMetrics
package/docs/cli.md CHANGED
@@ -78,8 +78,19 @@ ltc esc claim-by-meta orderId order-123 # Claim by metadata key
78
78
  ltc esc claim-by-meta orderId order-123 --assignee ext-user-42 # On behalf of user
79
79
  ltc esc resolve-by-meta orderId order-123 --data '{"approved": true}'
80
80
  ltc esc resolve-by-meta orderId order-123 --data '{"approved": true}' --assignee ext-user-42
81
+
82
+ # Analytics: grouped membership/dwell aggregates and per-entity timelines
83
+ # The fleet: how the entities spent the window, one row per state
84
+ ltc esc aggregate-facets --entity serialNumber --group-state \
85
+ --window '{"from":"2026-08-01T00:00:00Z","to":"2026-08-02T00:00:00Z"}'
86
+ # The individual: one printer's ordered interval timeline
87
+ ltc esc timeline serialNumber PRN-001 --entity serialNumber
81
88
  ```
82
89
 
90
+ `aggregate-facets` runs grouped analytics over the escalation intervals — membership at an instant (`--as-of`, default now), or dwell over a window (`--window` switches the measure). Scope with `--role`, `--roles`, or `--entity <key>` (the entity facet key — scopes to every role declaring it, the entity's system; no role at all spans every pond, global principals only). Group with `--group-columns role,subtype,status`, `--group-facets <list>`, or `--group-state` (the derived state label — each role's subtypes or itself). Refine with `--facets`/`--exists`, `--distinct-by <key>` (count entities, not rows; membership only), `--live-statuses`, and page the result groups with `--order-by`/`--limit`/`--offset`.
91
+
92
+ `timeline <key> <value>` prints one entity's ordered escalation-interval timeline (gaps = untracked time). `--entity` scopes to the entity's system (every role declaring that entity facet); `--roles` restricts to specific roles; a call with neither spans every pond and requires a global principal. `--from`/`--to` keep only overlapping intervals; `--select-facets` surfaces metadata keys per interval.
93
+
83
94
  ### Workflows
84
95
 
85
96
  ```bash
package/docs/dashboard.md CHANGED
@@ -56,7 +56,7 @@ The LLM authoring add-on. Appears when an Anthropic key is configured.
56
56
  | Page | Route | Purpose |
57
57
  |------|-------|---------|
58
58
  | **Accounts** | `/admin/users` | User accounts and service accounts (bots). Create, edit, assign roles, manage API keys. |
59
- | **Roles** | `/admin/roles` | Define roles — the queues, forms, and membership that connect workflows to people. Escalation chains, capacity settings, and versioned schemas live here. |
59
+ | **Roles** | `/admin/roles` | Define roles — the queues, forms, and membership that connect workflows to people. Pace Board dials, capacity settings, versioned schemas, members, and default pins live here. |
60
60
  | **Personas** | `/admin/personas` | Bundle roles into named, one-step assignments. Each linked role carries a relationship scope (write-all, write-self, read-all); assigning a persona composes the member's whole surface from its roles' pins and schemas. |
61
61
 
62
62
  ### Infrastructure
@@ -223,41 +223,43 @@ Because roles carry the queue, the form schema, the membership, and the capacity
223
223
 
224
224
  **Master list** (`/admin/roles`) — one row per role:
225
225
 
226
- - **Ops dot** — a green dot marks roles that appear as stations on the [Pace Board](#pace-board).
227
- - **Role / Label / Description** — the role key and its human-facing identity.
226
+ - **Role** — an ops status dot (green marks roles that appear as stations on the [Pace Board](#pace-board)) beside the display name: the user-set title, else Title Case derived from the key.
227
+ - **Key** — the exact technical role id.
228
+ - **Description** — the role's human-facing summary.
228
229
  - **Preceded By** — the role's prior step (`parent_role`), linked. Prior steps compose the Pace Board's sequences.
229
- - **Escalates To** — the chain targets this role can forward work to.
230
- - **Member Count** — how many accounts hold the role.
230
+ - **Members** — how many accounts hold the role.
231
231
  - **Capacity** — SLA (minutes), Target (per hour), and Staff side by side. These drive the Pace Board's computed metrics.
232
232
 
233
233
  Search filters by key, label, or description. **+ Add Role** creates a role here; roles referenced in workflow configs are also auto-created at startup. Click any row to open [Role Detail](#role-detail).
234
234
 
235
- **API:** `GET /api/roles` lists roles. `POST /api/roles` creates. `GET /api/roles/details` returns full `RoleDetail` shapes. `GET /api/roles/escalation-chains` lists chains.
235
+ **API:** `GET /api/roles` lists roles. `POST /api/roles` creates. `GET /api/roles/details` returns full `RoleDetail` shapes.
236
236
 
237
237
  ### Role Detail
238
238
 
239
- Accessible at `/admin/roles/:role`. One page per role — a header plus three columns covering identity, routing and people, and schemas.
239
+ Accessible at `/admin/roles/:role`. One page per role — a quiet header carrying the role's identity, with the configuration organized into a five-section left sub-nav: **Identity · Pace Board · Schemas · Members · Pins**. The active section rides the URL (`?section=pace-board`), so deep links land on the right concern. One draft spans every section — switching sections never loses edits — and the **Save** button sits in the sub-nav footer, visible from every section. (Members, Pins, and Upstream Inputs save live.)
240
240
 
241
- **Header** — the role key with its prior step, the capacity set, and the **Ops** toggle that shows the role as a station on the [Pace Board](#pace-board). The capacity set holds `sla_minutes`, `target_per_hour`, and `worker_count`; enter any two and the header hints the derived third (`throughput = workers / (sla / 60)`). Beside it, the priority set holds `priority_threshold_minutes` and `priority_facet` — the age threshold and metadata timestamp key driving the Pace Board priority count (blank = `sla_minutes` and `created_at`). Save applies header and identity edits via `PATCH /api/roles/:role`.
241
+ **Identity** — display name and description, shown on role rows and station labels. The danger zone lives here too: a role nothing references can be deleted.
242
242
 
243
- **Identity** — display name and description, shown on role rows and station labels.
243
+ **Pace Board** — everything the board consumes about this role, in one column:
244
244
 
245
- **Sequence placement** — appears when the Ops toggle is on:
245
+ - **Station**the toggle that shows the role as a station on the [Pace Board](#pace-board).
246
+ - **Capacity** — `sla_minutes`, `target_per_hour`, and `worker_count`; enter any two and the section hints the derived third (`throughput = workers / (sla / 60)`).
247
+ - **Priority** — `priority_threshold_minutes` and the age facet (`priority_facet`) driving the Pace Board priority count and the jeopardy filter (blank = `sla_minutes` and `created_at`). The dials stay editable while the station is hidden — they also drive jeopardy in the queues.
248
+ - **Entity** — the metadata key naming what moves through this station (`entity_facet`, e.g. `serialNumber`, `orderId`). Roles sharing a key form that entity's system: the [entity lens](operations.md#entity-lens), per-entity dwell, and timelines all derive from it. Once a key is set, the **States from** picker (`entity_state_source`) chooses how the station names the entity's state — **Station** (being here is one state, e.g. a servicing queue) or **Subtypes** (this one role holds several states named by each escalation's subtype, e.g. a fleet role parking `idle` / `printing`).
249
+ - **Sequence** — board geometry, shown while the station is visible: **Prior Step** (places the role in one Pace Board sequence; a role with no prior step starts its own), **Upstream Inputs** (the roles this station also draws from in other sequences — mixin-like, many allowed, saved live, rendered as a merge glyph on the station, never a bend in the line), and **Home Segment** (lead the home Pace Board with this role's sequence; one role holds this).
246
250
 
247
- - **Prior Step** — places the role in one Pace Board sequence; a role with no prior step starts its own.
248
- - **Upstream Inputs** — the roles this station also draws from in other sequences. Mixin-like, many allowed, saved live. Rendered on the Pace Board as a merge glyph on the station, never a bend in the line: an upstream is an input, not a descendant.
251
+ **Schemas** — the role's contracts:
249
252
 
250
- **Escalation Targets** — the chain out of this role, saved live. When a member escalates an item, these targets receive it. Chains are directional and support multiple targets per source.
253
+ - **Enforcement** — the `enforce_schema` toggle: validate every resolve payload server-side against the role's form schema.
254
+ - **Escalation Schema** — shows the schema version currently in use and links to the [Escalation Schema](#escalation-schema) editor page.
255
+ - **Escalations List Schema** — shows the list-schema version in use and links to the [Escalations List Schema](#escalations-list-schema) editor page.
256
+ - **Metadata Schema** — JSON Schema that validates each escalation's `metadata` at creation time. Its keys appear in faceted search autocomplete.
251
257
 
252
258
  **Members** — who holds the role: admins manage it; members work its queue according to their read/write work-surface scope (read = which items appear; write = which they can claim and resolve). A `member` grant carries a scope chosen from five named profiles: full worker (`all`/`all`, default), see-all-act-own (`all`/`self`), own-items-only (`self`/`self`), read-only auditor (`all`/`none`), and read-only own (`self`/`none`). The picker enforces **write ⊆ read**. `admin` and `superadmin` grants always work the whole queue.
253
259
 
254
- **Escalation Schema** — shows the schema version currently in use and links to the [Escalation Schema](#escalation-schema) editor page.
260
+ **Pins** — the pinned-view seeds this role hands its members (`default_pins`): label, dashboard-relative URL, optional badge. Members see them in their Pinned nav section from first login (marked role-provided) and may promote, hide, or reorder them via preferences. Live-save.
255
261
 
256
- **Metadata Schema** — JSON Schema that validates each escalation's `metadata` at creation time. Its keys appear in faceted search autocomplete.
257
-
258
- **Properties** — a free JSON bag for custom per-role values.
259
-
260
- **API:** `PATCH /api/roles/:role` updates identity, capacity, placement, and schemas. `POST /api/roles/escalation-chains` adds a chain target; `DELETE /api/roles/escalation-chains` removes one.
262
+ **API:** `PATCH /api/roles/:role` updates identity, Pace Board dials, placement, schemas, and pins.
261
263
 
262
264
  ### Escalation Schema
263
265
 
@@ -334,10 +336,10 @@ The central queue for all escalation activity across every workflow.
334
336
  - **Title = queue selector** — the page title reads as the selected role's friendly title, or "All Escalations" when unfiltered; clicking it switches queues over the same `?role=` param the filter bar mirrors. The **My Escalations** personal inbox (`/escalations/queue`) carries the identical control, titling itself "My Escalations" or the filtered role's title.
335
337
  - **Filter bar** — filter by status (pending/claimed/resolved), role, workflow type, priority, and time window.
336
338
  - **Columns:** Escalation ID, workflow type, role, status, priority, created time, and claimed-by user.
339
+ - **Metadata value affordances** — each metadata key/value row in the list carries a three-icon triad on hover: **filter** (filter the current role's queue to rows where the key equals this value), **search** (the same facet match across all roles), and **history** (open the entity's cross-queue interval timeline in the right panel — every station this value has moved through, with durations and gaps). Filter-present, search-present, history-past. The history affordance renders for string values, since the timeline's GIN containment match serves JSON-string facets.
337
340
  - **Rich list view** — when the list is scoped to exactly one role (`?role=<role>`) and that role owns a [list schema](#escalations-list-schema), a role-authored view renders in place of the table (the live item as a card, plus a load-on-demand history), with a **Table view** toggle back to the columns.
338
341
  - **Claim** — click the claim action to lock an escalation to your user. Only users with matching roles see pending escalations. The queue list and aggregate stats reflect `read_all` memberships — a member scoped to `read_self` lands directly on their own assigned item in user mode rather than browsing the full queue.
339
342
  - **Resolve** — after claiming, submit a resolver payload. The form is pre-filled from the role's versioned `form_schema` field defaults and from the workflow's seeded `envelope.formDefaults` (reverse-mapped through each field's `x-lt-bind`). The dashboard maps the flat form to the nested payload via `x-lt-bind` and the submitted payload is stored as-is. Resolution triggers a workflow re-run with the resolver data injected. A `member` whose `write_scope` is `self` can resolve only items already assigned to them; `write_scope=none` is read-only.
340
- - **Escalate** — forward a claimed escalation to a higher-tier role via the escalation chain.
341
343
  - **Side panel** — a slide-in beside the resolve form with switchable views, selected by the icon set at its top: **Help** (the form's `x-lt-help` markdown, `{{domain.path}}`-interpolated against the live record, or a state-aware hint such as "Claim this escalation to enable the form"), **Details** (status, role, priority, claim provenance, timestamps, and — for builders — identifier links), **AI Analysis** (what triage diagnosed and corrected — shown when AI is enabled and triage data is present), **Metadata** (the row's metadata values), **Context** (input envelope, escalation context, resolver payload), and **Record** (the raw escalation JSON, builders only). The panel and form share the width as a flex set — the form column narrows as the panel expands. It opens expanded on Help when the form carries `x-lt-help`, stays hidden otherwise, and the page-header panel button toggles it either way.
342
344
 
343
345
  **API:** `GET /api/escalations` lists with filters. `POST /api/escalations/:id/claim` claims. `POST /api/escalations/:id/resolve` resolves.
package/docs/data.md CHANGED
@@ -106,9 +106,9 @@ Updated when claimed or resolved.
106
106
 
107
107
  The storage is the **shared HotMesh table `public.hmsh_escalations`**
108
108
  (written by both long-tail and the HotMesh SDK). `lt_escalations` remains as a
109
- backward-compatible **view** over it — `SELECT *` plus a computed `available`
110
- column — so existing read queries and the public API are unchanged. Indexes are
111
- managed by the SDK on `hmsh_escalations` (see below).
109
+ backward-compatible **view** over it — `SELECT *` plus computed `available` and
110
+ `ended_at` columns — so existing read queries and the public API are unchanged.
111
+ Indexes are managed by the SDK on `hmsh_escalations` (see below).
112
112
 
113
113
  Role read/write scope does **not** add columns here. An escalation carries a `role`
114
114
  and an optional `assigned_to`; work-surface scope lives on the membership table
@@ -159,11 +159,12 @@ contract is stable.
159
159
  | `created_at` | `TIMESTAMPTZ NOT NULL` | `NOW()` | Row creation time |
160
160
  | `updated_at` | `TIMESTAMPTZ NOT NULL` | `NOW()` | Last modification |
161
161
 
162
- The `lt_escalations` view adds one computed column on top of the above:
162
+ The `lt_escalations` view adds two computed columns on top of the above:
163
163
 
164
164
  | Column | Type | Description |
165
165
  |--------|------|-------------|
166
166
  | `available` | `BOOLEAN` | `true` when claimable — `assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()` |
167
+ | `ended_at` | `TIMESTAMPTZ` | The instant the row left the live set — `NULL` while `pending`, `COALESCE(resolved_at, updated_at)` once terminal. Resolve stamps `resolved_at` in its guarded transition; cancel and expire stamp only `updated_at` in theirs, and no long-tail write path touches a terminal row afterward, so the coalesce is the transition instant for all three terminal statuses. Every escalation is thereby one open interval `[created_at, ended_at)` — the time-series the analytics surfaces read |
167
168
 
168
169
  **Claiming is implicit.** There is no separate status for "claimed". An escalation is considered claimed when `assigned_to IS NOT NULL` and `assigned_until > NOW()`. When the claim expires, the escalation is available again without any status change. The `/available` endpoint uses this logic:
169
170
 
@@ -187,6 +188,13 @@ WHERE status = 'pending'
187
188
  | `idx_hmsh_esc_task` | `(task_id) WHERE task_id IS NOT NULL` | Join escalations to their parent task |
188
189
  | `idx_hmsh_esc_workflow` | `(workflow_id) WHERE workflow_id IS NOT NULL` | Look up escalation by workflow ID |
189
190
 
191
+ **Indexes** (app-managed, built at startup with `CREATE INDEX CONCURRENTLY`):
192
+
193
+ | Index | Columns | Purpose |
194
+ |-------|---------|---------|
195
+ | `idx_hmsh_esc_resolved_cover` | `(role, resolved_at DESC, claimed_at, created_at) WHERE status = 'resolved'` | Station-metrics percentile pass — bounds each station's scan to its resolved window and feeds the wait/work percentiles from the index |
196
+ | `idx_hmsh_esc_ended_at` | `((COALESCE(resolved_at, updated_at))) WHERE status <> 'pending'` | The analytics interval scans — dwell and past-membership need terminal rows whose end instant falls after the window/`asOf`. The predicate excludes `pending` inserts, so each row costs exactly one index write, at its terminal transition |
197
+
190
198
  ### lt_users
191
199
 
192
200
  User and bot identity records. Users are created via the API and assigned roles that determine which escalations they can claim. Bot accounts (`account_type = 'bot'`) are service identities that authenticate with API keys.
@@ -24,9 +24,11 @@ swatches, and overlay scrims.
24
24
 
25
25
  Chroma is spent, not spread.
26
26
 
27
- - **Surfaces are near-neutral.** Hover fills, section bands, and borders
28
- carry only a whisper of the theme hue. Chroma never lands on a large
29
- rectangle — a colored slab makes every real accent invisible.
27
+ - **Surfaces are near-neutral; lines carry the hue.** Hover fills and section
28
+ bands stay a whisper off-neutral, but the thin structure borders,
29
+ dividers, outlines, field rules carries a clear, light tint of the accent
30
+ so the frame reads as thematic. Chroma never lands on a large rectangle — a
31
+ colored slab makes every real accent invisible — but a hairline may glow.
30
32
  - **One elevation logic, both polarities.** The page carries the base wash
31
33
  (a light theme tint by day, the deep shade at night); sections and sheets
32
34
  are distinct surfaces on it (white paper by day, the deeper well at
@@ -64,10 +66,10 @@ Chroma is spent, not spread.
64
66
 
65
67
  ## 4. Fields and controls
66
68
 
67
- - **One recipe** (`.field/.input/.select/.textarea`): white body, light
68
- neutral border, 3px radius (`--lt-radius-field`), border deepens to accent
69
- and fill takes the theme tint on focus. Every field-like control uses the
70
- recipe or the tokens — no bespoke field styling.
69
+ - **One recipe** (`.field/.input/.select/.textarea`): white body, a light
70
+ accent-tinted border, soft radius (`--lt-radius-field`), border deepens to
71
+ accent and fill takes the theme tint on focus. Every field-like control uses
72
+ the recipe or the tokens — no bespoke field styling.
71
73
  - **Width follows content, capped by the measure:**
72
74
  - Generated forms hold a readable measure (`max-w-form`, 56rem). A wide
73
75
  monitor gets margin, never a 2000px input.