@kedem/okdb 1.0.1 → 1.0.4

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 (142) hide show
  1. package/bin/okdb.js +1 -1
  2. package/keys/public.pem +3 -0
  3. package/okdb.js +1 -1
  4. package/package.json +2 -2
  5. package/public/admin-app.js +1 -1
  6. package/public/components/admin-route-host.ok.js +1 -1
  7. package/public/components/env-picker.ok.js +1 -1
  8. package/public/components/field-path-input.ok.js +1 -1
  9. package/public/components/field.html +154 -2
  10. package/public/components/geo-map.ok.js +1 -1
  11. package/public/components/ok-cron-input.ok.js +1 -1
  12. package/public/components/ok-dropzone.ok.js +1 -1
  13. package/public/components/ok-js-input.ok.js +1 -1
  14. package/public/components/ok-json-input.ok.js +1 -1
  15. package/public/components/ok-text-monaco-input.ok.js +1 -1
  16. package/public/components/theme-toggle.ok.js +1 -1
  17. package/public/index.html +29 -2
  18. package/public/layouts/_default.ok.html +1 -1
  19. package/public/layouts/okdb-cube.ok.html +1 -1
  20. package/public/login-app.js +1 -1
  21. package/public/login.html +205 -1
  22. package/public/modules/ok.js +1 -1
  23. package/public/sections/api/index.ok.html +1 -1
  24. package/public/sections/api/parts/api-endpoint.ok.js +1 -1
  25. package/public/sections/auth/index.ok.html +1 -1
  26. package/public/sections/auth/modals/create-token-modal.ok.js +1 -1
  27. package/public/sections/auth/modals/edit-token-modal.ok.js +1 -1
  28. package/public/sections/auth/modals/perm-info-popover.ok.js +1 -1
  29. package/public/sections/auth/parts/token-detail.ok.js +1 -1
  30. package/public/sections/auth/parts/token-list.ok.js +1 -1
  31. package/public/sections/db/index.ok.html +1 -1
  32. package/public/sections/db/modals/add-item-modal.ok.js +1 -1
  33. package/public/sections/db/modals/create-env-modal.ok.js +1 -1
  34. package/public/sections/db/modals/create-fts-modal.ok.js +1 -1
  35. package/public/sections/db/modals/create-index-modal.ok.js +1 -1
  36. package/public/sections/db/modals/create-pipeline-modal.ok.js +1 -1
  37. package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
  38. package/public/sections/db/modals/remove-env-modal.ok.js +1 -1
  39. package/public/sections/db/modals/schema-edit-modal.ok.js +1 -1
  40. package/public/sections/db/parts/db-overview.ok.js +1 -1
  41. package/public/sections/db/parts/db-pipelines-panel.ok.js +1 -1
  42. package/public/sections/db/parts/edit-row.ok.js +1 -1
  43. package/public/sections/db/parts/tm-timeline.ok.js +1 -1
  44. package/public/sections/db/parts/type-changelog.ok.js +1 -1
  45. package/public/sections/db/parts/type-list.ok.js +1 -1
  46. package/public/sections/db/parts/type-pipelines-tab.ok.js +1 -1
  47. package/public/sections/db/parts/type-schema-tab.ok.js +1 -1
  48. package/public/sections/db/parts/type-viewer.ok.js +1 -1
  49. package/public/sections/db/parts/type-violations-tab.ok.js +1 -1
  50. package/public/sections/docs/index.ok.html +1 -1
  51. package/public/sections/embeddings/index.ok.html +1 -1
  52. package/public/sections/embeddings/modals/register-model-modal.ok.js +1 -1
  53. package/public/sections/embeddings/parts/embed-create-panel.ok.js +1 -1
  54. package/public/sections/embeddings/parts/embed-embedder-panel.ok.js +1 -1
  55. package/public/sections/embeddings/parts/embed-engine-declaration-editor.ok.js +1 -1
  56. package/public/sections/embeddings/parts/embed-engine-list.ok.js +1 -1
  57. package/public/sections/embeddings/parts/embed-indexer-panel.ok.js +1 -1
  58. package/public/sections/embeddings/parts/embed-lifecycle-bar.ok.js +1 -1
  59. package/public/sections/embeddings/parts/embed-model-catalog.ok.js +1 -1
  60. package/public/sections/embeddings/parts/embed-overview.ok.js +1 -1
  61. package/public/sections/embeddings/parts/embed-search-panel.ok.js +1 -1
  62. package/public/sections/embeddings/parts/embed-worker-panel.ok.js +1 -1
  63. package/public/sections/embeddings/parts/embeddings-engine-panel.ok.js +1 -1
  64. package/public/sections/embeddings/parts/engines-overview.ok.js +1 -1
  65. package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -1
  66. package/public/sections/engines/engine-panel-shared-style.js +1 -1
  67. package/public/sections/engines/engine-ui-utils.js +1 -1
  68. package/public/sections/engines/index.ok.html +1 -1
  69. package/public/sections/engines/modals/engine-modal.ok.js +1 -1
  70. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
  71. package/public/sections/engines/parts/engine-lifecycle-actions.ok.js +1 -1
  72. package/public/sections/engines/parts/engine-panel-shell.ok.js +1 -1
  73. package/public/sections/engines/parts/engine-resource-card.ok.js +1 -1
  74. package/public/sections/engines/parts/engine-row-item.ok.js +1 -1
  75. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
  76. package/public/sections/files/index.ok.html +1 -1
  77. package/public/sections/files/parts/files-blob-status.ok.js +1 -1
  78. package/public/sections/files/parts/files-collection-list.ok.js +1 -1
  79. package/public/sections/files/parts/files-collection-panel.ok.js +1 -1
  80. package/public/sections/files/parts/files-file-list.ok.js +1 -1
  81. package/public/sections/files/parts/files-overview.ok.js +1 -1
  82. package/public/sections/files/parts/files-path-browser.ok.js +1 -1
  83. package/public/sections/files/parts/files-upload-panel.ok.js +1 -1
  84. package/public/sections/functions/index.ok.html +1 -1
  85. package/public/sections/functions/modals/function-modal.ok.js +1 -1
  86. package/public/sections/functions/parts/function-detail.ok.js +1 -1
  87. package/public/sections/functions/parts/function-logs.ok.js +1 -1
  88. package/public/sections/functions/parts/function-runner.ok.js +1 -1
  89. package/public/sections/functions/parts/functions-list.ok.js +1 -1
  90. package/public/sections/license/index.ok.html +1 -1
  91. package/public/sections/license/parts/license-panel.ok.js +1 -1
  92. package/public/sections/processors/index.ok.html +1 -1
  93. package/public/sections/queue/index.ok.html +1 -1
  94. package/public/sections/queue/modals/create-bucket-modal.ok.js +1 -1
  95. package/public/sections/queue/modals/edit-bucket-modal.ok.js +1 -1
  96. package/public/sections/queue/modals/edit-job-modal.ok.js +1 -1
  97. package/public/sections/queue/modals/enqueue-job-modal.ok.js +1 -1
  98. package/public/sections/queue/modals/job-type-modal.ok.js +1 -1
  99. package/public/sections/queue/parts/ok-job-form.ok.js +1 -1
  100. package/public/sections/queue/parts/queue-buckets.ok.js +1 -1
  101. package/public/sections/queue/parts/queue-job-types.ok.js +1 -1
  102. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  103. package/public/sections/queue/parts/queue-types.ok.js +1 -1
  104. package/public/sections/queue/parts/queue-workers.ok.js +1 -1
  105. package/public/sections/sync/index.ok.html +1 -1
  106. package/public/sections/sync/parts/sync-node-detail.ok.js +1 -1
  107. package/public/sections/sync/parts/sync-retention.ok.js +1 -1
  108. package/public/sections/sync/parts/sync-topology.ok.js +1 -1
  109. package/public/sections/system/index.ok.html +1 -1
  110. package/public/sections/system/modals/data-ops-modal.ok.js +1 -1
  111. package/public/sections/system/parts/system-api-panel.ok.js +1 -1
  112. package/public/sections/system/parts/system-auth-panel.ok.js +1 -1
  113. package/public/sections/system/parts/system-log-history.ok.js +1 -1
  114. package/public/sections/system/parts/system-log.ok.js +1 -1
  115. package/public/sections/system/parts/system-status.ok.js +1 -1
  116. package/public/sections/system/parts/system-sync-panel.ok.js +1 -1
  117. package/public/sections/views/index.ok.html +1 -1
  118. package/public/sections/views/modals/create-view-modal.ok.js +1 -1
  119. package/public/sections/views/parts/view-detail.ok.js +1 -1
  120. package/public/sections/views/parts/views-list.ok.js +1 -1
  121. package/public/sections/views/parts/views-overview.ok.js +1 -1
  122. package/public/setup-app.js +1 -1
  123. package/public/setup.html +189 -1
  124. package/public/stores/auth-store.js +1 -1
  125. package/public/stores/events-store.js +1 -1
  126. package/public/stores/functions-store.js +1 -1
  127. package/public/stores/okdb-store.js +1 -1
  128. package/public/stores/theme-store.js +1 -1
  129. package/public/test-cron-input.html +53 -3
  130. package/public/test-json-input.html +182 -3
  131. package/public/util/permissions.js +1 -1
  132. package/public/util/sift-schema.js +1 -1
  133. package/public/vendor/okjs/ok.esm.min.js +1 -2
  134. package/public/view-models/type-query-vm.js +1 -1
  135. package/okdb-data/default/data.mdb +0 -0
  136. package/okdb-data/default/lock.mdb +0 -0
  137. package/okdb-data/~log/data.mdb +0 -0
  138. package/okdb-data/~log/lock.mdb +0 -0
  139. package/okdb-data/~system/data.mdb +0 -0
  140. package/okdb-data/~system/lock.mdb +0 -0
  141. package/public/images/logo.png +0 -0
  142. package/public/images/logo.svg +0 -333
@@ -1 +1 @@
1
- function _0x332d(){const _0x2d8cf2=['length','\x0a<div\x20style=\x22display:flex;\x20flex-direction:column;\x20gap:8px;\x20min-height:0;\x22>\x0a\x0a\x20\x20<div\x20style=\x22display:flex;\x20align-items:center;\x20justify-content:space-between;\x20gap:8px;\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22display:flex;\x20align-items:center;\x20gap:6px;\x20flex-wrap:wrap;\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-weight:600;\x20font-size:12px;\x20opacity:.7;\x20text-transform:uppercase;\x20letter-spacing:.05em;\x22>Types</div>\x0a\x20\x20\x20\x20\x20\x20<if\x20:=\x22totalWorkers()\x20>\x200\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:10px;\x20padding:1px\x206px;\x20border-radius:999px;\x20border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-accent)\x2027%,\x20transparent);\x20color:var(--okdb-accent);\x20background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x207%,\x20transparent);\x22>⚙\x20{{\x20totalWorkers()\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20<button\x20@click=\x22load\x22\x20:disabled=\x22busy\x22\x20style=\x22opacity:.6;\x20font-size:11px;\x20padding:2px\x206px;\x22>↻</button>\x0a\x20\x20</div>\x0a\x0a\x20\x20<div\x20style=\x22display:flex;\x20align-items:center;\x20gap:6px;\x22>\x0a\x20\x20\x20\x20<input\x0a\x20\x20\x20\x20\x20\x20:value=\x22filterText\x22\x0a\x20\x20\x20\x20\x20\x20@input=\x22filterText\x20=\x20$event.target.value\x22\x0a\x20\x20\x20\x20\x20\x20placeholder=\x22Filter\x20types…\x22\x0a\x20\x20\x20\x20\x20\x20style=\x22width:100%;\x20min-width:0;\x20padding:6px\x208px;\x20border-radius:6px;\x20border:1px\x20solid\x20var(--okdb-app-border);\x20background:var(--okdb-app-bg-deep);\x20color:var(--okdb-app-fg-strong);\x20font-size:12px;\x22\x0a\x20\x20\x20\x20/>\x0a\x20\x20\x20\x20<if\x20:=\x22filterText\x22>\x0a\x20\x20\x20\x20\x20\x20<button\x20@click=\x22clearFilter()\x22\x20style=\x22opacity:.7;\x20font-size:11px;\x20padding:5px\x207px;\x22>✕</button>\x0a\x20\x20\x20\x20</if>\x0a\x20\x20</div>\x0a\x0a\x20\x20<!--\x20Types\x20list\x20-->\x0a\x20\x20<div\x20style=\x22display:flex;\x20flex-direction:column;\x20gap:3px;\x20overflow:auto;\x20min-height:0;\x20padding-right:4px;\x22>\x0a\x20\x20\x20\x20<button\x0a\x20\x20\x20\x20\x20\x20@click=\x22selectType(null)\x22\x0a\x20\x20\x20\x20\x20\x20:class=\x22selectedType\x20==\x20null\x20?\x20\x27type-row\x20type-row--on\x27\x20:\x20\x27type-row\x27\x22>\x0a\x20\x20\x20\x20\x20\x20<span\x20style=\x22display:flex;\x20align-items:center;\x20gap:6px;\x20min-width:0;\x20flex:1;\x20text-align:left;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22white-space:nowrap;\x20overflow:hidden;\x20text-overflow:ellipsis;\x22>All\x20types</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22totalWorkers()\x20>\x200\x22><span\x20class=\x22worker-chip\x22>⚙\x20{{\x20totalWorkers()\x20}}</span></if>\x0a\x20\x20\x20\x20\x20\x20</span>\x0a\x20\x20\x20\x20\x20\x20<span\x20style=\x22opacity:.55;\x20font-size:11px;\x22>{{\x20totalFor(null)\x20}}</span>\x0a\x20\x20\x20\x20</button>\x0a\x0a\x20\x20\x20\x20<each\x20:of=\x22types\x22\x20let:item=\x22item\x22>\x0a\x20\x20\x20\x20\x20\x20<button\x0a\x20\x20\x20\x20\x20\x20\x20\x20@click=\x22selectType(item.type)\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20:class=\x22selectedType\x20===\x20item.type\x20?\x20\x27type-row\x20type-row--on\x27\x20:\x20\x27type-row\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22display:flex;\x20align-items:center;\x20gap:6px;\x20min-width:0;\x20flex:1;\x20text-align:left;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22jtStatusColor(item.type)\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20:style=\x22\x27width:6px;height:6px;border-radius:50%;background:\x27\x20+\x20jtStatusColor(item.type)\x20+\x20\x27;flex-shrink:0;\x27\x22\x20:title=\x22jtStatus(item.type)\x22></span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22white-space:nowrap;\x20overflow:hidden;\x20text-overflow:ellipsis;\x22>{{\x20item.type\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22item.workerCount\x20>\x200\x22><span\x20class=\x22worker-chip\x22>⚙\x20{{\x20item.workerCount\x20}}</span></if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22opacity:.55;\x20font-size:11px;\x22>{{\x20item.total\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20</each>\x0a\x0a\x20\x20\x20\x20<if\x20:=\x22!busy\x20&&\x20types.length\x20===\x200\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22opacity:.5;\x20font-size:12px;\x20padding:6px\x208px;\x20line-height:1.5;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20{{\x20filterText\x20?\x20\x27No\x20types\x20match\x20this\x20filter.\x27\x20:\x20\x27No\x20queued\x20job\x20types\x20in\x20this\x20environment.\x27\x20}}\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</if>\x0a\x20\x20</div>\x0a\x0a</div>\x0a','_reloadTimer','values','slice','catch','$attr','json','typesData','job_type','2uiAbyC','selectedEnv','status','registeredTypes','engine:error','loadFromStats','trim','engine:started','queue:reconciled','/queue/types','types','queue:retry','byStatus','4048UhjrDb','fromEntries','localeCompare','_mergeRegisteredTypes','has','total','result','byType','/engines?type=queue-worker','types\x20failed','25927891KAwcxt','isArray','keys','sort','13635550XcaiCr','532657GAcZUn','config','532XUtjEN','queue-types\x20load\x20error','queue:done','filterText','find','apiBase','queue-types\x20stats\x20fallback\x20error','filter','_unsubs','workerCounts','941164jMUYHJ','includes','busy','queue-types','selectedType','33165KgCLmD','queue:failed','stats\x20failed','26115XsIYKH','toLowerCase','2459769YavDmZ','type','jtStatus','$dispatch','78IxUafj','engine:uninstalled','selectedtype','map','$watch','all','data','/queue/stats','some','load'];_0x332d=function(){return _0x2d8cf2;};return _0x332d();}const _0x5f18da=_0x1e21;(function(_0x140853,_0x3a10c9){const _0x313516=_0x1e21,_0x29897b=_0x140853();while(!![]){try{const _0xb91319=-parseInt(_0x313516(0x15e))/0x1*(-parseInt(_0x313516(0x18c))/0x2)+parseInt(_0x313516(0x174))/0x3+parseInt(_0x313516(0x160))/0x4*(parseInt(_0x313516(0x172))/0x5)+-parseInt(_0x313516(0x178))/0x6*(-parseInt(_0x313516(0x16a))/0x7)+-parseInt(_0x313516(0x199))/0x8*(parseInt(_0x313516(0x16f))/0x9)+parseInt(_0x313516(0x15d))/0xa+-parseInt(_0x313516(0x159))/0xb;if(_0xb91319===_0x3a10c9)break;else _0x29897b['push'](_0x29897b['shift']());}catch(_0x6617bb){_0x29897b['push'](_0x29897b['shift']());}}}(_0x332d,0xe4c13));function _0x1e21(_0x30a59c,_0x2d7c22){_0x30a59c=_0x30a59c-0x153;const _0x332dd0=_0x332d();let _0x1e21dc=_0x332dd0[_0x30a59c];return _0x1e21dc;}import _0x1bd5d0 from'../../../stores/events-store.js';import _0x5defce from'../../../stores/okdb-store.js';const s={'paused':'#f59e0b','draining':'#3b82f6'};export default{'tag':_0x5f18da(0x16d),'attr':{'selectedtype':{}},'context':_0x5b1e27=>({'busy':!0x1,'total':0x0,'typesData':[],'workerCounts':{},'registeredTypes':{},'filterText':'','_unsubs':[],'_reloadTimer':null,'selectedType':null,async 'load'(){const _0x5d0c08=_0x5f18da;this[_0x5d0c08(0x16c)]=!0x0;try{const [_0x479a4b,_0x34924b,_0x8489ce]=await Promise[_0x5d0c08(0x17d)]([fetch(_0x5defce[_0x5d0c08(0x165)]+_0x5d0c08(0x195)),fetch(_0x5defce['apiBase']+_0x5d0c08(0x157))[_0x5d0c08(0x187)](()=>null),fetch(_0x5defce['apiBase']+'/queue/job-types')[_0x5d0c08(0x187)](()=>null)]);if(!_0x479a4b['ok'])throw new Error(_0x5d0c08(0x158));const _0x575b60=await _0x479a4b[_0x5d0c08(0x189)](),_0x187d3a=_0x575b60?.[_0x5d0c08(0x155)]??_0x575b60?.[_0x5d0c08(0x17e)]??null,_0x54f812=_0x34924b?.['ok']?await _0x34924b[_0x5d0c08(0x189)]()[_0x5d0c08(0x187)](()=>({})):null,_0x43e841=_0x54f812?.[_0x5d0c08(0x155)]??_0x54f812?.[_0x5d0c08(0x17e)]??[],_0x44c057={};for(const _0x5a3c3e of _0x43e841){const _0x16f1c5=_0x5a3c3e?.[_0x5d0c08(0x15f)]?.[_0x5d0c08(0x18b)];_0x16f1c5&&(_0x44c057[_0x16f1c5]=(_0x44c057[_0x16f1c5]??0x0)+0x1);}if(this[_0x5d0c08(0x169)]=_0x44c057,_0x8489ce?.['ok']){const _0x1e3c2c=await _0x8489ce[_0x5d0c08(0x189)]()[_0x5d0c08(0x187)](()=>({})),_0x23dfc9=_0x1e3c2c?.['result']??[];this[_0x5d0c08(0x18f)]=Object[_0x5d0c08(0x19a)](_0x23dfc9[_0x5d0c08(0x17b)](_0xc12cc7=>[_0xc12cc7['id'],_0xc12cc7]));}this[_0x5d0c08(0x154)]=_0x187d3a?.[_0x5d0c08(0x154)]??0x0,this[_0x5d0c08(0x18a)]=Array[_0x5d0c08(0x15a)](_0x187d3a?.['types'])?_0x187d3a[_0x5d0c08(0x196)][_0x5d0c08(0x186)]()[_0x5d0c08(0x15c)]((_0x5e1038,_0x78e965)=>String(_0x5e1038?.[_0x5d0c08(0x175)]||'')[_0x5d0c08(0x19b)](String(_0x78e965?.[_0x5d0c08(0x175)]||'')))[_0x5d0c08(0x17b)](_0x5bc269=>({..._0x5bc269,'workerCount':_0x44c057[_0x5bc269?.[_0x5d0c08(0x175)]]??0x0})):[],this[_0x5d0c08(0x18a)][_0x5d0c08(0x182)]||await this[_0x5d0c08(0x191)](),this['_mergeRegisteredTypes'](),this[_0x5d0c08(0x16e)]&&!this['typesData'][_0x5d0c08(0x180)](_0x3ee7d1=>_0x3ee7d1?.[_0x5d0c08(0x175)]===this[_0x5d0c08(0x16e)])&&(this[_0x5d0c08(0x16e)]=null,_0x5b1e27[_0x5d0c08(0x177)](_0x5d0c08(0x175),null));}catch(_0x2f0acd){console['error'](_0x5d0c08(0x161),_0x2f0acd),await this[_0x5d0c08(0x191)](),this[_0x5d0c08(0x19c)]();}finally{this[_0x5d0c08(0x16c)]=!0x1;}},'_mergeRegisteredTypes'(){const _0x6af768=_0x5f18da,_0x14e208=new Set(this['typesData'][_0x6af768(0x17b)](_0x4897d1=>_0x4897d1?.[_0x6af768(0x175)]));for(const _0x38e27b of Object['keys'](this['registeredTypes']))_0x14e208[_0x6af768(0x153)](_0x38e27b)||this['typesData']['push']({'type':_0x38e27b,'total':0x0,'byStatus':{},'workerCount':this[_0x6af768(0x169)][_0x38e27b]??0x0});this[_0x6af768(0x18a)]['sort']((_0x2d6c00,_0x4e34ab)=>String(_0x2d6c00?.[_0x6af768(0x175)]||'')[_0x6af768(0x19b)](String(_0x4e34ab?.['type']||'')));},async 'loadFromStats'(){const _0x3cbd37=_0x5f18da;try{const _0x1ac8a1=await fetch(_0x5defce[_0x3cbd37(0x165)]+_0x3cbd37(0x17f));if(!_0x1ac8a1['ok'])throw new Error(_0x3cbd37(0x171));const _0xd10230=await _0x1ac8a1[_0x3cbd37(0x189)](),_0x3e14cc=_0xd10230?.['result']??_0xd10230?.[_0x3cbd37(0x17e)]??null,_0x122fb1=_0x3e14cc?.[_0x3cbd37(0x156)]||{};this['total']=_0x3e14cc?.[_0x3cbd37(0x154)]??0x0,this['typesData']=Object[_0x3cbd37(0x15b)](_0x122fb1)['sort']((_0xbbea02,_0x35245f)=>_0xbbea02[_0x3cbd37(0x19b)](_0x35245f))[_0x3cbd37(0x17b)](_0x417dc5=>({'type':_0x417dc5,'total':_0x122fb1[_0x417dc5]?.['total']??0x0,'byStatus':_0x122fb1[_0x417dc5]?.[_0x3cbd37(0x198)]||{},'workerCount':this[_0x3cbd37(0x169)]?.[_0x417dc5]??0x0}));}catch(_0x102132){console['error'](_0x3cbd37(0x166),_0x102132),this[_0x3cbd37(0x154)]=0x0,this[_0x3cbd37(0x18a)]=[];}},get 'types'(){const _0x495278=_0x5f18da,_0x122a0b=(this[_0x495278(0x163)]||'')[_0x495278(0x192)]()[_0x495278(0x173)]();return _0x122a0b?this['typesData'][_0x495278(0x167)](_0x246922=>String(_0x246922?.['type']||'')[_0x495278(0x173)]()[_0x495278(0x16b)](_0x122a0b)):this[_0x495278(0x18a)];},'totalFor'(_0x1109ef){const _0x384b44=_0x5f18da;return _0x1109ef?this['typesData'][_0x384b44(0x164)](_0x143712=>_0x143712?.[_0x384b44(0x175)]===_0x1109ef)?.[_0x384b44(0x154)]??0x0:this[_0x384b44(0x154)];},'totalWorkers'(){const _0x556931=_0x5f18da;return Object[_0x556931(0x185)](this[_0x556931(0x169)]||{})['reduce']((_0x1dc02e,_0x59b8a1)=>_0x1dc02e+Number(_0x59b8a1||0x0),0x0);},'jtStatus'(_0x5f0918){const _0xbf63c3=_0x5f18da;return this[_0xbf63c3(0x18f)][_0x5f0918]?.[_0xbf63c3(0x18e)]??null;},'jtStatusColor'(_0x3fa8ae){const _0x1fa046=_0x5f18da;return s[this[_0x1fa046(0x176)](_0x3fa8ae)]??null;},'selectType'(_0x386fb4){const _0x5dc408=_0x5f18da;this[_0x5dc408(0x16e)]=_0x386fb4??null,_0x5b1e27[_0x5dc408(0x177)]('type',this[_0x5dc408(0x16e)]);},async 'refresh'(){await this['load']();},'clearFilter'(){this['filterText']='';},'_scheduleLoad'(){const _0x1f01de=_0x5f18da;clearTimeout(this['_reloadTimer']),this[_0x1f01de(0x184)]=setTimeout(()=>this[_0x1f01de(0x181)](),0x190);}}),async 'mount'(_0x2620b1){const _0x3ff16b=_0x5f18da;await this[_0x3ff16b(0x181)](),this[_0x3ff16b(0x16e)]=_0x2620b1[_0x3ff16b(0x188)][_0x3ff16b(0x17a)]??null;const _0x201673=()=>this['_scheduleLoad']();this[_0x3ff16b(0x168)]=[_0x1bd5d0['on']('queue:enqueued',_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x162),_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x170),_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x197),_0x201673),_0x1bd5d0['on']('queue:removed',_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x194),_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x193),_0x201673),_0x1bd5d0['on']('engine:stopped',_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x190),_0x201673),_0x1bd5d0['on'](_0x3ff16b(0x179),_0x201673)],_0x2620b1[_0x3ff16b(0x17c)](()=>_0x5defce[_0x3ff16b(0x18d)],()=>{const _0xf74b73=_0x3ff16b;this[_0xf74b73(0x154)]=0x0,this[_0xf74b73(0x18a)]=[],this[_0xf74b73(0x163)]='',this[_0xf74b73(0x16e)]=null,_0x2620b1['$dispatch'](_0xf74b73(0x175),null),this[_0xf74b73(0x181)]();}),_0x2620b1['$watch'](()=>_0x2620b1[_0x3ff16b(0x188)]['selectedtype'],_0x4d2c77=>{const _0x3c5039=_0x3ff16b;this[_0x3c5039(0x16e)]=_0x4d2c77??null;});},'unmount'(){const _0x4520fa=_0x5f18da;clearTimeout(this[_0x4520fa(0x184)]);for(const _0x5e0b69 of this[_0x4520fa(0x168)])_0x5e0b69();this[_0x4520fa(0x168)]=[];},'template':_0x5f18da(0x183),'style':'.type-row{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px\x208px;border-radius:5px;font-size:12px;border:1px\x20solid\x20transparent;background:0\x200;cursor:pointer;width:100%;color:var(--okdb-app-fg-muted);transition:background\x20.12s,border-color\x20.12s,color\x20.12s}.type-row--on,.type-row:hover{background:var(--okdb-app-surface);border-color:var(--okdb-app-border);color:var(--okdb-app-fg)}.type-row--on{border-color:var(--okdb-accent);color:var(--okdb-app-fg-strong)}.worker-chip{display:inline-flex;align-items:center;gap:4px;padding:1px\x206px;border-radius:999px;border:1px\x20solid\x20color-mix(in\x20srgb,var(--okdb-accent)\x2020%,transparent);color:var(--okdb-accent);background:color-mix(in\x20srgb,var(--okdb-accent)\x205%,transparent);font-size:10px;flex-shrink:0}'};
1
+ import t from"../../../stores/events-store.js";import e from"../../../stores/okdb-store.js";const s={paused:"#f59e0b",draining:"#3b82f6"};export default{tag:"queue-types",attr:{selectedtype:{}},context:t=>({busy:!1,total:0,typesData:[],workerCounts:{},registeredTypes:{},filterText:"",_unsubs:[],_reloadTimer:null,selectedType:null,async load(){this.busy=!0;try{const[s,o,a]=await Promise.all([fetch(e.apiBase+"/queue/types"),fetch(e.apiBase+"/engines?type=queue-worker").catch(()=>null),fetch(e.apiBase+"/queue/job-types").catch(()=>null)]);if(!s.ok)throw new Error("types failed");const n=await s.json(),r=n?.result??n?.data??null,i=o?.ok?await o.json().catch(()=>({})):null,l=i?.result??i?.data??[],p={};for(const t of l){const e=t?.config?.job_type;e&&(p[e]=(p[e]??0)+1)}if(this.workerCounts=p,a?.ok){const t=await a.json().catch(()=>({})),e=t?.result??[];this.registeredTypes=Object.fromEntries(e.map(t=>[t.id,t]))}this.total=r?.total??0,this.typesData=Array.isArray(r?.types)?r.types.slice().sort((t,e)=>String(t?.type||"").localeCompare(String(e?.type||""))).map(t=>({...t,workerCount:p[t?.type]??0})):[],this.typesData.length||await this.loadFromStats(),this._mergeRegisteredTypes(),this.selectedType&&!this.typesData.some(t=>t?.type===this.selectedType)&&(this.selectedType=null,t.$dispatch("type",null))}catch(t){console.error("queue-types load error",t),await this.loadFromStats(),this._mergeRegisteredTypes()}finally{this.busy=!1}},_mergeRegisteredTypes(){const t=new Set(this.typesData.map(t=>t?.type));for(const e of Object.keys(this.registeredTypes))t.has(e)||this.typesData.push({type:e,total:0,byStatus:{},workerCount:this.workerCounts[e]??0});this.typesData.sort((t,e)=>String(t?.type||"").localeCompare(String(e?.type||"")))},async loadFromStats(){try{const t=await fetch(e.apiBase+"/queue/stats");if(!t.ok)throw new Error("stats failed");const s=await t.json(),o=s?.result??s?.data??null,a=o?.byType||{};this.total=o?.total??0,this.typesData=Object.keys(a).sort((t,e)=>t.localeCompare(e)).map(t=>({type:t,total:a[t]?.total??0,byStatus:a[t]?.byStatus||{},workerCount:this.workerCounts?.[t]??0}))}catch(t){console.error("queue-types stats fallback error",t),this.total=0,this.typesData=[]}},get types(){const t=(this.filterText||"").trim().toLowerCase();return t?this.typesData.filter(e=>String(e?.type||"").toLowerCase().includes(t)):this.typesData},totalFor(t){return t?this.typesData.find(e=>e?.type===t)?.total??0:this.total},totalWorkers(){return Object.values(this.workerCounts||{}).reduce((t,e)=>t+Number(e||0),0)},jtStatus(t){return this.registeredTypes[t]?.status??null},jtStatusColor(t){return s[this.jtStatus(t)]??null},selectType(e){this.selectedType=e??null,t.$dispatch("type",this.selectedType)},async refresh(){await this.load()},clearFilter(){this.filterText=""},_scheduleLoad(){clearTimeout(this._reloadTimer),this._reloadTimer=setTimeout(()=>this.load(),400)}}),async mount(s){await this.load(),this.selectedType=s.$attr.selectedtype??null;const o=()=>this._scheduleLoad();this._unsubs=[t.on("queue:enqueued",o),t.on("queue:done",o),t.on("queue:failed",o),t.on("queue:retry",o),t.on("queue:removed",o),t.on("queue:reconciled",o),t.on("engine:started",o),t.on("engine:stopped",o),t.on("engine:error",o),t.on("engine:uninstalled",o)],s.$watch(()=>e.selectedEnv,()=>{this.total=0,this.typesData=[],this.filterText="",this.selectedType=null,s.$dispatch("type",null),this.load()}),s.$watch(()=>s.$attr.selectedtype,t=>{this.selectedType=t??null})},unmount(){clearTimeout(this._reloadTimer);for(const t of this._unsubs)t();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:8px; min-height:0;">\n\n <div style="display:flex; align-items:center; justify-content:space-between; gap:8px;">\n <div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap;">\n <div style="font-weight:600; font-size:12px; opacity:.7; text-transform:uppercase; letter-spacing:.05em;">Types</div>\n <if :="totalWorkers() > 0">\n <span style="font-size:10px; padding:1px 6px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-accent) 27%, transparent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);">⚙ {{ totalWorkers() }}</span>\n </if>\n </div>\n <button @click="load" :disabled="busy" style="opacity:.6; font-size:11px; padding:2px 6px;">↻</button>\n </div>\n\n <div style="display:flex; align-items:center; gap:6px;">\n <input\n :value="filterText"\n @input="filterText = $event.target.value"\n placeholder="Filter types…"\n style="width:100%; min-width:0; padding:6px 8px; border-radius:6px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-strong); font-size:12px;"\n />\n <if :="filterText">\n <button @click="clearFilter()" style="opacity:.7; font-size:11px; padding:5px 7px;">✕</button>\n </if>\n </div>\n\n \x3c!-- Types list --\x3e\n <div style="display:flex; flex-direction:column; gap:3px; overflow:auto; min-height:0; padding-right:4px;">\n <button\n @click="selectType(null)"\n :class="selectedType == null ? \'type-row type-row--on\' : \'type-row\'">\n <span style="display:flex; align-items:center; gap:6px; min-width:0; flex:1; text-align:left;">\n <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">All types</span>\n <if :="totalWorkers() > 0"><span class="worker-chip">⚙ {{ totalWorkers() }}</span></if>\n </span>\n <span style="opacity:.55; font-size:11px;">{{ totalFor(null) }}</span>\n </button>\n\n <each :of="types" let:item="item">\n <button\n @click="selectType(item.type)"\n :class="selectedType === item.type ? \'type-row type-row--on\' : \'type-row\'">\n <span style="display:flex; align-items:center; gap:6px; min-width:0; flex:1; text-align:left;">\n <if :="jtStatusColor(item.type)">\n <span :style="\'width:6px;height:6px;border-radius:50%;background:\' + jtStatusColor(item.type) + \';flex-shrink:0;\'" :title="jtStatus(item.type)"></span>\n </if>\n <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ item.type }}</span>\n <if :="item.workerCount > 0"><span class="worker-chip">⚙ {{ item.workerCount }}</span></if>\n </span>\n <span style="opacity:.55; font-size:11px;">{{ item.total }}</span>\n </button>\n </each>\n\n <if :="!busy && types.length === 0">\n <div style="opacity:.5; font-size:12px; padding:6px 8px; line-height:1.5;">\n {{ filterText ? \'No types match this filter.\' : \'No queued job types in this environment.\' }}\n </div>\n </if>\n </div>\n\n</div>\n',style:".type-row{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;border-radius:5px;font-size:12px;border:1px solid transparent;background:0 0;cursor:pointer;width:100%;color:var(--okdb-app-fg-muted);transition:background .12s,border-color .12s,color .12s}.type-row--on,.type-row:hover{background:var(--okdb-app-surface);border-color:var(--okdb-app-border);color:var(--okdb-app-fg)}.type-row--on{border-color:var(--okdb-accent);color:var(--okdb-app-fg-strong)}.worker-chip{display:inline-flex;align-items:center;gap:4px;padding:1px 6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--okdb-accent) 20%,transparent);color:var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 5%,transparent);font-size:10px;flex-shrink:0}"};
@@ -1 +1 @@
1
- const _0x5b4081=_0x52ef;(function(_0x235e21,_0x3f1304){const _0x5657c8=_0x52ef,_0x5f1838=_0x235e21();while(!![]){try{const _0x54c268=parseInt(_0x5657c8(0x208))/0x1+parseInt(_0x5657c8(0x22a))/0x2+-parseInt(_0x5657c8(0x1d4))/0x3+parseInt(_0x5657c8(0x1da))/0x4*(parseInt(_0x5657c8(0x1f2))/0x5)+-parseInt(_0x5657c8(0x1c3))/0x6*(-parseInt(_0x5657c8(0x213))/0x7)+-parseInt(_0x5657c8(0x1cf))/0x8*(parseInt(_0x5657c8(0x217))/0x9)+-parseInt(_0x5657c8(0x204))/0xa;if(_0x54c268===_0x3f1304)break;else _0x5f1838['push'](_0x5f1838['shift']());}catch(_0x515b62){_0x5f1838['push'](_0x5f1838['shift']());}}}(_0x47e3,0x53b16));import _0x5268d5 from'../../../stores/events-store.js';import _0xb8fb87 from'../../../stores/okdb-store.js';function _0x47e3(){const _0x18f1ea=['/functions/preview','Must\x20be\x20(ctx)\x20=>\x20{\x20…\x20}','$attr','986178EfRGPP','workers','apiBase','functions','catch','async\x20(ctx)\x20=>\x20{\x0a\x20\x20//\x20ctx.payload\x20—\x20the\x20raw\x20job\x20payload\x20(whatever\x20was\x20enqueued)\x0a\x20\x20//\x20ctx.job\x20\x20\x20\x20\x20—\x20queue\x20job\x20context\x0a\x20\x20//\x20\x20\x20ctx.job.id,\x20ctx.job.type,\x20ctx.job.tries,\x20ctx.job.tags,\x20...\x0a\x20\x20//\x20\x20\x20ctx.job.heartbeat()\x20\x20\x20\x20\x20\x20\x20\x20—\x20keep\x20the\x20job\x20alive\x0a\x20\x20//\x20\x20\x20ctx.job.markProgress(msg)\x20\x20—\x20report\x20progress\x0a\x20\x20//\x20ctx.env\x20\x20\x20\x20\x20—\x20env\x20facade:\x20put,\x20get,\x20query,\x20txn,\x20...\x0a\x20\x20//\x20ctx.log\x20\x20\x20\x20\x20—\x20structured\x20logger:\x20ctx.log.info(msg,\x20data)\x0a\x0a\x20\x20return\x20{\x20ok:\x20true\x20};\x0a}','handler','DELETE','6LgRfue','durationMs','now','#facc15','Worker\x20','sort','logs','samplePayload','var(--okdb-danger)','status','return\x20(','canCreate','4936MYNkKM','loadFunctions','building','stringify','processed','739386VlDGFH','_unsubs','$ok','restart','previewRunning','name','622340wCDbHr','reduce','createError','load','envName','/engines?type=queue-worker','execution','Worker\x20\x22','fnSource','-preview-1','modal','/engines/','\x0a<div\x20style=\x22display:flex;flex-direction:column;gap:14px;flex:1;overflow:auto;padding-right:4px;\x22>\x0a\x0a\x20\x20<!--\x20Header\x20-->\x0a\x20\x20<div\x20style=\x22display:flex;align-items:center;gap:8px;flex-wrap:wrap;\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22font-weight:600;font-size:15px;\x22>\x0a\x20\x20\x20\x20\x20\x20Queue\x20Workers\x0a\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20<if\x20:=\x22jobType\x22>\x0a\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:11px;padding:2px\x208px;border-radius:999px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x209%,\x20transparent);border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-accent)\x2027%,\x20transparent);color:var(--okdb-accent);\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20bound\x20to\x20{{\x20jobType\x20}}\x0a\x20\x20\x20\x20\x20\x20</span>\x0a\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20<if\x20:=\x22workers.length\x20>\x200\x22>\x0a\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:11px;padding:2px\x208px;border-radius:999px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x209%,\x20transparent);border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-accent)\x2027%,\x20transparent);color:var(--okdb-accent);\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20{{\x20workers.length\x20}}\x20worker{{\x20workers.length\x20!==\x201\x20?\x20\x27s\x27\x20:\x20\x27\x27\x20}}\x0a\x20\x20\x20\x20\x20\x20</span>\x0a\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20<div\x20style=\x22flex:1;\x22></div>\x0a\x20\x20\x20\x20<button\x20@click=\x22load()\x22\x20:disabled=\x22busy\x22\x20style=\x22font-size:11px;padding:3px\x208px;opacity:.6;\x22>↻</button>\x0a\x20\x20\x20\x20<if\x20:=\x22!showCreate\x20&&\x20hasBoundJobType\x22>\x0a\x20\x20\x20\x20\x20\x20<button\x20@click=\x22openCreate()\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20style=\x22font-size:12px;padding:5px\x2014px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x2013%,\x20transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20+\x20Add\x20Worker\x0a\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20</if>\x0a\x20\x20</div>\x0a\x0a\x20\x20<if\x20:=\x22jobType\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;padding:12px\x2014px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;gap:4px;min-width:220px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Scope</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:14px;font-weight:600;\x22>{{\x20jobType\x20}}</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:12px;opacity:.55;line-height:1.6;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Workers\x20here\x20are\x20explicitly\x20tied\x20to\x20this\x20queue\x20job\x20type.\x20They\x20are\x20not\x20general-purpose\x20consumers.\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;gap:18px;flex-wrap:wrap;font-size:11px;opacity:.65;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span>workers:\x20<b\x20style=\x22color:var(--okdb-app-fg-strong);opacity:1;\x22>{{\x20workers.length\x20}}</b></span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span>processed:\x20<b\x20style=\x22color:var(--okdb-success);opacity:1;\x22>{{\x20totalProcessed\x20}}</b></span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22totalFailed\x20>\x200\x22><span>failed:\x20<b\x20style=\x22color:var(--okdb-danger);opacity:1;\x22>{{\x20totalFailed\x20}}</b></span></if>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</div>\x0a\x20\x20</if>\x0a\x0a\x20\x20<if\x20:=\x22createSuccess\x20&&\x20!showCreate\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-success);font-size:12px;padding:6px\x2010px;border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-success)\x2020%,\x20transparent);border-radius:6px;\x22>\x0a\x20\x20\x20\x20\x20\x20✓\x20{{\x20createSuccess\x20}}\x0a\x20\x20\x20\x20</div>\x0a\x20\x20</if>\x0a\x20\x20<if\x20:=\x22error\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-danger);font-size:12px;padding:6px\x2010px;border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);border-radius:6px;\x22>{{\x20error\x20}}</div>\x0a\x20\x20</if>\x0a\x0a\x20\x20<if\x20:=\x22!hasBoundJobType\x20&&\x20!showCreate\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22padding:32px\x2024px;background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;display:flex;flex-direction:column;gap:12px;align-items:center;text-align:center;\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:34px;opacity:.18;\x22>⇶</div>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:14px;font-weight:600;opacity:.72;\x22>Select\x20a\x20queue\x20job\x20type</div>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:12px;opacity:.48;line-height:1.8;max-width:430px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20Queue\x20workers\x20are\x20managed\x20per\x20job\x20type.\x20Pick\x20a\x20type\x20from\x20the\x20sidebar\x20to\x20view\x20its\x20workers,\x0a\x20\x20\x20\x20\x20\x20\x20\x20create\x20a\x20new\x20one,\x20and\x20test\x20it\x20against\x20sample\x20job\x20payloads.\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</div>\x0a\x20\x20</if>\x0a\x0a\x20\x20<!--\x20Worker\x20list\x20-->\x0a\x20\x20<if\x20:=\x22!showCreate\x22>\x0a\x20\x20\x20\x20<if\x20:=\x22hasBoundJobType\x20&&\x20!busy\x20&&\x20workers.length\x20===\x200\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22padding:28px\x2020px;background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;text-align:center;display:flex;flex-direction:column;gap:10px;align-items:center;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:32px;opacity:.2;\x22>⚙</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:14px;font-weight:600;opacity:.7;\x22>No\x20workers\x20yet\x20for\x20\x22{{\x20jobType\x20}}\x22</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:12px;opacity:.45;max-width:420px;line-height:1.7;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Create\x20a\x20worker\x20bound\x20to\x20this\x20queue\x20job\x20type.\x20The\x20worker\x20will\x20consume\x20only\x20<code>{{\x20jobType\x20}}</code>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20jobs\x20and\x20run\x20a\x20specific\x20function\x20handler.\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22openCreate()\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22margin-top:4px;font-size:12px;padding:6px\x2018px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x2013%,\x20transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20+\x20Add\x20Worker\x0a\x20\x20\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</if>\x0a\x0a\x20\x20\x20\x20<each\x20:of=\x22workers\x22\x20let:item=\x22w\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22background:var(--okdb-app-surface);border:1px\x20solid\x20var(--okdb-app-border);border-radius:12px;padding:14px\x2016px;display:flex;flex-direction:column;gap:10px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:center;gap:8px;min-width:0;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<engine-row-item\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20:engine=\x22w\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20label=\x22Worker\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20label-color=\x22var(--okdb-accent)\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20:subtitle=\x22stateLabel(w)\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20@select=\x22openWorkerModal($event.detail)\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22doAction(w,\x27pause\x27)\x22\x20\x20\x20title=\x22Pause\x22\x20\x20\x20style=\x22font-size:11px;padding:2px\x207px;color:var(--okdb-warning);border-color:color-mix(in\x20srgb,\x20var(--okdb-warning)\x2033%,\x20transparent);\x22>⏸</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22doAction(w,\x27resume\x27)\x22\x20\x20title=\x22Resume\x22\x20\x20style=\x22font-size:11px;padding:2px\x207px;color:var(--okdb-success);border-color:color-mix(in\x20srgb,\x20var(--okdb-success)\x2033%,\x20transparent);\x22>▶</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22doAction(w,\x27restart\x27)\x22\x20title=\x22Restart\x22\x20style=\x22font-size:11px;padding:2px\x207px;opacity:.6;\x22>↺</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22doDelete(w)\x22\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20title=\x22Delete\x22\x20\x20style=\x22font-size:11px;padding:2px\x207px;color:var(--okdb-danger);border-color:color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);\x22>🗑</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px\x2014px;font-size:11px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Job\x20type</div><div\x20style=\x22margin-top:3px;color:var(--okdb-app-fg-strong);\x22>{{\x20w.config?.job_type\x20||\x20\x27—\x27\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Handler</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22margin-top:3px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22w.config?.handler?.name\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22openWorkerFunction(w,\x20$event)\x22\x20title=\x22Open\x20this\x20handler\x20in\x20the\x20Functions\x20section.\x22\x20style=\x22background:none;border:none;padding:0;color:var(--okdb-accent);font-size:11px;text-decoration:underline;text-underline-offset:2px;cursor:pointer;\x22>ƒ\x20{{\x20w.config?.handler?.name\x20}}\x20↗</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22!w.config?.handler?.name\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22color:var(--okdb-app-fg-strong);\x22>—</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Concurrency</div><div\x20style=\x22margin-top:3px;color:var(--okdb-app-fg-strong);\x22>{{\x20w.config?.concurrency\x20??\x201\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Poll\x20interval</div><div\x20style=\x22margin-top:3px;color:var(--okdb-app-fg-strong);\x22>{{\x20w.config?.pollInterval\x20??\x20100\x20}}\x20ms</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22w.stats\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Processed</div><div\x20style=\x22margin-top:3px;color:var(--okdb-success);\x22>{{\x20w.stats.processed\x20??\x200\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;\x22>Failed</div><div\x20:style=\x22\x27margin-top:3px;color:\x27\x20+\x20((w.stats.failed\x20??\x200)\x20>\x200\x20?\x20\x27var(--okdb-danger)\x27\x20:\x20\x27var(--okdb-app-fg-strong)\x27)\x22>{{\x20w.stats.failed\x20??\x200\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22w.stats?.error\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-danger);font-size:11px;padding:4px\x208px;border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);border-radius:4px;font-family:monospace;\x22>{{\x20w.stats.error\x20}}</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</each>\x0a\x20\x20</if>\x0a\x0a\x20\x20<!--\x20Create\x20panel\x20-->\x0a\x20\x20<if\x20:=\x22showCreate\x22>\x0a\x20\x20\x20\x20<div\x20style=\x22background:var(--okdb-app-surface);border:1px\x20solid\x20var(--okdb-app-border);border-radius:12px;padding:18px;display:flex;flex-direction:column;gap:16px;\x22>\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span>New\x20Queue\x20Worker</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:12px;color:var(--okdb-accent);opacity:.8;\x22>bound\x20to\x20{{\x20jobType\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22flex:1;\x22></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22cancelCreate()\x22\x20style=\x22font-size:11px;padding:2px\x208px;opacity:.6;\x22>✕\x20Cancel</button>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:minmax(0,1.35fr)\x20minmax(320px,.95fr);gap:16px;align-items:start;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;gap:14px;min-width:0;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22padding:14px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Binding</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px\x2014px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;\x22>Environment</div><div\x20style=\x22margin-top:3px;color:var(--okdb-app-fg-strong);\x22>{{\x20envName\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div><div\x20style=\x22font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;\x22>Job\x20type</div><div\x20style=\x22margin-top:3px;color:var(--okdb-accent);font-weight:600;\x22>{{\x20jobType\x20}}</div></div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:11px;opacity:.5;line-height:1.6;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20This\x20worker\x20will\x20consume\x20only\x20<code>{{\x20jobType\x20}}</code>\x20jobs.\x20The\x20job\x20type\x20is\x20fixed\x20by\x20the\x20queue\x20sidebar\x20selection.\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22padding:14px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:center;gap:8px;flex-wrap:wrap;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Handler</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;gap:6px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22pickNewFn()\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20:style=\x22fnMode===\x27new\x27\x20?\x20\x27font-size:11px;padding:2px\x2010px;border-color:var(--okdb-accent);color:var(--okdb-accent);background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x207%,\x20transparent);\x27\x20:\x20\x27font-size:11px;padding:2px\x2010px;opacity:.55;\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Write\x20new\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22pickExistingFn()\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20:style=\x22fnMode===\x27existing\x27\x20?\x20\x27font-size:11px;padding:2px\x2010px;border-color:var(--okdb-accent);color:var(--okdb-accent);background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x207%,\x20transparent);\x27\x20:\x20\x27font-size:11px;padding:2px\x2010px;opacity:.55;\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Pick\x20existing\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22fnMode\x20===\x20\x27new\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:120px\x201fr;gap:10px;align-items:center;font-size:13px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22opacity:.6;\x22>Function\x20name</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<input\x20:value=\x22fnName\x22\x20@input=\x22fnName\x20=\x20$event.target.value\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20placeholder=\x22e.g.\x20handleOrderJob\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:6px;padding:6px\x208px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;\x22\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;gap:6px;min-width:0;min-height:0;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Implementation</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;min-height:420px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;overflow:hidden;background:#10171e;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px\x2012px;border-bottom:1px\x20solid\x20var(--okdb-app-border);\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Worker\x20handler</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;opacity:.4;font-family:monospace;\x22>async\x20(ctx)\x20=&gt;\x20{\x20}</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22flex:1;min-height:0;background:#0d1318;display:flex;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<ok-js-input\x20:value=\x22fnSource\x22\x20@edit=\x22updateSource($event.detail)\x22\x20style=\x22display:block;\x22\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22fnValidation.ok\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22color:#86efac;font-size:11px;\x22>✓\x20Valid\x20function\x20source</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22!fnValidation.ok\x20&&\x20fnSource.trim()\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-danger);font-size:11px;\x22>{{\x20fnValidation.message\x20}}</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22fnMode\x20===\x20\x27existing\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:120px\x201fr;gap:10px;align-items:center;font-size:13px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22opacity:.6;\x22>Function</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<select\x20:value=\x22existingFn\x22\x20@change=\x22existingFn\x20=\x20$event.target.value\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:6px;padding:6px\x208px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<option\x20value=\x22\x22>—\x20select\x20function\x20—</option>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<each\x20:of=\x22functions\x22\x20let:item=\x22fn\x22><option\x20:value=\x22fn\x22>{{\x20fn\x20}}</option></each>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</select>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:11px;opacity:.5;line-height:1.6;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Pick\x20an\x20existing\x20function\x20already\x20stored\x20in\x20this\x20environment.\x20Inline\x20test\x20preview\x20is\x20available\x20when\x20writing\x20a\x20new\x20function\x20in\x20this\x20panel.\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;gap:14px;min-width:0;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22padding:14px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Runtime</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:grid;grid-template-columns:1fr\x201fr;gap:10px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;\x22>Concurrency</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<input\x20type=\x22number\x22\x20min=\x221\x22\x20max=\x2264\x22\x20:value=\x22concurrency\x22\x20@input=\x22concurrency\x20=\x20$event.target.value\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22margin-top:4px;background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:6px;padding:6px\x208px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;\x22\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;\x22>Poll\x20interval</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<input\x20type=\x22number\x22\x20min=\x2210\x22\x20max=\x2210000\x22\x20:value=\x22pollInterval\x22\x20@input=\x22pollInterval\x20=\x20$event.target.value\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22margin-top:4px;background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);border-radius:6px;padding:6px\x208px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;\x22\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;opacity:.4;margin-top:4px;\x22>milliseconds</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22padding:14px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:center;gap:8px;flex-wrap:wrap;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;\x22>Testing</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22fnMode\x20!==\x20\x27new\x27\x22><span\x20style=\x22font-size:10px;color:var(--okdb-warning);opacity:.8;\x22>inline\x20preview\x20works\x20for\x20new\x20functions</span></if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;\x22>Sample\x20job\x20payload</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22margin-top:6px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:8px;overflow:hidden;background:var(--okdb-app-bg-deep);min-height:220px;display:flex;flex-direction:column;min-width:0;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<ok-json-input\x20mode=\x22textbox\x22\x20:value=\x22samplePayload\x22\x20@edit=\x22samplePayload\x20=\x20$event.detail\x22\x20style=\x22height:100%;display:flex;min-height:220px;\x22\x20/>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22fnMode\x20===\x20\x27new\x27\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;align-items:center;gap:8px;flex-wrap:wrap;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22preview()\x22\x20:disabled=\x22!fnValidation.ok\x20||\x20previewRunning\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22font-size:12px;padding:5px\x2014px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x209%,\x20transparent);border-color:color-mix(in\x20srgb,\x20var(--okdb-accent)\x2027%,\x20transparent);color:var(--okdb-accent);\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20{{\x20previewRunning\x20?\x20\x27Running…\x27\x20:\x20\x27▷\x20Test\x20with\x20sample\x20job\x27\x20}}\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:11px;opacity:.45;line-height:1.5;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20Runs\x20the\x20draft\x20against\x20the\x20sample\x20payload\x20above.\x20Side-effects\x20still\x20write\x20to\x20live\x20data.\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22previewError\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-danger);font-size:11px;padding:8px\x2010px;border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);border-radius:6px;\x22>{{\x20previewError\x20}}</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22previewResult\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;flex-direction:column;gap:8px;padding:10px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:8px;background:#0f1418;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20:style=\x22\x27font-size:12px;font-weight:600;color:\x27+previewStatusColor(previewResult)\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20{{\x20previewOk\x20?\x20\x27✓\x20Function\x20executed\x20successfully\x27\x20:\x20\x27✕\x20Function\x20returned\x20an\x20error\x27\x20}}\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.55;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span>status:\x20{{\x20previewResult.status\x20||\x20\x27ok\x27\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22previewDuration\x20!=\x20null\x22><span>{{\x20previewDuration\x20}}ms</span></if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<pre\x20:style=\x22\x27margin:0;max-height:220px;overflow:auto;padding:10px;border-radius:8px;background:var(--okdb-app-bg-deep);border:1px\x20solid\x20var(--okdb-app-border);font-size:11px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:\x27\x20+\x20(previewOk\x20?\x20\x27var(--okdb-app-fg)\x27\x20:\x20\x27#fca5a5\x27)\x22>{{\x20formatJson(previewOk\x20?\x20(previewResult.result?.value\x20??\x20previewResult)\x20:\x20(previewResult.error\x20??\x20previewResult.result?.value\x20??\x20previewResult))\x20}}</pre>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<if\x20:=\x22previewLogs.length\x20>\x200\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<details>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<summary\x20style=\x22font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;cursor:pointer;user-select:none;\x22>Logs\x20({{\x20previewLogs.length\x20}})</summary>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22margin-top:6px;display:flex;flex-direction:column;gap:4px;max-height:140px;overflow:auto;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<each\x20:of=\x22previewLogs\x22\x20let:item=\x22entry\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22font-size:11px;padding:6px\x208px;border:1px\x20solid\x20var(--okdb-app-border);border-radius:6px;background:var(--okdb-app-bg-deep);display:flex;gap:8px;align-items:flex-start;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22font-size:10px;text-transform:uppercase;opacity:.5;min-width:40px;\x22>{{\x20entry.level\x20||\x20\x27log\x27\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<span\x20style=\x22flex:1;\x22>{{\x20entry.msg\x20||\x20entry.message\x20||\x20formatJson(entry)\x20}}</span>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</each>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</details>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</if>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x0a\x20\x20\x20\x20\x20\x20<if\x20:=\x22createError\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<div\x20style=\x22color:var(--okdb-danger);font-size:12px;padding:6px\x2010px;border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);border-radius:6px;\x22>{{\x20createError\x20}}</div>\x0a\x20\x20\x20\x20\x20\x20</if>\x0a\x0a\x20\x20\x20\x20\x20\x20<div\x20style=\x22display:flex;justify-content:flex-end;gap:8px;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22cancelCreate()\x22\x20style=\x22font-size:12px;padding:5px\x2014px;opacity:.6;\x22>Cancel</button>\x0a\x20\x20\x20\x20\x20\x20\x20\x20<button\x20@click=\x22create()\x22\x20:disabled=\x22!canCreate\x20||\x20creating\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20style=\x22font-size:12px;padding:5px\x2018px;background:color-mix(in\x20srgb,\x20var(--okdb-accent)\x2013%,\x20transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;\x22>\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20{{\x20creating\x20?\x20\x27Creating…\x27\x20:\x20\x27Create\x20Worker\x27\x20}}\x0a\x20\x20\x20\x20\x20\x20\x20\x20</button>\x0a\x20\x20\x20\x20\x20\x20</div>\x0a\x20\x20\x20\x20</div>\x0a\x20\x20</if>\x0a\x0a</div>','var(--okdb-app-fg-muted)','application/json','stats','Failed\x20to\x20create\x20function','Worker\x20restarted','var(--okdb-success)','existingFn','createSuccess','Worker\x20paused','Queue\x20worker\x20for\x20','job','5GaiVqA','engine:started','\x20failed','__preview__','handlerName','jobType','json','#818cf8','pollInterval','data','normal','shared','default','message','existing','fnValidation','previewResult','replace','5927230ENOrCo','state','paused','creating','350849Hzdfzc','new','busy','fnName','process','function','selectedEnv','showCreate','$watch','result','\x22\x20created','3769339NxwGQr','config','error','jobtype','5193VRByhi','filter','isArray','concurrency','Source\x20required','Worker\x20resumed','previewError','toast','open','fnMode','Failed\x20(','resume','engine:stopped','/functions','POST','trim'];_0x47e3=function(){return _0x18f1ea;};return _0x47e3();}import _0x1d191c from'../../../components/ok-js-input.ok.js';import _0x23f689 from'../../../components/ok-json-input.ok.js';import _0x357550 from'../../engines/modals/engine-modal.ok.js';import _0x20bd87 from'../../engines/parts/engine-row-item.ok.js';import{openFunctionRoute as _0x1301dc}from'../../engines/engine-ui-utils.js';function _0x52ef(_0x168852,_0x4a385a){_0x168852=_0x168852-0x1bf;const _0x47e31c=_0x47e3();let _0x52ef67=_0x47e31c[_0x168852];return _0x52ef67;}const s=_0x5b4081(0x1c0),p=[/(^|[^\w$.])require\s*\(/,/(^|[^\w$.])import\b/,/(^|[^\w$.])console\b/,/(^|[^\w$.])process\b/];function d(_0x4466d2){const _0x4b6f83=_0x5b4081,_0x19aa72=(_0x4466d2||'')[_0x4b6f83(0x203)](/\r\n/g,'\x0a')[_0x4b6f83(0x226)]();if(!_0x19aa72)return{'ok':!0x1,'message':_0x4b6f83(0x21b)};if(!/^(async\s+)?\(\s*[A-Za-z_$][\w$]*\s*\)\s*=>/['test'](_0x19aa72))return{'ok':!0x1,'message':_0x4b6f83(0x228)};for(const _0x3bc6b6 of p)if(_0x3bc6b6['test'](_0x19aa72))return{'ok':!0x1,'message':'Forbidden\x20pattern'};try{new Function(_0x4b6f83(0x1cd)+_0x19aa72+');')();}catch(_0x314f56){return{'ok':!0x1,'message':_0x314f56[_0x4b6f83(0x1ff)]};}return{'ok':!0x0,'normalized':_0x19aa72};}function l(_0x1b6854='job'){const _0xe110a7=_0x5b4081;return{'id':_0x1b6854+_0xe110a7(0x1e3),'type':_0x1b6854,'action':_0xe110a7(0x20c),'priority':_0xe110a7(0x1fc),'sample':!0x0,'data':{'note':'Example\x20queue\x20payload'}};}function c(_0xf74275){const _0x204673=_0x5b4081;try{return JSON[_0x204673(0x1d2)](_0xf74275,null,0x2);}catch{return String(_0xf74275??'');}}export default{'tag':'queue-workers','dependencies':[_0x1d191c,_0x23f689,_0x357550,_0x20bd87],'attr':{'jobtype':{}},'context':_0x4716e7=>({'workers':[],'busy':!0x1,'error':null,'_unsubs':[],'showCreate':!0x1,'creating':!0x1,'createError':null,'createSuccess':null,'fnName':'','fnSource':s,'fnValidation':d(s),'fnMode':'new','existingFn':'','functions':[],'concurrency':0x1,'pollInterval':0x64,'advanced':!0x1,'samplePayload':l(_0x5b4081(0x1f1)),'previewRunning':!0x1,'previewResult':null,'previewError':null,get 'jobType'(){return _0x4716e7['$attr']['jobtype']||'';},get 'envName'(){const _0x536aaa=_0x5b4081;return _0xb8fb87[_0x536aaa(0x20e)]||_0x536aaa(0x1fe);},get 'apiBase'(){return _0xb8fb87['apiBase'];},get 'handlerName'(){const _0x56d9d4=_0x5b4081;return _0x56d9d4(0x200)===this[_0x56d9d4(0x220)]?this[_0x56d9d4(0x1ed)]:this['fnName'][_0x56d9d4(0x226)]();},get 'hasBoundJobType'(){const _0x5e5c33=_0x5b4081;return!!this[_0x5e5c33(0x1f7)];},get 'canCreate'(){const _0x4c7636=_0x5b4081;return!(!this[_0x4c7636(0x1f7)]||!this[_0x4c7636(0x1f6)]||'new'===this[_0x4c7636(0x220)]&&!this[_0x4c7636(0x201)]['ok']);},get 'previewOk'(){const _0xfa8ffe=_0x5b4081;return!!this[_0xfa8ffe(0x202)]&&_0xfa8ffe(0x215)!==this[_0xfa8ffe(0x202)][_0xfa8ffe(0x1cc)]&&!this[_0xfa8ffe(0x202)][_0xfa8ffe(0x215)];},get 'previewLogs'(){const _0x39be72=_0x5b4081;return Array[_0x39be72(0x219)](this['previewResult']?.['logs'])?this[_0x39be72(0x202)][_0x39be72(0x1c9)]:[];},get 'previewDuration'(){const _0x157c15=_0x5b4081;return this[_0x157c15(0x202)]?.[_0x157c15(0x1e0)]?.[_0x157c15(0x1c4)]??null;},get 'totalProcessed'(){const _0x11efb2=_0x5b4081;return(this[_0x11efb2(0x22b)]??[])[_0x11efb2(0x1db)]((_0x517ffc,_0x2ed4df)=>_0x517ffc+Number(_0x2ed4df?.[_0x11efb2(0x1e9)]?.[_0x11efb2(0x1d3)]??0x0),0x0);},get 'totalFailed'(){const _0x29edc2=_0x5b4081;return(this[_0x29edc2(0x22b)]??[])[_0x29edc2(0x1db)]((_0x83ea61,_0x57ce5f)=>_0x83ea61+Number(_0x57ce5f?.[_0x29edc2(0x1e9)]?.['failed']??0x0),0x0);},async 'load'(){const _0x3f8068=_0x5b4081;if(!this[_0x3f8068(0x1f7)])return this[_0x3f8068(0x22b)]=[],this['error']=null,void(this['busy']=!0x1);this[_0x3f8068(0x20a)]=!0x0,this['error']=null;try{const _0x403145=await fetch(this[_0x3f8068(0x22c)]+_0x3f8068(0x1df)),_0x37371d=await _0x403145[_0x3f8068(0x1f8)](),_0x5c87bc=_0x37371d?.['result']??_0x37371d?.['data']??[];this['workers']=_0x5c87bc[_0x3f8068(0x218)](_0x3ba5ba=>_0x3ba5ba[_0x3f8068(0x214)]?.['job_type']===this[_0x3f8068(0x1f7)]);}catch(_0x2bca06){this['error']=_0x2bca06?.[_0x3f8068(0x1ff)]||String(_0x2bca06);}finally{this[_0x3f8068(0x20a)]=!0x1;}},async 'loadFunctions'(){const _0x5a9737=_0x5b4081;try{const _0x296d3d=await fetch(this[_0x5a9737(0x22c)]+'/functions'),_0x3deb23=await _0x296d3d[_0x5a9737(0x1f8)]();this[_0x5a9737(0x22d)]=(_0x3deb23?.[_0x5a9737(0x211)]??_0x3deb23?.[_0x5a9737(0x1fb)]??[])['map'](_0x22ceea=>_0x22ceea['name']??_0x22ceea['key']??_0x22ceea)[_0x5a9737(0x218)](Boolean)[_0x5a9737(0x1c8)]();}catch{this[_0x5a9737(0x22d)]=[];}},'openCreate'(){const _0x4f4ba6=_0x5b4081;this[_0x4f4ba6(0x1f7)]&&(this[_0x4f4ba6(0x20f)]=!0x0,this['createError']=null,this[_0x4f4ba6(0x1ee)]=null,this[_0x4f4ba6(0x20b)]='',this[_0x4f4ba6(0x1e2)]=s,this[_0x4f4ba6(0x201)]=d(s),this[_0x4f4ba6(0x220)]='new',this[_0x4f4ba6(0x1ed)]='',this['previewResult']=null,this['previewError']=null,this['concurrency']=0x1,this[_0x4f4ba6(0x1fa)]=0x64,this[_0x4f4ba6(0x1ca)]=l(this[_0x4f4ba6(0x1f7)]),this[_0x4f4ba6(0x1d0)]());},'cancelCreate'(){const _0x500fc1=_0x5b4081;this[_0x500fc1(0x20f)]=!0x1,this[_0x500fc1(0x1dc)]=null;},'pickExistingFn'(){const _0x3a9fc7=_0x5b4081;this[_0x3a9fc7(0x220)]=_0x3a9fc7(0x200),this[_0x3a9fc7(0x1d0)]();},'pickNewFn'(){const _0x4fdcec=_0x5b4081;this['fnMode']=_0x4fdcec(0x209),this[_0x4fdcec(0x1e2)]?.['trim']()||(this[_0x4fdcec(0x1e2)]=s,this['fnValidation']=d(this[_0x4fdcec(0x1e2)]));},'updateSource'(_0x1d4614){const _0x246571=_0x5b4081;this['fnSource']=_0x1d4614??'',this['fnValidation']=d(this[_0x246571(0x1e2)]);},async 'preview'(){const _0x4dc309=_0x5b4081;if(this[_0x4dc309(0x201)]['ok']&&!this[_0x4dc309(0x1d8)]){this['previewRunning']=!0x0,this[_0x4dc309(0x202)]=null,this[_0x4dc309(0x21d)]=null;try{const _0x549108=await fetch(this[_0x4dc309(0x22c)]+_0x4dc309(0x227),{'method':'POST','headers':{'Content-Type':_0x4dc309(0x1e8)},'body':JSON[_0x4dc309(0x1d2)]({'source':this['fnValidation']['normalized'],'payload':this[_0x4dc309(0x1ca)],'jobContext':{'id':_0x4dc309(0x1f5),'type':this[_0x4dc309(0x1f7)],'tries':0x1,'created':Date[_0x4dc309(0x1c5)](),'tags':null,'priority':0x0}})}),_0x21a776=await _0x549108[_0x4dc309(0x1f8)]();this['previewResult']=_0x21a776?.[_0x4dc309(0x211)]??_0x21a776?.[_0x4dc309(0x1fb)]??null;}catch(_0x3993f0){this[_0x4dc309(0x21d)]=_0x3993f0?.[_0x4dc309(0x1ff)]||String(_0x3993f0);}finally{this[_0x4dc309(0x1d8)]=!0x1;}}},async 'create'(){const _0x174339=_0x5b4081;if(this[_0x174339(0x1ce)]&&!this[_0x174339(0x207)]){this[_0x174339(0x207)]=!0x0,this[_0x174339(0x1dc)]=null;try{if('new'===this[_0x174339(0x220)]){const _0x8de1fe=await fetch(this['apiBase']+_0x174339(0x224),{'method':'POST','headers':{'Content-Type':_0x174339(0x1e8)},'body':JSON['stringify']({'name':this['fnName'][_0x174339(0x226)](),'source':this[_0x174339(0x201)]['normalized'],'enabled':!0x0,'metadata':{'description':_0x174339(0x1f0)+this[_0x174339(0x1f7)]}})}),_0x380323=await _0x8de1fe[_0x174339(0x1f8)]();if(!_0x8de1fe['ok'])throw new Error(_0x380323?.[_0x174339(0x215)]?.['message']||_0x174339(0x1ea));}const _0x4093eb=this[_0x174339(0x1f7)]+'-'+this[_0x174339(0x1f6)],_0x3376cf=await fetch(this[_0x174339(0x22c)]+'/engines',{'method':_0x174339(0x225),'headers':{'Content-Type':_0x174339(0x1e8)},'body':JSON[_0x174339(0x1d2)]({'type':'queue-worker','name':_0x4093eb,'config':{'job_type':this[_0x174339(0x1f7)],'handler':{'kind':_0x174339(0x20d),'name':this[_0x174339(0x1f6)]},'concurrency':Number(this[_0x174339(0x21a)])||0x1,'pollInterval':Number(this[_0x174339(0x1fa)])||0x64}})}),_0x35f87b=await _0x3376cf['json']();if(!_0x3376cf['ok'])throw new Error(_0x35f87b?.[_0x174339(0x215)]?.[_0x174339(0x1ff)]||_0x174339(0x221)+_0x3376cf[_0x174339(0x1cc)]+')');this[_0x174339(0x1ee)]=_0x174339(0x1e1)+_0x4093eb+_0x174339(0x212),this['showCreate']=!0x1,await this[_0x174339(0x1dd)]();}catch(_0x3b6b42){this[_0x174339(0x1dc)]=_0x3b6b42?.[_0x174339(0x1ff)]||String(_0x3b6b42);}finally{this[_0x174339(0x207)]=!0x1;}}},async 'doAction'(_0x290de6,_0x43fa9b){const _0x8261c8=_0x5b4081;try{const _0x2e1f01=await fetch(this[_0x8261c8(0x22c)]+_0x8261c8(0x1e5)+encodeURIComponent(_0x290de6['type'])+'/'+encodeURIComponent(_0x290de6['name'])+'/'+_0x43fa9b,{'method':_0x8261c8(0x225)}),_0x3f4cd9=await _0x2e1f01['json']()[_0x8261c8(0x1bf)](()=>({}));if(!_0x2e1f01['ok'])throw new Error(_0x3f4cd9?.[_0x8261c8(0x215)]?.[_0x8261c8(0x1ff)]||_0x43fa9b+'\x20failed');const _0x107789='pause'===_0x43fa9b?_0x8261c8(0x1ef):_0x8261c8(0x222)===_0x43fa9b?_0x8261c8(0x21c):_0x8261c8(0x1d7)===_0x43fa9b?_0x8261c8(0x1eb):_0x8261c8(0x1c7)+_0x43fa9b+'d';_0x4716e7[_0x8261c8(0x1d6)]['shared']?.[_0x8261c8(0x21e)]?.['info']?.(_0x107789),await this[_0x8261c8(0x1dd)]();}catch(_0xc42839){_0x4716e7[_0x8261c8(0x1d6)][_0x8261c8(0x1fd)]?.[_0x8261c8(0x21e)]?.[_0x8261c8(0x215)]?.(_0xc42839?.['message']||_0x43fa9b+_0x8261c8(0x1f4));}},async 'doDelete'(_0x5c66b8){const _0x4e350d=_0x5b4081;if(confirm('Delete\x20worker\x20\x22'+_0x5c66b8[_0x4e350d(0x1d9)]+'\x22?'))try{await fetch(this[_0x4e350d(0x22c)]+_0x4e350d(0x1e5)+encodeURIComponent(_0x5c66b8['type'])+'/'+encodeURIComponent(_0x5c66b8[_0x4e350d(0x1d9)]),{'method':_0x4e350d(0x1c2)}),await this['load']();}catch{}},async 'openWorkerModal'(_0x5f5165){const _0x34a83a=_0x5b4081;if(!_0x5f5165)return;const _0x5155f2=this;await _0x4716e7[_0x34a83a(0x1d6)]['shared'][_0x34a83a(0x1e4)][_0x34a83a(0x21f)]({'owner':_0x4716e7,'title':'Edit\x20Worker\x20·\x20'+_0x5f5165[_0x34a83a(0x1d9)],'canDrag':!0x0,'content':'<engine-modal\x20:modal=\x22modal\x22\x20:engine=\x22engine\x22\x20:env=\x22envName\x22\x20:on-done=\x22onDone\x22\x20/>','context':_0x4f93bd=>({'modal':_0x4f93bd,'engine':_0x5f5165,'envName':_0x5155f2[_0x34a83a(0x1de)],'onDone':async()=>{const _0x102a23=_0x34a83a;await _0x5155f2[_0x102a23(0x1dd)]();}})});},'openWorkerFunction'(_0x55c39f,_0x17e648){const _0x23402a=_0x5b4081;_0x17e648?.['stopPropagation']?.();const _0x5736b6=_0x55c39f?.[_0x23402a(0x214)]?.[_0x23402a(0x1c1)]?.[_0x23402a(0x1d9)];_0x5736b6&&_0x1301dc(_0x4716e7,_0x5736b6);},'stateColor'(_0x43b356){const _0x1c65cb=_0x5b4081,_0x33ffc0=_0x43b356?.['stats']?.[_0x1c65cb(0x205)]??_0x43b356?.[_0x1c65cb(0x1cc)];return'online'===_0x33ffc0?'var(--okdb-success)':_0x1c65cb(0x215)===_0x33ffc0?_0x1c65cb(0x1cb):_0x1c65cb(0x206)===_0x33ffc0?_0x1c65cb(0x1f9):_0x1c65cb(0x1d1)===_0x33ffc0?_0x1c65cb(0x1c6):_0x1c65cb(0x1e7);},'stateLabel':_0x5bed96=>_0x5bed96?.['stats']?.['state']??_0x5bed96?.['status']??'—','previewStatusColor':_0x2afea9=>_0x2afea9?_0x5b4081(0x215)===_0x2afea9[_0x5b4081(0x1cc)]||_0x2afea9[_0x5b4081(0x215)]?_0x5b4081(0x1cb):_0x5b4081(0x1ec):'var(--okdb-app-fg-muted)','formatJson':c}),async 'mount'(_0x3b3e9d){const _0x3488e2=_0x5b4081;await this[_0x3488e2(0x1dd)]();const _0x364221=()=>this[_0x3488e2(0x1dd)]();this[_0x3488e2(0x1d5)]=[_0x5268d5['on'](_0x3488e2(0x1f3),_0x364221),_0x5268d5['on'](_0x3488e2(0x223),_0x364221),_0x5268d5['on']('engine:error',_0x364221)],_0x3b3e9d[_0x3488e2(0x210)](()=>_0x3b3e9d[_0x3488e2(0x229)][_0x3488e2(0x216)],()=>{const _0xd57362=_0x3488e2;this['showCreate']=!0x1,this[_0xd57362(0x202)]=null,this[_0xd57362(0x21d)]=null,this[_0xd57362(0x1ca)]=l(_0x3b3e9d[_0xd57362(0x229)][_0xd57362(0x216)]||_0xd57362(0x1f1)),this['load']();}),_0x3b3e9d[_0x3488e2(0x210)](()=>_0xb8fb87[_0x3488e2(0x20e)],()=>{const _0x4beaa2=_0x3488e2;this[_0x4beaa2(0x20f)]=!0x1,this[_0x4beaa2(0x1dd)]();});},'unmount'(){const _0x595ba4=_0x5b4081;for(const _0x44fb4 of this['_unsubs'])_0x44fb4();this[_0x595ba4(0x1d5)]=[];},'template':_0x5b4081(0x1e6)};
1
+ import e from"../../../stores/events-store.js";import n from"../../../stores/okdb-store.js";import t from"../../../components/ok-js-input.ok.js";import i from"../../../components/ok-json-input.ok.js";import r from"../../engines/modals/engine-modal.ok.js";import o from"../../engines/parts/engine-row-item.ok.js";import{openFunctionRoute as a}from"../../engines/engine-ui-utils.js";const s="async (ctx) => {\n // ctx.payload — the raw job payload (whatever was enqueued)\n // ctx.job — queue job context\n // ctx.job.id, ctx.job.type, ctx.job.tries, ctx.job.tags, ...\n // ctx.job.heartbeat() — keep the job alive\n // ctx.job.markProgress(msg) — report progress\n // ctx.env — env facade: put, get, query, txn, ...\n // ctx.log — structured logger: ctx.log.info(msg, data)\n\n return { ok: true };\n}",p=[/(^|[^\w$.])require\s*\(/,/(^|[^\w$.])import\b/,/(^|[^\w$.])console\b/,/(^|[^\w$.])process\b/];function d(e){const n=(e||"").replace(/\r\n/g,"\n").trim();if(!n)return{ok:!1,message:"Source required"};if(!/^(async\s+)?\(\s*[A-Za-z_$][\w$]*\s*\)\s*=>/.test(n))return{ok:!1,message:"Must be (ctx) => { … }"};for(const e of p)if(e.test(n))return{ok:!1,message:"Forbidden pattern"};try{new Function(`return (${n});`)()}catch(e){return{ok:!1,message:e.message}}return{ok:!0,normalized:n}}function l(e="job"){return{id:`${e}-preview-1`,type:e,action:"process",priority:"normal",sample:!0,data:{note:"Example queue payload"}}}function c(e){try{return JSON.stringify(e,null,2)}catch{return String(e??"")}}export default{tag:"queue-workers",dependencies:[t,i,r,o],attr:{jobtype:{}},context:e=>({workers:[],busy:!1,error:null,_unsubs:[],showCreate:!1,creating:!1,createError:null,createSuccess:null,fnName:"",fnSource:s,fnValidation:d(s),fnMode:"new",existingFn:"",functions:[],concurrency:1,pollInterval:100,advanced:!1,samplePayload:l("job"),previewRunning:!1,previewResult:null,previewError:null,get jobType(){return e.$attr.jobtype||""},get envName(){return n.selectedEnv||"default"},get apiBase(){return n.apiBase},get handlerName(){return"existing"===this.fnMode?this.existingFn:this.fnName.trim()},get hasBoundJobType(){return!!this.jobType},get canCreate(){return!(!this.jobType||!this.handlerName||"new"===this.fnMode&&!this.fnValidation.ok)},get previewOk(){return!!this.previewResult&&"error"!==this.previewResult.status&&!this.previewResult.error},get previewLogs(){return Array.isArray(this.previewResult?.logs)?this.previewResult.logs:[]},get previewDuration(){return this.previewResult?.execution?.durationMs??null},get totalProcessed(){return(this.workers??[]).reduce((e,n)=>e+Number(n?.stats?.processed??0),0)},get totalFailed(){return(this.workers??[]).reduce((e,n)=>e+Number(n?.stats?.failed??0),0)},async load(){if(!this.jobType)return this.workers=[],this.error=null,void(this.busy=!1);this.busy=!0,this.error=null;try{const e=await fetch(`${this.apiBase}/engines?type=queue-worker`),n=await e.json(),t=n?.result??n?.data??[];this.workers=t.filter(e=>e.config?.job_type===this.jobType)}catch(e){this.error=e?.message||String(e)}finally{this.busy=!1}},async loadFunctions(){try{const e=await fetch(`${this.apiBase}/functions`),n=await e.json();this.functions=(n?.result??n?.data??[]).map(e=>e.name??e.key??e).filter(Boolean).sort()}catch{this.functions=[]}},openCreate(){this.jobType&&(this.showCreate=!0,this.createError=null,this.createSuccess=null,this.fnName="",this.fnSource=s,this.fnValidation=d(s),this.fnMode="new",this.existingFn="",this.previewResult=null,this.previewError=null,this.concurrency=1,this.pollInterval=100,this.samplePayload=l(this.jobType),this.loadFunctions())},cancelCreate(){this.showCreate=!1,this.createError=null},pickExistingFn(){this.fnMode="existing",this.loadFunctions()},pickNewFn(){this.fnMode="new",this.fnSource?.trim()||(this.fnSource=s,this.fnValidation=d(this.fnSource))},updateSource(e){this.fnSource=e??"",this.fnValidation=d(this.fnSource)},async preview(){if(this.fnValidation.ok&&!this.previewRunning){this.previewRunning=!0,this.previewResult=null,this.previewError=null;try{const e=await fetch(`${this.apiBase}/functions/preview`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source:this.fnValidation.normalized,payload:this.samplePayload,jobContext:{id:"__preview__",type:this.jobType,tries:1,created:Date.now(),tags:null,priority:0}})}),n=await e.json();this.previewResult=n?.result??n?.data??null}catch(e){this.previewError=e?.message||String(e)}finally{this.previewRunning=!1}}},async create(){if(this.canCreate&&!this.creating){this.creating=!0,this.createError=null;try{if("new"===this.fnMode){const e=await fetch(`${this.apiBase}/functions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:this.fnName.trim(),source:this.fnValidation.normalized,enabled:!0,metadata:{description:`Queue worker for ${this.jobType}`}})}),n=await e.json();if(!e.ok)throw new Error(n?.error?.message||"Failed to create function")}const e=`${this.jobType}-${this.handlerName}`,n=await fetch(`${this.apiBase}/engines`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"queue-worker",name:e,config:{job_type:this.jobType,handler:{kind:"function",name:this.handlerName},concurrency:Number(this.concurrency)||1,pollInterval:Number(this.pollInterval)||100}})}),t=await n.json();if(!n.ok)throw new Error(t?.error?.message||`Failed (${n.status})`);this.createSuccess=`Worker "${e}" created`,this.showCreate=!1,await this.load()}catch(e){this.createError=e?.message||String(e)}finally{this.creating=!1}}},async doAction(n,t){try{const i=await fetch(`${this.apiBase}/engines/${encodeURIComponent(n.type)}/${encodeURIComponent(n.name)}/${t}`,{method:"POST"}),r=await i.json().catch(()=>({}));if(!i.ok)throw new Error(r?.error?.message||`${t} failed`);const o="pause"===t?"Worker paused":"resume"===t?"Worker resumed":"restart"===t?"Worker restarted":`Worker ${t}d`;e.$ok.shared?.toast?.info?.(o),await this.load()}catch(n){e.$ok.shared?.toast?.error?.(n?.message||`${t} failed`)}},async doDelete(e){if(confirm(`Delete worker "${e.name}"?`))try{await fetch(`${this.apiBase}/engines/${encodeURIComponent(e.type)}/${encodeURIComponent(e.name)}`,{method:"DELETE"}),await this.load()}catch{}},async openWorkerModal(n){if(!n)return;const t=this;await e.$ok.shared.modal.open({owner:e,title:`Edit Worker · ${n.name}`,canDrag:!0,content:'<engine-modal :modal="modal" :engine="engine" :env="envName" :on-done="onDone" />',context:e=>({modal:e,engine:n,envName:t.envName,onDone:async()=>{await t.load()}})})},openWorkerFunction(n,t){t?.stopPropagation?.();const i=n?.config?.handler?.name;i&&a(e,i)},stateColor(e){const n=e?.stats?.state??e?.status;return"online"===n?"var(--okdb-success)":"error"===n?"var(--okdb-danger)":"paused"===n?"#818cf8":"building"===n?"#facc15":"var(--okdb-app-fg-muted)"},stateLabel:e=>e?.stats?.state??e?.status??"—",previewStatusColor:e=>e?"error"===e.status||e.error?"var(--okdb-danger)":"var(--okdb-success)":"var(--okdb-app-fg-muted)",formatJson:c}),async mount(t){await this.load();const i=()=>this.load();this._unsubs=[e.on("engine:started",i),e.on("engine:stopped",i),e.on("engine:error",i)],t.$watch(()=>t.$attr.jobtype,()=>{this.showCreate=!1,this.previewResult=null,this.previewError=null,this.samplePayload=l(t.$attr.jobtype||"job"),this.load()}),t.$watch(()=>n.selectedEnv,()=>{this.showCreate=!1,this.load()})},unmount(){for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex;flex-direction:column;gap:14px;flex:1;overflow:auto;padding-right:4px;">\n\n \x3c!-- Header --\x3e\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div style="font-weight:600;font-size:15px;">\n Queue Workers\n </div>\n <if :="jobType">\n <span style="font-size:11px;padding:2px 8px;border-radius:999px;background:color-mix(in srgb, var(--okdb-accent) 9%, transparent);border:1px solid color-mix(in srgb, var(--okdb-accent) 27%, transparent);color:var(--okdb-accent);">\n bound to {{ jobType }}\n </span>\n </if>\n <if :="workers.length > 0">\n <span style="font-size:11px;padding:2px 8px;border-radius:999px;background:color-mix(in srgb, var(--okdb-accent) 9%, transparent);border:1px solid color-mix(in srgb, var(--okdb-accent) 27%, transparent);color:var(--okdb-accent);">\n {{ workers.length }} worker{{ workers.length !== 1 ? \'s\' : \'\' }}\n </span>\n </if>\n <div style="flex:1;"></div>\n <button @click="load()" :disabled="busy" style="font-size:11px;padding:3px 8px;opacity:.6;">↻</button>\n <if :="!showCreate && hasBoundJobType">\n <button @click="openCreate()"\n style="font-size:12px;padding:5px 14px;background:color-mix(in srgb, var(--okdb-accent) 13%, transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;">\n + Add Worker\n </button>\n </if>\n </div>\n\n <if :="jobType">\n <div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;padding:12px 14px;border:1px solid var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);">\n <div style="display:flex;flex-direction:column;gap:4px;min-width:220px;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Scope</div>\n <div style="font-size:14px;font-weight:600;">{{ jobType }}</div>\n <div style="font-size:12px;opacity:.55;line-height:1.6;">\n Workers here are explicitly tied to this queue job type. They are not general-purpose consumers.\n </div>\n </div>\n <div style="display:flex;gap:18px;flex-wrap:wrap;font-size:11px;opacity:.65;">\n <span>workers: <b style="color:var(--okdb-app-fg-strong);opacity:1;">{{ workers.length }}</b></span>\n <span>processed: <b style="color:var(--okdb-success);opacity:1;">{{ totalProcessed }}</b></span>\n <if :="totalFailed > 0"><span>failed: <b style="color:var(--okdb-danger);opacity:1;">{{ totalFailed }}</b></span></if>\n </div>\n </div>\n </if>\n\n <if :="createSuccess && !showCreate">\n <div style="color:var(--okdb-success);font-size:12px;padding:6px 10px;border:1px solid color-mix(in srgb, var(--okdb-success) 20%, transparent);border-radius:6px;">\n ✓ {{ createSuccess }}\n </div>\n </if>\n <if :="error">\n <div style="color:var(--okdb-danger);font-size:12px;padding:6px 10px;border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);border-radius:6px;">{{ error }}</div>\n </if>\n\n <if :="!hasBoundJobType && !showCreate">\n <div style="padding:32px 24px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:10px;display:flex;flex-direction:column;gap:12px;align-items:center;text-align:center;">\n <div style="font-size:34px;opacity:.18;">⇶</div>\n <div style="font-size:14px;font-weight:600;opacity:.72;">Select a queue job type</div>\n <div style="font-size:12px;opacity:.48;line-height:1.8;max-width:430px;">\n Queue workers are managed per job type. Pick a type from the sidebar to view its workers,\n create a new one, and test it against sample job payloads.\n </div>\n </div>\n </if>\n\n \x3c!-- Worker list --\x3e\n <if :="!showCreate">\n <if :="hasBoundJobType && !busy && workers.length === 0">\n <div style="padding:28px 20px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:10px;text-align:center;display:flex;flex-direction:column;gap:10px;align-items:center;">\n <div style="font-size:32px;opacity:.2;">⚙</div>\n <div style="font-size:14px;font-weight:600;opacity:.7;">No workers yet for "{{ jobType }}"</div>\n <div style="font-size:12px;opacity:.45;max-width:420px;line-height:1.7;">\n Create a worker bound to this queue job type. The worker will consume only <code>{{ jobType }}</code>\n jobs and run a specific function handler.\n </div>\n <button @click="openCreate()"\n style="margin-top:4px;font-size:12px;padding:6px 18px;background:color-mix(in srgb, var(--okdb-accent) 13%, transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;">\n + Add Worker\n </button>\n </div>\n </if>\n\n <each :of="workers" let:item="w">\n <div style="background:var(--okdb-app-surface);border:1px solid var(--okdb-app-border);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;">\n <div style="display:flex;align-items:center;gap:8px;min-width:0;">\n <engine-row-item\n :engine="w"\n label="Worker"\n label-color="var(--okdb-accent)"\n :subtitle="stateLabel(w)"\n @select="openWorkerModal($event.detail)"\n />\n <button @click="doAction(w,\'pause\')" title="Pause" style="font-size:11px;padding:2px 7px;color:var(--okdb-warning);border-color:color-mix(in srgb, var(--okdb-warning) 33%, transparent);">⏸</button>\n <button @click="doAction(w,\'resume\')" title="Resume" style="font-size:11px;padding:2px 7px;color:var(--okdb-success);border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">▶</button>\n <button @click="doAction(w,\'restart\')" title="Restart" style="font-size:11px;padding:2px 7px;opacity:.6;">↺</button>\n <button @click="doDelete(w)" title="Delete" style="font-size:11px;padding:2px 7px;color:var(--okdb-danger);border-color:color-mix(in srgb, var(--okdb-danger) 20%, transparent);">🗑</button>\n </div>\n <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px 14px;font-size:11px;">\n <div><div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Job type</div><div style="margin-top:3px;color:var(--okdb-app-fg-strong);">{{ w.config?.job_type || \'—\' }}</div></div>\n <div>\n <div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Handler</div>\n <div style="margin-top:3px;">\n <if :="w.config?.handler?.name">\n <button @click="openWorkerFunction(w, $event)" title="Open this handler in the Functions section." style="background:none;border:none;padding:0;color:var(--okdb-accent);font-size:11px;text-decoration:underline;text-underline-offset:2px;cursor:pointer;">ƒ {{ w.config?.handler?.name }} ↗</button>\n </if>\n <if :="!w.config?.handler?.name">\n <span style="color:var(--okdb-app-fg-strong);">—</span>\n </if>\n </div>\n </div>\n <div><div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Concurrency</div><div style="margin-top:3px;color:var(--okdb-app-fg-strong);">{{ w.config?.concurrency ?? 1 }}</div></div>\n <div><div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Poll interval</div><div style="margin-top:3px;color:var(--okdb-app-fg-strong);">{{ w.config?.pollInterval ?? 100 }} ms</div></div>\n <if :="w.stats">\n <div><div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Processed</div><div style="margin-top:3px;color:var(--okdb-success);">{{ w.stats.processed ?? 0 }}</div></div>\n <div><div style="opacity:.4;text-transform:uppercase;letter-spacing:.05em;font-size:10px;">Failed</div><div :style="\'margin-top:3px;color:\' + ((w.stats.failed ?? 0) > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ w.stats.failed ?? 0 }}</div></div>\n </if>\n </div>\n <if :="w.stats?.error">\n <div style="color:var(--okdb-danger);font-size:11px;padding:4px 8px;border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);border-radius:4px;font-family:monospace;">{{ w.stats.error }}</div>\n </if>\n </div>\n </each>\n </if>\n\n \x3c!-- Create panel --\x3e\n <if :="showCreate">\n <div style="background:var(--okdb-app-surface);border:1px solid var(--okdb-app-border);border-radius:12px;padding:18px;display:flex;flex-direction:column;gap:16px;">\n <div style="font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px;">\n <span>New Queue Worker</span>\n <span style="font-size:12px;color:var(--okdb-accent);opacity:.8;">bound to {{ jobType }}</span>\n <div style="flex:1;"></div>\n <button @click="cancelCreate()" style="font-size:11px;padding:2px 8px;opacity:.6;">✕ Cancel</button>\n </div>\n\n <div style="display:grid;grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);gap:16px;align-items:start;">\n <div style="display:flex;flex-direction:column;gap:14px;min-width:0;">\n <div style="padding:14px;border:1px solid var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Binding</div>\n <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 14px;">\n <div><div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;">Environment</div><div style="margin-top:3px;color:var(--okdb-app-fg-strong);">{{ envName }}</div></div>\n <div><div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;">Job type</div><div style="margin-top:3px;color:var(--okdb-accent);font-weight:600;">{{ jobType }}</div></div>\n </div>\n <div style="font-size:11px;opacity:.5;line-height:1.6;">\n This worker will consume only <code>{{ jobType }}</code> jobs. The job type is fixed by the queue sidebar selection.\n </div>\n </div>\n\n <div style="padding:14px;border:1px solid var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Handler</div>\n <div style="display:flex;gap:6px;">\n <button @click="pickNewFn()"\n :style="fnMode===\'new\' ? \'font-size:11px;padding:2px 10px;border-color:var(--okdb-accent);color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);\' : \'font-size:11px;padding:2px 10px;opacity:.55;\'">\n Write new\n </button>\n <button @click="pickExistingFn()"\n :style="fnMode===\'existing\' ? \'font-size:11px;padding:2px 10px;border-color:var(--okdb-accent);color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);\' : \'font-size:11px;padding:2px 10px;opacity:.55;\'">\n Pick existing\n </button>\n </div>\n </div>\n\n <if :="fnMode === \'new\'">\n <div style="display:grid;grid-template-columns:120px 1fr;gap:10px;align-items:center;font-size:13px;">\n <span style="opacity:.6;">Function name</span>\n <input :value="fnName" @input="fnName = $event.target.value"\n placeholder="e.g. handleOrderJob"\n style="background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:6px;padding:6px 8px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;" />\n </div>\n\n <div style="display:flex;flex-direction:column;gap:6px;min-width:0;min-height:0;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Implementation</div>\n <div style="display:flex;flex-direction:column;min-height:420px;border:1px solid var(--okdb-app-border);border-radius:10px;overflow:hidden;background:#10171e;">\n <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border-bottom:1px solid var(--okdb-app-border);">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Worker handler</div>\n <div style="font-size:10px;opacity:.4;font-family:monospace;">async (ctx) =&gt; { }</div>\n </div>\n <div style="flex:1;min-height:0;background:#0d1318;display:flex;">\n <ok-js-input :value="fnSource" @edit="updateSource($event.detail)" style="display:block;" />\n </div>\n </div>\n <if :="fnValidation.ok">\n <div style="color:#86efac;font-size:11px;">✓ Valid function source</div>\n </if>\n <if :="!fnValidation.ok && fnSource.trim()">\n <div style="color:var(--okdb-danger);font-size:11px;">{{ fnValidation.message }}</div>\n </if>\n </div>\n </if>\n\n <if :="fnMode === \'existing\'">\n <div style="display:grid;grid-template-columns:120px 1fr;gap:10px;align-items:center;font-size:13px;">\n <span style="opacity:.6;">Function</span>\n <select :value="existingFn" @change="existingFn = $event.target.value"\n style="background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:6px;padding:6px 8px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;">\n <option value="">— select function —</option>\n <each :of="functions" let:item="fn"><option :value="fn">{{ fn }}</option></each>\n </select>\n </div>\n <div style="font-size:11px;opacity:.5;line-height:1.6;">\n Pick an existing function already stored in this environment. Inline test preview is available when writing a new function in this panel.\n </div>\n </if>\n </div>\n </div>\n\n <div style="display:flex;flex-direction:column;gap:14px;min-width:0;">\n <div style="padding:14px;border:1px solid var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Runtime</div>\n <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">\n <div>\n <div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;">Concurrency</div>\n <input type="number" min="1" max="64" :value="concurrency" @input="concurrency = $event.target.value"\n style="margin-top:4px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:6px;padding:6px 8px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;" />\n </div>\n <div>\n <div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;">Poll interval</div>\n <input type="number" min="10" max="10000" :value="pollInterval" @input="pollInterval = $event.target.value"\n style="margin-top:4px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:6px;padding:6px 8px;color:inherit;font-size:13px;width:100%;box-sizing:border-box;" />\n <div style="font-size:10px;opacity:.4;margin-top:4px;">milliseconds</div>\n </div>\n </div>\n </div>\n\n <div style="padding:14px;border:1px solid var(--okdb-app-border);border-radius:10px;background:var(--okdb-app-bg-deep);display:flex;flex-direction:column;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;">Testing</div>\n <if :="fnMode !== \'new\'"><span style="font-size:10px;color:var(--okdb-warning);opacity:.8;">inline preview works for new functions</span></if>\n </div>\n\n <div>\n <div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em;">Sample job payload</div>\n <div style="margin-top:6px;border:1px solid var(--okdb-app-border);border-radius:8px;overflow:hidden;background:var(--okdb-app-bg-deep);min-height:220px;display:flex;flex-direction:column;min-width:0;">\n <ok-json-input mode="textbox" :value="samplePayload" @edit="samplePayload = $event.detail" style="height:100%;display:flex;min-height:220px;" />\n </div>\n </div>\n\n <if :="fnMode === \'new\'">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <button @click="preview()" :disabled="!fnValidation.ok || previewRunning"\n style="font-size:12px;padding:5px 14px;background:color-mix(in srgb, var(--okdb-accent) 9%, transparent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);color:var(--okdb-accent);">\n {{ previewRunning ? \'Running…\' : \'▷ Test with sample job\' }}\n </button>\n <span style="font-size:11px;opacity:.45;line-height:1.5;">\n Runs the draft against the sample payload above. Side-effects still write to live data.\n </span>\n </div>\n </if>\n\n <if :="previewError">\n <div style="color:var(--okdb-danger);font-size:11px;padding:8px 10px;border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);border-radius:6px;">{{ previewError }}</div>\n </if>\n\n <if :="previewResult">\n <div style="display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--okdb-app-border);border-radius:8px;background:#0f1418;">\n <div :style="\'font-size:12px;font-weight:600;color:\'+previewStatusColor(previewResult)">\n {{ previewOk ? \'✓ Function executed successfully\' : \'✕ Function returned an error\' }}\n </div>\n <div style="display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.55;">\n <span>status: {{ previewResult.status || \'ok\' }}</span>\n <if :="previewDuration != null"><span>{{ previewDuration }}ms</span></if>\n </div>\n <pre :style="\'margin:0;max-height:220px;overflow:auto;padding:10px;border-radius:8px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);font-size:11px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:\' + (previewOk ? \'var(--okdb-app-fg)\' : \'#fca5a5\')">{{ formatJson(previewOk ? (previewResult.result?.value ?? previewResult) : (previewResult.error ?? previewResult.result?.value ?? previewResult)) }}</pre>\n <if :="previewLogs.length > 0">\n <details>\n <summary style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.45;cursor:pointer;user-select:none;">Logs ({{ previewLogs.length }})</summary>\n <div style="margin-top:6px;display:flex;flex-direction:column;gap:4px;max-height:140px;overflow:auto;">\n <each :of="previewLogs" let:item="entry">\n <div style="font-size:11px;padding:6px 8px;border:1px solid var(--okdb-app-border);border-radius:6px;background:var(--okdb-app-bg-deep);display:flex;gap:8px;align-items:flex-start;">\n <span style="font-size:10px;text-transform:uppercase;opacity:.5;min-width:40px;">{{ entry.level || \'log\' }}</span>\n <span style="flex:1;">{{ entry.msg || entry.message || formatJson(entry) }}</span>\n </div>\n </each>\n </div>\n </details>\n </if>\n </div>\n </if>\n </div>\n </div>\n </div>\n\n <if :="createError">\n <div style="color:var(--okdb-danger);font-size:12px;padding:6px 10px;border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);border-radius:6px;">{{ createError }}</div>\n </if>\n\n <div style="display:flex;justify-content:flex-end;gap:8px;">\n <button @click="cancelCreate()" style="font-size:12px;padding:5px 14px;opacity:.6;">Cancel</button>\n <button @click="create()" :disabled="!canCreate || creating"\n style="font-size:12px;padding:5px 18px;background:color-mix(in srgb, var(--okdb-accent) 13%, transparent);border-color:var(--okdb-accent);color:var(--okdb-accent);font-weight:600;">\n {{ creating ? \'Creating…\' : \'Create Worker\' }}\n </button>\n </div>\n </div>\n </if>\n\n</div>'};
@@ -1,4 +1,4 @@
1
- <script type="module">import syncTopology from"./parts/sync-topology.ok.js";import syncRetention from"./parts/sync-retention.ok.js";export default{tag:"sync-index",unwrap:!0,dependencies:[syncTopology,syncRetention],context:()=>({loading:!1,error:null,topology:null,availableEnvs:[],selectedNodeId:null,showConnect:!1,joinAddress:"",dataLink:!0,joinToken:"",joinUsername:"",joinPassword:"",showPasswordAuth:!1,joining:!1,joinError:null,selfNode:null,selfClock:null,selfClocks:{},selfTagsInput:"",savingSelf:!1,saveSelfError:null,async loadSelf(){try{const s=await fetch(`${__ENV__.API}/sync/info`),o=await s.json();s.ok&&(this.selfNode=o.data?.node??null,this.selfClock=o.data?.clock??null,this.selfClocks=o.data?.clocks??{},this.selfTagsInput=(this.selfNode?.tags??[]).join(", "))}catch{}},async loadTopology(){this.loading=!0,this.error=null;try{const s=await fetch(`${__ENV__.API}/sync/topology`),o=await s.json();if(!s.ok)throw new Error(o?.error?.message||"Failed to load topology");const t=o.data??o;this.topology=t.topology??t,this.availableEnvs=t.availableEnvs??[]}catch(s){this.error=s?.message||String(s)}finally{this.loading=!1}},async loadIdentity(){},openConnect(){this.joinAddress="",this.joinToken="",this.joinUsername="",this.joinPassword="",this.joinError=null,this.showPasswordAuth=!1,this.showConnect=!0},closeConnect(){this.showConnect=!1,this.joinError=null},async joinCluster(){if(this.joinAddress&&!this.joining){this.joining=!0,this.joinError=null;try{const s={address:this.joinAddress,data_link:this.dataLink};!this.showPasswordAuth&&this.joinToken.trim()?s.token=this.joinToken.trim():this.showPasswordAuth&&this.joinUsername.trim()&&(s.username=this.joinUsername.trim(),s.password=this.joinPassword);const o=await fetch(`${__ENV__.API}/sync/connect`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),t=await o.json();if(!o.ok)throw new Error(t?.error?.message||"Connect failed");this.closeConnect(),await this.loadTopology()}catch(s){this.joinError=s.message}finally{this.joining=!1}}},get selfClockEntries(){return Object.entries(this.selfClocks).filter(([s])=>"~system"!==s)},get selfTagList(){return this.parseTags(this.selfTagsInput)},parseTags:s=>String(s??"").split(",").map(s=>s.trim()).filter(Boolean).filter((s,o,t)=>t.indexOf(s)===o),selectNode(s){this.selectedNodeId=s||null},async saveSelfTags(){if(!this.savingSelf){this.savingSelf=!0,this.saveSelfError=null;try{const s=await fetch(`${__ENV__.API}/sync/self`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({tags:this.selfTagList})}),o=await s.json();if(!s.ok)throw new Error(o?.error?.message||"Failed to save node tags");this.selfNode=o.data?.node??o.result?.node??this.selfNode,this.selfTagsInput=(this.selfNode?.tags??[]).join(", "),await this.loadTopology()}catch(s){this.saveSelfError=s?.message||String(s)}finally{this.savingSelf=!1}}},async copyText(s){try{await navigator.clipboard.writeText(String(s??""))}catch(s){}}}),async mount(){await this.loadSelf(),await this.loadTopology()}}</script><template><component import="../../layouts/_default.ok.html"><template name="sidebar"><if :="selfNode"><div style="background:var(--okdb-app-surface);border:1px solid var(--okdb-app-border);border-radius:10px;padding:10px 12px;font-size:12px;display:flex;flex-direction:column;gap:6px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.5;margin-bottom:2px">This Node</div><div style="display:flex;align-items:center;gap:6px"><span style="width:7px;height:7px;border-radius:50%;background:var(--okdb-success);flex-shrink:0"></span> <span style="font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{selfNode.name || selfNode.id?.slice(0,8)}}</span></div><div style="display:grid;grid-template-columns:52px 1fr;gap:3px 8px;opacity:.8"><span style="opacity:.6">ID</span><div style="display:flex;align-items:center;gap:4px"><span style="font-family:monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{selfNode.id?.slice(0, 12)}}… </span><span @click="copyText(selfNode.id)" style="cursor:pointer;opacity:.5;font-size:10px">⎘</span></div><if :="selfNode.address"><span style="opacity:.6">Addr</span> <span style="font-family:monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="selfNode.address">{{selfNode.address}}</span></if></div><div style="border-top:1px solid var(--okdb-app-border);padding-top:8px;margin-top:4px;display:flex;flex-direction:column;gap:8px"><div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em">Node Tags</div><input :value="selfTagsInput" @input="selfTagsInput = $event.target.value" @keydown.enter="saveSelfTags()" placeholder="gpu, region:eu, fast-storage" style="font-family:monospace;font-size:12px"/><div style="display:flex;flex-wrap:wrap;gap:6px;min-height:20px"><if :="selfTagList.length === 0"><span style="font-size:11px;opacity:.45">No tags</span></if><for :="selfTagList" let:tag><span style="font-size:10px;padding:2px 8px;border-radius:999px;border:1px solid var(--okdb-accent-soft-border);color:var(--okdb-accent);background:var(--okdb-accent-soft-bg)">{{tag}}</span></for></div><if :="saveSelfError"><div style="font-size:11px;color:var(--okdb-danger)">{{saveSelfError}}</div></if><div style="display:flex;justify-content:flex-end"><button @click="saveSelfTags()" :disabled="savingSelf" style="font-size:11px;padding:4px 10px;background:var(--okdb-sky-soft-bg);border-color:var(--okdb-sky-soft-border);color:var(--okdb-sky)">{{ savingSelf ? 'Saving…' : 'Save Tags' }}</button></div></div><if :="selfClockEntries.length"><div style="border-top:1px solid var(--okdb-app-border);padding-top:6px;margin-top:2px"><div style="font-size:10px;opacity:.4;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Clocks</div><div style="display:grid;grid-template-columns:64px 1fr;gap:2px 8px;font-size:11px"><for :="selfClockEntries" let:entry><span style="opacity:.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{entry[0]}}</span> <span style="font-family:monospace;opacity:.8">{{entry[1]}}</span></for></div></div></if></div></if></template><template name="default"><div style="display:flex;flex-direction:column;gap:20px;height:100%"><div style="display:flex;justify-content:space-between;align-items:center"><div style="font-weight:600;font-size:18px">Sync Cluster</div><div style="display:flex;gap:8px"><button @click="loadTopology()" :disabled="loading">Refresh</button> <button @click="openConnect()" style="background:var(--okdb-sky-soft-bg);border-color:var(--okdb-sky-soft-border);color:var(--okdb-sky)">+ Connect Node</button></div></div><if :="error"><div style="color:var(--okdb-danger);font-size:13px">{{error}}</div></if><sync-topology :topology="topology" :available-envs="availableEnvs" :self-id="selfNode?.id" :selected-node-id="selectedNodeId" @select="selectNode($event.detail)"/><div style="border-top:1px solid var(--okdb-app-border);padding-top:20px"><sync-retention/></div></div><if :="showConnect"><div @click.self="closeConnect()" style="position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center"><div style="background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:14px;padding:24px;width:440px;max-width:calc(100vw - 32px);display:flex;flex-direction:column;gap:16px;box-shadow:0 24px 64px rgba(0,0,0,.6)"><div style="display:flex;justify-content:space-between;align-items:center"><div style="font-weight:700;font-size:16px">Connect to Node</div><span @click="closeConnect()" style="cursor:pointer;opacity:.5;font-size:18px;line-height:1;padding:2px 6px">✕</span></div><div style="display:flex;flex-direction:column;gap:6px"><label style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.06em">Remote address</label> <input :value="joinAddress" @input="joinAddress = $event.target.value" @keydown.enter="joinCluster()" placeholder="http://host:8484" style="font-family:monospace" autofocus/></div><div style="display:flex;flex-direction:column;gap:8px"><div style="display:flex;align-items:center;justify-content:space-between"><label style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.06em">Credential</label><div style="display:flex;border:1px solid var(--okdb-app-border);border-radius:6px;overflow:hidden;font-size:11px"><button @click="showPasswordAuth = false" :style="!showPasswordAuth
1
+ <script type="module">import s from"./parts/sync-topology.ok.js";import t from"./parts/sync-retention.ok.js";export default{tag:"sync-index",unwrap:!0,dependencies:[s,t],context:()=>({loading:!1,error:null,topology:null,availableEnvs:[],selectedNodeId:null,showConnect:!1,joinAddress:"",dataLink:!0,joinToken:"",joinUsername:"",joinPassword:"",showPasswordAuth:!1,joining:!1,joinError:null,selfNode:null,selfClock:null,selfClocks:{},selfTagsInput:"",savingSelf:!1,saveSelfError:null,async loadSelf(){try{const s=await fetch(`${__ENV__.API}/sync/info`),t=await s.json();s.ok&&(this.selfNode=t.data?.node??null,this.selfClock=t.data?.clock??null,this.selfClocks=t.data?.clocks??{},this.selfTagsInput=(this.selfNode?.tags??[]).join(", "))}catch{}},async loadTopology(){this.loading=!0,this.error=null;try{const s=await fetch(`${__ENV__.API}/sync/topology`),t=await s.json();if(!s.ok)throw new Error(t?.error?.message||"Failed to load topology");const o=t.data??t;this.topology=o.topology??o,this.availableEnvs=o.availableEnvs??[]}catch(s){this.error=s?.message||String(s)}finally{this.loading=!1}},async loadIdentity(){},openConnect(){this.joinAddress="",this.joinToken="",this.joinUsername="",this.joinPassword="",this.joinError=null,this.showPasswordAuth=!1,this.showConnect=!0},closeConnect(){this.showConnect=!1,this.joinError=null},async joinCluster(){if(this.joinAddress&&!this.joining){this.joining=!0,this.joinError=null;try{const s={address:this.joinAddress,data_link:this.dataLink};!this.showPasswordAuth&&this.joinToken.trim()?s.token=this.joinToken.trim():this.showPasswordAuth&&this.joinUsername.trim()&&(s.username=this.joinUsername.trim(),s.password=this.joinPassword);const t=await fetch(`${__ENV__.API}/sync/connect`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),o=await t.json();if(!t.ok)throw new Error(o?.error?.message||"Connect failed");this.closeConnect(),await this.loadTopology()}catch(s){this.joinError=s.message}finally{this.joining=!1}}},get selfClockEntries(){return Object.entries(this.selfClocks).filter(([s])=>"~system"!==s)},get selfTagList(){return this.parseTags(this.selfTagsInput)},parseTags:s=>String(s??"").split(",").map(s=>s.trim()).filter(Boolean).filter((s,t,o)=>o.indexOf(s)===t),selectNode(s){this.selectedNodeId=s||null},async saveSelfTags(){if(!this.savingSelf){this.savingSelf=!0,this.saveSelfError=null;try{const s=await fetch(`${__ENV__.API}/sync/self`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({tags:this.selfTagList})}),t=await s.json();if(!s.ok)throw new Error(t?.error?.message||"Failed to save node tags");this.selfNode=t.data?.node??t.result?.node??this.selfNode,this.selfTagsInput=(this.selfNode?.tags??[]).join(", "),await this.loadTopology()}catch(s){this.saveSelfError=s?.message||String(s)}finally{this.savingSelf=!1}}},async copyText(s){try{await navigator.clipboard.writeText(String(s??""))}catch(s){}}}),async mount(){await this.loadSelf(),await this.loadTopology()}};</script><template><component import="../../layouts/_default.ok.html"><template name="sidebar"><if :="selfNode"><div style="background:var(--okdb-app-surface);border:1px solid var(--okdb-app-border);border-radius:10px;padding:10px 12px;font-size:12px;display:flex;flex-direction:column;gap:6px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.5;margin-bottom:2px">This Node</div><div style="display:flex;align-items:center;gap:6px"><span style="width:7px;height:7px;border-radius:50%;background:var(--okdb-success);flex-shrink:0"></span> <span style="font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{selfNode.name || selfNode.id?.slice(0,8)}}</span></div><div style="display:grid;grid-template-columns:52px 1fr;gap:3px 8px;opacity:.8"><span style="opacity:.6">ID</span><div style="display:flex;align-items:center;gap:4px"><span style="font-family:monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{selfNode.id?.slice(0, 12)}}… </span><span @click="copyText(selfNode.id)" style="cursor:pointer;opacity:.5;font-size:10px">⎘</span></div><if :="selfNode.address"><span style="opacity:.6">Addr</span> <span style="font-family:monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="selfNode.address">{{selfNode.address}}</span></if></div><div style="border-top:1px solid var(--okdb-app-border);padding-top:8px;margin-top:4px;display:flex;flex-direction:column;gap:8px"><div style="font-size:10px;opacity:.4;text-transform:uppercase;letter-spacing:.05em">Node Tags</div><input :value="selfTagsInput" @input="selfTagsInput = $event.target.value" @keydown.enter="saveSelfTags()" placeholder="gpu, region:eu, fast-storage" style="font-family:monospace;font-size:12px"/><div style="display:flex;flex-wrap:wrap;gap:6px;min-height:20px"><if :="selfTagList.length === 0"><span style="font-size:11px;opacity:.45">No tags</span></if><for :="selfTagList" let:tag><span style="font-size:10px;padding:2px 8px;border-radius:999px;border:1px solid var(--okdb-accent-soft-border);color:var(--okdb-accent);background:var(--okdb-accent-soft-bg)">{{tag}}</span></for></div><if :="saveSelfError"><div style="font-size:11px;color:var(--okdb-danger)">{{saveSelfError}}</div></if><div style="display:flex;justify-content:flex-end"><button @click="saveSelfTags()" :disabled="savingSelf" style="font-size:11px;padding:4px 10px;background:var(--okdb-sky-soft-bg);border-color:var(--okdb-sky-soft-border);color:var(--okdb-sky)">{{ savingSelf ? 'Saving…' : 'Save Tags' }}</button></div></div><if :="selfClockEntries.length"><div style="border-top:1px solid var(--okdb-app-border);padding-top:6px;margin-top:2px"><div style="font-size:10px;opacity:.4;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Clocks</div><div style="display:grid;grid-template-columns:64px 1fr;gap:2px 8px;font-size:11px"><for :="selfClockEntries" let:entry><span style="opacity:.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{entry[0]}}</span> <span style="font-family:monospace;opacity:.8">{{entry[1]}}</span></for></div></div></if></div></if></template><template name="default"><div style="display:flex;flex-direction:column;gap:20px;height:100%"><div style="display:flex;justify-content:space-between;align-items:center"><div style="font-weight:600;font-size:18px">Sync Cluster</div><div style="display:flex;gap:8px"><button @click="loadTopology()" :disabled="loading">Refresh</button> <button @click="openConnect()" style="background:var(--okdb-sky-soft-bg);border-color:var(--okdb-sky-soft-border);color:var(--okdb-sky)">+ Connect Node</button></div></div><if :="error"><div style="color:var(--okdb-danger);font-size:13px">{{error}}</div></if><sync-topology :topology="topology" :available-envs="availableEnvs" :self-id="selfNode?.id" :selected-node-id="selectedNodeId" @select="selectNode($event.detail)"/><div style="border-top:1px solid var(--okdb-app-border);padding-top:20px"><sync-retention/></div></div><if :="showConnect"><div @click.self="closeConnect()" style="position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center"><div style="background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:14px;padding:24px;width:440px;max-width:calc(100vw - 32px);display:flex;flex-direction:column;gap:16px;box-shadow:0 24px 64px rgba(0,0,0,.6)"><div style="display:flex;justify-content:space-between;align-items:center"><div style="font-weight:700;font-size:16px">Connect to Node</div><span @click="closeConnect()" style="cursor:pointer;opacity:.5;font-size:18px;line-height:1;padding:2px 6px">✕</span></div><div style="display:flex;flex-direction:column;gap:6px"><label style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.06em">Remote address</label> <input :value="joinAddress" @input="joinAddress = $event.target.value" @keydown.enter="joinCluster()" placeholder="http://host:8484" style="font-family:monospace" autofocus/></div><div style="display:flex;flex-direction:column;gap:8px"><div style="display:flex;align-items:center;justify-content:space-between"><label style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.06em">Credential</label><div style="display:flex;border:1px solid var(--okdb-app-border);border-radius:6px;overflow:hidden;font-size:11px"><button @click="showPasswordAuth = false" :style="!showPasswordAuth
2
2
  ? 'background:var(--okdb-app-surface);color:var(--okdb-app-fg-strong);border:none;padding:3px 12px;cursor:pointer;'
3
3
  : 'background:transparent;color:var(--okdb-app-fg-subtle);border:none;padding:3px 12px;cursor:pointer;'">Token</button> <button @click="showPasswordAuth = true" :style="showPasswordAuth
4
4
  ? 'background:var(--okdb-app-surface);color:var(--okdb-app-fg-strong);border:none;padding:3px 12px;cursor:pointer;'
@@ -1 +1 @@
1
- function _0x4789(_0x412098,_0x55e357){_0x412098=_0x412098-0xfd;var _0x19cf6c=_0x19cf();var _0x478914=_0x19cf6c[_0x412098];return _0x478914;}var _0x5edc02=_0x4789;(function(_0x460750,_0x919b6a){var _0xeab1d3=_0x4789,_0x19889b=_0x460750();while(!![]){try{var _0x27a178=parseInt(_0xeab1d3(0x101))/0x1+parseInt(_0xeab1d3(0xff))/0x2+parseInt(_0xeab1d3(0xfd))/0x3+-parseInt(_0xeab1d3(0x103))/0x4+-parseInt(_0xeab1d3(0x108))/0x5+parseInt(_0xeab1d3(0x106))/0x6+parseInt(_0xeab1d3(0x107))/0x7*(-parseInt(_0xeab1d3(0x102))/0x8);if(_0x27a178===_0x919b6a)break;else _0x19889b['push'](_0x19889b['shift']());}catch(_0xcf8b71){_0x19889b['push'](_0x19889b['shift']());}}}(_0x19cf,0x316c3));export default{'tag':_0x5edc02(0x105),'unwrap':!0x0,'context':_0x2784ee=>({get 'topology'(){var _0x3570f4=_0x5edc02;return _0x2784ee['$attr'][_0x3570f4(0x100)]||{};},get 'nodeId'(){var _0x37c566=_0x5edc02;return _0x2784ee[_0x37c566(0x109)][_0x37c566(0xfe)];},get 'node'(){var _0x247996=_0x5edc02;return this[_0x247996(0x100)]?.[this[_0x247996(0xfe)]]||null;},get 'tags'(){var _0x78a1f8=_0x5edc02;return Array['isArray'](this[_0x78a1f8(0x10a)]?.[_0x78a1f8(0x104)])?this['node'][_0x78a1f8(0x104)]:[];},'pretty'(_0x1af3e9){if(null==_0x1af3e9)return'';try{return JSON['stringify'](_0x1af3e9,null,0x2);}catch{return String(_0x1af3e9);}}}),'template':'<if\x20:=\x22node\x22><div\x20style=\x22border:1px\x20solid\x20var(--okdb-app-border);\x20border-radius:10px;\x20padding:14px;\x20background:var(--okdb-app-bg);\x20display:flex;\x20flex-direction:column;\x20gap:12px;\x22><div\x20style=\x22display:flex;\x20align-items:center;\x20gap:8px;\x22><div\x20style=\x22font-weight:600;\x20flex:1;\x20min-width:0;\x20overflow:hidden;\x20text-overflow:ellipsis;\x20white-space:nowrap;\x22>Node\x20Detail:\x20{{node.id}}</div><if\x20:=\x22node.error\x22><span\x20style=\x22font-size:10px;\x20padding:2px\x208px;\x20border-radius:999px;\x20color:var(--okdb-danger);\x20border:1px\x20solid\x20color-mix(in\x20srgb,\x20var(--okdb-danger)\x2020%,\x20transparent);\x22>error</span></if></div><div\x20style=\x22display:grid;\x20grid-template-columns:repeat(auto-fit,minmax(180px,1fr));\x20gap:10px\x2014px;\x20font-size:12px;\x22><div><div\x20style=\x22opacity:.45;\x20font-size:10px;\x20text-transform:uppercase;\x20letter-spacing:.05em;\x22>Address</div><div\x20style=\x22margin-top:4px;\x20font-family:monospace;\x20word-break:break-all;\x22>{{node.address\x20||\x20\x27—\x27}}</div></div><div><div\x20style=\x22opacity:.45;\x20font-size:10px;\x20text-transform:uppercase;\x20letter-spacing:.05em;\x22>Clock</div><div\x20style=\x22margin-top:4px;\x20font-family:monospace;\x22>{{node.clock\x20??\x20\x27—\x27}}</div></div><div><div\x20style=\x22opacity:.45;\x20font-size:10px;\x20text-transform:uppercase;\x20letter-spacing:.05em;\x22>Available\x20envs</div><div\x20style=\x22margin-top:4px;\x22>{{(node.availableEnvs\x20||\x20[]).join(\x27,\x20\x27)\x20||\x20\x27—\x27}}</div></div></div><div\x20style=\x22display:flex;\x20flex-direction:column;\x20gap:6px;\x22><div\x20style=\x22opacity:.45;\x20font-size:10px;\x20text-transform:uppercase;\x20letter-spacing:.05em;\x22>Tags</div><div\x20style=\x22display:flex;\x20flex-wrap:wrap;\x20gap:6px;\x20min-height:20px;\x22><if\x20:=\x22tags.length\x20===\x200\x22><span\x20style=\x22font-size:11px;\x20opacity:.45;\x22>No\x20tags</span></if><for\x20:=\x22tags\x22\x20let:tag><span\x20style=\x22font-size:10px;\x20padding:2px\x208px;\x20border-radius:999px;\x20border:1px\x20solid\x20var(--okdb-accent-soft-border);\x20color:var(--okdb-accent);\x20background:var(--okdb-accent-soft-bg);\x22>{{tag}}</span></for></div></div><pre\x20style=\x22font-size:12px;\x20margin:0;\x20overflow:auto;\x20background:var(--okdb-app-bg-deep);\x20border:1px\x20solid\x20var(--okdb-app-border);\x20border-radius:8px;\x20padding:10px;\x22>\x0a{{pretty(node)}}\x0a\x20\x20\x20\x20\x20\x20\x20\x20</pre></div></if>'};function _0x19cf(){var _0x338f07=['topology','402828NgwPtG','8JBqsgC','922536SfLuFT','tags','sync-node-detail','2149056rBTngs','1364783xXmQAy','1910550eVIzMc','$attr','node','447732BBjeyY','nodeId','199800QOTNeS'];_0x19cf=function(){return _0x338f07;};return _0x19cf();}
1
+ export default{tag:"sync-node-detail",unwrap:!0,context:e=>({get topology(){return e.$attr.topology||{}},get nodeId(){return e.$attr.nodeId},get node(){return this.topology?.[this.nodeId]||null},get tags(){return Array.isArray(this.node?.tags)?this.node.tags:[]},pretty(e){if(null==e)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}}}),template:'<if :="node"><div style="border:1px solid var(--okdb-app-border); border-radius:10px; padding:14px; background:var(--okdb-app-bg); display:flex; flex-direction:column; gap:12px;"><div style="display:flex; align-items:center; gap:8px;"><div style="font-weight:600; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">Node Detail: {{node.id}}</div><if :="node.error"><span style="font-size:10px; padding:2px 8px; border-radius:999px; color:var(--okdb-danger); border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);">error</span></if></div><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px 14px; font-size:12px;"><div><div style="opacity:.45; font-size:10px; text-transform:uppercase; letter-spacing:.05em;">Address</div><div style="margin-top:4px; font-family:monospace; word-break:break-all;">{{node.address || \'—\'}}</div></div><div><div style="opacity:.45; font-size:10px; text-transform:uppercase; letter-spacing:.05em;">Clock</div><div style="margin-top:4px; font-family:monospace;">{{node.clock ?? \'—\'}}</div></div><div><div style="opacity:.45; font-size:10px; text-transform:uppercase; letter-spacing:.05em;">Available envs</div><div style="margin-top:4px;">{{(node.availableEnvs || []).join(\', \') || \'—\'}}</div></div></div><div style="display:flex; flex-direction:column; gap:6px;"><div style="opacity:.45; font-size:10px; text-transform:uppercase; letter-spacing:.05em;">Tags</div><div style="display:flex; flex-wrap:wrap; gap:6px; min-height:20px;"><if :="tags.length === 0"><span style="font-size:11px; opacity:.45;">No tags</span></if><for :="tags" let:tag><span style="font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid var(--okdb-accent-soft-border); color:var(--okdb-accent); background:var(--okdb-accent-soft-bg);">{{tag}}</span></for></div></div><pre style="font-size:12px; margin:0; overflow:auto; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:8px; padding:10px;">\n{{pretty(node)}}\n </pre></div></if>'};
@@ -1 +1 @@
1
- function _0x467d(){const _0x7cc5d0=['json','floor','loading','10URzfeY','round','peerStalenessLimitMs','#f59e0b18','values','~system','Force\x20resync\x20failed','/sync/gc/status','Stale','479596TVHmqu','44085YHhTbD','GC\x20run\x20failed','72ZZqePH','result','61853Jhurhr','3NMjtLf','API','message','285288yvLdYb','m\x20ago','peer_id','runError','4720YqNKyu','includes','/resync','1821744YDArKo','just\x20now','gcHorizon','running','1190LSchxs','envs','var(--okdb-success)','Healthy','/sync/gc/run','status','data','envRows','sync-retention','d\x20ago','var(--okdb-warning)','toLocaleString','/sync/gc/peer/','load','runResult','<div\x20style=\x22display:flex;\x20flex-direction:column;\x20gap:16px;\x22><div\x20style=\x22display:flex;\x20align-items:center;\x20gap:10px;\x22><div\x20style=\x22flex:1;\x22><div\x20style=\x22font-weight:600;\x20font-size:15px;\x22>Changelog\x20GC\x20—\x20this\x20node</div><div\x20style=\x22font-size:11px;\x20opacity:.4;\x20margin-top:3px;\x20display:flex;\x20align-items:center;\x20gap:10px;\x20flex-wrap:wrap;\x22><span>Runs\x20automatically\x20when\x20peer\x20watermarks\x20advance.</span><if\x20:=\x22stalenessLimit\x22><span\x20style=\x22font-family:monospace;\x20opacity:.8;\x22>stale\x20after:\x20{{stalenessLimit}}</span></if><span>Each\x20cluster\x20node\x20manages\x20its\x20own\x20changelog\x20independently.</span></div></div><button\x20@click=\x22load()\x22\x20:disabled=\x22loading\x22\x20style=\x22font-size:11px;\x20padding:3px\x2010px;\x22>{{\x20loading\x20?\x20\x27Loading…\x27\x20:\x20\x27Refresh\x27\x20}}</button>\x20<button\x20@click=\x22runGc()\x22\x20:disabled=\x22running\x22\x20style=\x22font-size:11px;\x20padding:3px\x2010px;\x20background:#1a3a20;\x20border-color:#2a6a30;\x20color:#6ee08a;\x22\x20title=\x22Immediately\x20sweep\x20this\x20node\x27s\x20changelog\x20across\x20all\x20environments.\x20Other\x20cluster\x20nodes\x20are\x20unaffected.\x22>{{\x20running\x20?\x20\x27Running…\x27\x20:\x20\x27Run\x20GC\x20now\x27\x20}}</button></div><if\x20:=\x22error\x22><div\x20style=\x22font-size:12px;\x20color:var(--okdb-danger);\x20background:var(--okdb-danger-soft-bg);\x20border:1px\x20solid\x20var(--okdb-danger-soft-border);\x20border-radius:8px;\x20padding:10px\x2012px;\x22>{{error}}</div></if><if\x20:=\x22runResult\x22><div\x20style=\x22font-size:12px;\x20color:var(--okdb-success);\x20background:#0f2a1a;\x20border:1px\x20solid\x20#1a5a30;\x20border-radius:8px;\x20padding:10px\x2012px;\x22>GC\x20complete\x20—\x20removed\x20{{runResult.removed?.toLocaleString()\x20??\x200}}\x20entries\x20across\x20all\x20environments\x20({{runResult.durationMs}}ms)</div></if><if\x20:=\x22runError\x22><div\x20style=\x22font-size:12px;\x20color:var(--okdb-danger);\x20background:var(--okdb-danger-soft-bg);\x20border:1px\x20solid\x20var(--okdb-danger-soft-border);\x20border-radius:8px;\x20padding:10px\x2012px;\x22>{{runError}}</div></if><if\x20:=\x22status\x20&&\x20envRows.length\x22><div\x20style=\x22background:var(--okdb-app-bg-deep);\x20border:1px\x20solid\x20var(--okdb-app-border);\x20border-radius:10px;\x20overflow:hidden;\x22><div\x20style=\x22padding:10px\x2014px;\x20font-size:11px;\x20text-transform:uppercase;\x20letter-spacing:.06em;\x20opacity:.45;\x20border-bottom:1px\x20solid\x20var(--okdb-app-border);\x22>Per-Environment\x20Changelog</div><div\x20style=\x22display:grid;\x20grid-template-columns:1fr\x201fr\x201fr\x201fr;\x20font-size:11px;\x22><div\x20style=\x22padding:8px\x2014px;\x20opacity:.45;\x22>Env</div><div\x20style=\x22padding:8px\x2014px;\x20opacity:.45;\x20text-align:right;\x22>Entries</div><div\x20style=\x22padding:8px\x2014px;\x20opacity:.45;\x20text-align:right;\x22>Current\x20clock</div><div\x20style=\x22padding:8px\x2014px;\x20opacity:.45;\x20text-align:right;\x22\x20title=\x22Lowest\x20clock\x20all\x20healthy\x20peers\x20have\x20confirmed\x20receiving\x20from\x20this\x20node.\x20Entries\x20below\x20this\x20are\x20safe\x20to\x20delete.\x22>GC\x20horizon</div><for\x20:=\x22envRows\x22\x20let:env><div\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20font-family:monospace;\x20color:var(--okdb-sky);\x22>{{env.env}}</div><div\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x20font-family:monospace;\x22>{{(env.entryCount\x20??\x200).toLocaleString()}}</div><div\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x20font-family:monospace;\x20opacity:.7;\x22>{{fmtClock(env.currentClock)}}</div><div\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x20font-family:monospace;\x22\x20:style=\x22env.gcHorizon\x20>\x200\x20?\x20\x27color:var(--okdb-success);\x27\x20:\x20\x27opacity:.35;\x27\x22>{{env.gcHorizon\x20>\x200\x20?\x20fmtClock(env.gcHorizon)\x20:\x20\x27no\x20peers\x20synced\x20yet\x27}}</div></for></div></div></if><if\x20:=\x22status\x22><div\x20style=\x22background:var(--okdb-app-bg-deep);\x20border:1px\x20solid\x20var(--okdb-app-border);\x20border-radius:10px;\x20overflow:hidden;\x22><div\x20style=\x22padding:10px\x2014px;\x20font-size:11px;\x20text-transform:uppercase;\x20letter-spacing:.06em;\x20opacity:.45;\x20border-bottom:1px\x20solid\x20var(--okdb-app-border);\x22>Peer\x20Watermarks\x20—\x20as\x20seen\x20by\x20this\x20node</div><if\x20:=\x22peerRows.length\x20===\x200\x22><div\x20style=\x22padding:20px\x2014px;\x20font-size:12px;\x20opacity:.4;\x20text-align:center;\x22>No\x20peers\x20have\x20pulled\x20deltas\x20from\x20this\x20node\x20yet.\x20Watermarks\x20are\x20recorded\x20when\x20a\x20peer\x20sends\x20its\x20first\x20delta\x20request.</div></if><if\x20:=\x22peerRows.length\x20>\x200\x22><div\x20style=\x22overflow-x:auto;\x22><table\x20style=\x22width:100%;\x20border-collapse:collapse;\x20font-size:11px;\x22><thead><tr\x20style=\x22opacity:.45;\x22><th\x20style=\x22padding:8px\x2014px;\x20text-align:left;\x20font-weight:normal;\x20border-bottom:1px\x20solid\x20var(--okdb-app-surface);\x22>Peer</th><th\x20style=\x22padding:8px\x2014px;\x20text-align:left;\x20font-weight:normal;\x20border-bottom:1px\x20solid\x20var(--okdb-app-surface);\x22>Last\x20seen</th><th\x20style=\x22padding:8px\x2014px;\x20text-align:right;\x20font-weight:normal;\x20border-bottom:1px\x20solid\x20var(--okdb-app-surface);\x22>Status</th><th\x20style=\x22padding:8px\x2014px;\x20text-align:right;\x20font-weight:normal;\x20border-bottom:1px\x20solid\x20var(--okdb-app-surface);\x22\x20title=\x22Highest\x20clock\x20this\x20peer\x20has\x20confirmed\x20receiving\x20from\x20this\x20node,\x20per\x20environment.\x22>Confirmed\x20up\x20to</th><th\x20style=\x22padding:8px\x2014px;\x20border-bottom:1px\x20solid\x20var(--okdb-app-surface);\x22/></tr></thead><tbody><tr\x20as=\x22repeat\x22\x20:of=\x22peerRows\x22\x20let:item=\x22peer\x22><td\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20font-family:monospace;\x20color:var(--okdb-sky);\x20max-width:200px;\x20overflow:hidden;\x20text-overflow:ellipsis;\x20white-space:nowrap;\x22\x20:style=\x22isPinnedPeer(peer)\x20?\x20\x27color:var(--okdb-warning);\x27\x20:\x20\x27\x27\x22\x20:title=\x22peer.peer_id\x22>{{peer.peer_id.slice(0,\x2016)}}…</td><td\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20opacity:.7;\x22>{{fmtTime(peer.since_ms)}}</td><td\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x22><span\x20:style=\x22\x27font-size:10px;\x20padding:2px\x208px;\x20border-radius:999px;\x20border:1px\x20solid\x20\x27\x20+\x20statusBadge(peer).color\x20+\x20\x2744;\x20color:\x27\x20+\x20statusBadge(peer).color\x20+\x20\x27;\x20background:\x27\x20+\x20statusBadge(peer).bg\x22>{{statusBadge(peer).label}}</span></td><td\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x20font-family:monospace;\x20font-size:10px;\x20opacity:.7;\x22><for\x20:=\x22Object.entries(peer.envs\x20||\x20{}).filter(([k])\x20=>\x20k\x20!==\x20\x27~system\x27)\x22\x20let:entry><div>{{entry[0]}}:\x20{{fmtClock(entry[1])}}</div></for></td><td\x20style=\x22padding:8px\x2014px;\x20border-top:1px\x20solid\x20var(--okdb-app-surface);\x20text-align:right;\x22><button\x20@click=\x22forceResync(peer.peer_id)\x22\x20style=\x22font-size:10px;\x20padding:2px\x208px;\x20background:var(--okdb-danger-soft-bg);\x20border-color:var(--okdb-danger-soft-border);\x20color:var(--okdb-danger);\x22\x20title=\x22Reset\x20this\x20peer\x27s\x20watermark\x20to\x20zero\x20on\x20this\x20node.\x20Their\x20next\x20connection\x20will\x20require\x20a\x20full\x20snapshot\x20pull\x20from\x20this\x20node.\x22>Force\x20resync</button></td></tr></tbody></table></div></if></div></if><div\x20style=\x22font-size:11px;\x20opacity:.35;\x20line-height:1.6;\x22>The\x20GC\x20horizon\x20is\x20the\x20lowest\x20clock\x20any\x20healthy\x20peer\x20has\x20confirmed\x20receiving\x20from\x20this\x20node,\x20per\x20environment.\x20Entries\x20below\x20it\x20are\x20already\x20in\x20every\x20peer\x27s\x20local\x20store\x20and\x20are\x20safe\x20to\x20delete.\x20Stale\x20peers\x20are\x20excluded\x20so\x20an\x20offline\x20node\x20never\x20blocks\x20GC\x20indefinitely.\x20Each\x20cluster\x20node\x20maintains\x20its\x20own\x20watermarks\x20and\x20runs\x20GC\x20on\x20its\x20own\x20schedule.</div></div>','POST','444722cIjCje','horizonPinnedBy','filter','h\x20ago','error','env','stale'];_0x467d=function(){return _0x7cc5d0;};return _0x467d();}const _0x214b50=_0x5a1a;(function(_0x11b2e5,_0x4160c7){const _0x42a9b1=_0x5a1a,_0x977bcb=_0x11b2e5();while(!![]){try{const _0x3c5610=-parseInt(_0x42a9b1(0xe5))/0x1+parseInt(_0x42a9b1(0x101))/0x2*(parseInt(_0x42a9b1(0xe2))/0x3)+parseInt(_0x42a9b1(0xdc))/0x4+-parseInt(_0x42a9b1(0xdd))/0x5*(-parseInt(_0x42a9b1(0xdf))/0x6)+-parseInt(_0x42a9b1(0xf0))/0x7*(parseInt(_0x42a9b1(0xe9))/0x8)+parseInt(_0x42a9b1(0xec))/0x9+-parseInt(_0x42a9b1(0x10b))/0xa*(parseInt(_0x42a9b1(0xe1))/0xb);if(_0x3c5610===_0x4160c7)break;else _0x977bcb['push'](_0x977bcb['shift']());}catch(_0x2823c2){_0x977bcb['push'](_0x977bcb['shift']());}}}(_0x467d,0x3f4c5));function _0x5a1a(_0x855536,_0x124f0e){_0x855536=_0x855536-0xd7;const _0x467d08=_0x467d();let _0x5a1aa3=_0x467d08[_0x855536];return _0x5a1aa3;}export default{'tag':_0x214b50(0xf8),'unwrap':!0x0,'context':_0x21844e=>({'loading':!0x1,'error':null,'status':null,'running':!0x1,'runResult':null,'runError':null,async 'load'(){const _0x10bfb6=_0x214b50;this[_0x10bfb6(0x10a)]=!0x0,this[_0x10bfb6(0x105)]=null;try{const _0x282500=await fetch(__ENV__[_0x10bfb6(0xe3)]+_0x10bfb6(0xda)),_0x396b20=await _0x282500[_0x10bfb6(0x108)]();if(!_0x282500['ok'])throw new Error(_0x396b20?.[_0x10bfb6(0x105)]?.['message']||'Failed\x20to\x20load\x20GC\x20status');this[_0x10bfb6(0xf5)]=_0x396b20[_0x10bfb6(0xf6)]??_0x396b20[_0x10bfb6(0xe0)]??null;}catch(_0x5356ba){this[_0x10bfb6(0x105)]=_0x5356ba?.[_0x10bfb6(0xe4)]||String(_0x5356ba);}finally{this['loading']=!0x1;}},async 'runGc'(){const _0x5f1835=_0x214b50;if(!this[_0x5f1835(0xef)]){this['running']=!0x0,this[_0x5f1835(0xfe)]=null,this[_0x5f1835(0xe8)]=null;try{const _0x1bb1b1=await fetch(__ENV__['API']+_0x5f1835(0xf4),{'method':_0x5f1835(0x100)}),_0x2a1508=await _0x1bb1b1[_0x5f1835(0x108)]();if(!_0x1bb1b1['ok'])throw new Error(_0x2a1508?.['error']?.[_0x5f1835(0xe4)]||_0x5f1835(0xde));this[_0x5f1835(0xfe)]=_0x2a1508[_0x5f1835(0xf6)]??_0x2a1508[_0x5f1835(0xe0)]??null,await this[_0x5f1835(0xfd)]();}catch(_0x1eca9c){this[_0x5f1835(0xe8)]=_0x1eca9c?.[_0x5f1835(0xe4)]||String(_0x1eca9c);}finally{this['running']=!0x1;}}},async 'forceResync'(_0x5d6cf5){const _0x41edc2=_0x214b50;try{const _0x3fac37=await fetch(__ENV__[_0x41edc2(0xe3)]+_0x41edc2(0xfc)+encodeURIComponent(_0x5d6cf5)+_0x41edc2(0xeb),{'method':'POST'}),_0x397dc2=await _0x3fac37[_0x41edc2(0x108)]();if(!_0x3fac37['ok'])throw new Error(_0x397dc2?.['error']?.[_0x41edc2(0xe4)]||_0x41edc2(0xd9));await this[_0x41edc2(0xfd)]();}catch(_0x56851d){this[_0x41edc2(0x105)]=_0x56851d?.[_0x41edc2(0xe4)]||String(_0x56851d);}},'fmtClock':_0x232f92=>null==_0x232f92?'—':Number(_0x232f92)[_0x214b50(0xfb)](),'fmtTime'(_0x253242){const _0x28fdf0=_0x214b50;if(!_0x253242)return'—';const _0x322eaa=Math[_0x28fdf0(0x109)](_0x253242/0x5265c00),_0x235c08=Math[_0x28fdf0(0x109)](_0x253242%0x5265c00/0x36ee80),_0xeb0883=Math['floor'](_0x253242%0x36ee80/0xea60);return _0x322eaa>0x0?_0x322eaa+_0x28fdf0(0xf9):_0x235c08>0x0?_0x235c08+_0x28fdf0(0x104):_0xeb0883>0x0?_0xeb0883+_0x28fdf0(0xe6):_0x28fdf0(0xed);},'statusBadge':_0x140040=>_0x140040[_0x214b50(0x107)]?{'label':_0x214b50(0xdb),'color':_0x214b50(0xfa),'bg':_0x214b50(0x10e)}:{'label':_0x214b50(0xf3),'color':_0x214b50(0xf2),'bg':'#4ade8018'},get 'envRows'(){const _0x3aa940=_0x214b50;return(this[_0x3aa940(0xf5)]?.[_0x3aa940(0xf1)]??[])[_0x3aa940(0x103)](_0x938ea7=>_0x3aa940(0xd8)!==_0x938ea7[_0x3aa940(0x106)]);},get 'peerRows'(){const _0x4d40d0=_0x214b50;return this[_0x4d40d0(0xf5)]?.['peers']??[];},get 'horizonPinnedBy'(){const _0x3ad588=_0x214b50,_0x40c2a7={};for(const _0x1cf3f5 of this[_0x3ad588(0xf7)]){if(!_0x1cf3f5[_0x3ad588(0xee)])continue;const _0x4927fe=this['peerRows'][_0x3ad588(0x103)](_0x3dff5e=>!_0x3dff5e[_0x3ad588(0x107)]&&null!=_0x3dff5e[_0x3ad588(0xf1)]?.[_0x1cf3f5[_0x3ad588(0x106)]])['sort']((_0x5a68a8,_0x736f7)=>(_0x5a68a8[_0x3ad588(0xf1)][_0x1cf3f5[_0x3ad588(0x106)]]||0x0)-(_0x736f7['envs'][_0x1cf3f5['env']]||0x0));_0x40c2a7[_0x1cf3f5[_0x3ad588(0x106)]]=_0x4927fe[0x0]?.[_0x3ad588(0xe7)]??null;}return _0x40c2a7;},'isPinnedPeer'(_0x3fc84a){const _0x4fa34b=_0x214b50;return!!_0x3fc84a?.[_0x4fa34b(0xe7)]&&Object[_0x4fa34b(0xd7)](this[_0x4fa34b(0x102)])[_0x4fa34b(0xea)](_0x3fc84a[_0x4fa34b(0xe7)]);},get 'stalenessLimit'(){const _0x145524=_0x214b50,_0x4e43d4=this[_0x145524(0xf5)]?.[_0x145524(0x10d)];return _0x4e43d4?Math[_0x145524(0x10c)](_0x4e43d4/0x5265c00)+'d':null;}}),async 'mount'(){const _0x5c6f2f=_0x214b50;await this[_0x5c6f2f(0xfd)]();},'template':_0x214b50(0xff)};
1
+ export default{tag:"sync-retention",unwrap:!0,context:e=>({loading:!1,error:null,status:null,running:!1,runResult:null,runError:null,async load(){this.loading=!0,this.error=null;try{const e=await fetch(`${__ENV__.API}/sync/gc/status`),t=await e.json();if(!e.ok)throw new Error(t?.error?.message||"Failed to load GC status");this.status=t.data??t.result??null}catch(e){this.error=e?.message||String(e)}finally{this.loading=!1}},async runGc(){if(!this.running){this.running=!0,this.runResult=null,this.runError=null;try{const e=await fetch(`${__ENV__.API}/sync/gc/run`,{method:"POST"}),t=await e.json();if(!e.ok)throw new Error(t?.error?.message||"GC run failed");this.runResult=t.data??t.result??null,await this.load()}catch(e){this.runError=e?.message||String(e)}finally{this.running=!1}}},async forceResync(e){try{const t=await fetch(`${__ENV__.API}/sync/gc/peer/${encodeURIComponent(e)}/resync`,{method:"POST"}),r=await t.json();if(!t.ok)throw new Error(r?.error?.message||"Force resync failed");await this.load()}catch(e){this.error=e?.message||String(e)}},fmtClock:e=>null==e?"—":Number(e).toLocaleString(),fmtTime(e){if(!e)return"—";const t=Math.floor(e/864e5),r=Math.floor(e%864e5/36e5),o=Math.floor(e%36e5/6e4);return t>0?`${t}d ago`:r>0?`${r}h ago`:o>0?`${o}m ago`:"just now"},statusBadge:e=>e.stale?{label:"Stale",color:"var(--okdb-warning)",bg:"#f59e0b18"}:{label:"Healthy",color:"var(--okdb-success)",bg:"#4ade8018"},get envRows(){return(this.status?.envs??[]).filter(e=>"~system"!==e.env)},get peerRows(){return this.status?.peers??[]},get horizonPinnedBy(){const e={};for(const t of this.envRows){if(!t.gcHorizon)continue;const r=this.peerRows.filter(e=>!e.stale&&null!=e.envs?.[t.env]).sort((e,r)=>(e.envs[t.env]||0)-(r.envs[t.env]||0));e[t.env]=r[0]?.peer_id??null}return e},isPinnedPeer(e){return!!e?.peer_id&&Object.values(this.horizonPinnedBy).includes(e.peer_id)},get stalenessLimit(){const e=this.status?.peerStalenessLimitMs;return e?`${Math.round(e/864e5)}d`:null}}),async mount(){await this.load()},template:'<div style="display:flex; flex-direction:column; gap:16px;"><div style="display:flex; align-items:center; gap:10px;"><div style="flex:1;"><div style="font-weight:600; font-size:15px;">Changelog GC — this node</div><div style="font-size:11px; opacity:.4; margin-top:3px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;"><span>Runs automatically when peer watermarks advance.</span><if :="stalenessLimit"><span style="font-family:monospace; opacity:.8;">stale after: {{stalenessLimit}}</span></if><span>Each cluster node manages its own changelog independently.</span></div></div><button @click="load()" :disabled="loading" style="font-size:11px; padding:3px 10px;">{{ loading ? \'Loading…\' : \'Refresh\' }}</button> <button @click="runGc()" :disabled="running" style="font-size:11px; padding:3px 10px; background:#1a3a20; border-color:#2a6a30; color:#6ee08a;" title="Immediately sweep this node\'s changelog across all environments. Other cluster nodes are unaffected.">{{ running ? \'Running…\' : \'Run GC now\' }}</button></div><if :="error"><div style="font-size:12px; color:var(--okdb-danger); background:var(--okdb-danger-soft-bg); border:1px solid var(--okdb-danger-soft-border); border-radius:8px; padding:10px 12px;">{{error}}</div></if><if :="runResult"><div style="font-size:12px; color:var(--okdb-success); background:#0f2a1a; border:1px solid #1a5a30; border-radius:8px; padding:10px 12px;">GC complete — removed {{runResult.removed?.toLocaleString() ?? 0}} entries across all environments ({{runResult.durationMs}}ms)</div></if><if :="runError"><div style="font-size:12px; color:var(--okdb-danger); background:var(--okdb-danger-soft-bg); border:1px solid var(--okdb-danger-soft-border); border-radius:8px; padding:10px 12px;">{{runError}}</div></if><if :="status && envRows.length"><div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:10px; overflow:hidden;"><div style="padding:10px 14px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; opacity:.45; border-bottom:1px solid var(--okdb-app-border);">Per-Environment Changelog</div><div style="display:grid; grid-template-columns:1fr 1fr 1fr 1fr; font-size:11px;"><div style="padding:8px 14px; opacity:.45;">Env</div><div style="padding:8px 14px; opacity:.45; text-align:right;">Entries</div><div style="padding:8px 14px; opacity:.45; text-align:right;">Current clock</div><div style="padding:8px 14px; opacity:.45; text-align:right;" title="Lowest clock all healthy peers have confirmed receiving from this node. Entries below this are safe to delete.">GC horizon</div><for :="envRows" let:env><div style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); font-family:monospace; color:var(--okdb-sky);">{{env.env}}</div><div style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right; font-family:monospace;">{{(env.entryCount ?? 0).toLocaleString()}}</div><div style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right; font-family:monospace; opacity:.7;">{{fmtClock(env.currentClock)}}</div><div style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right; font-family:monospace;" :style="env.gcHorizon > 0 ? \'color:var(--okdb-success);\' : \'opacity:.35;\'">{{env.gcHorizon > 0 ? fmtClock(env.gcHorizon) : \'no peers synced yet\'}}</div></for></div></div></if><if :="status"><div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:10px; overflow:hidden;"><div style="padding:10px 14px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; opacity:.45; border-bottom:1px solid var(--okdb-app-border);">Peer Watermarks — as seen by this node</div><if :="peerRows.length === 0"><div style="padding:20px 14px; font-size:12px; opacity:.4; text-align:center;">No peers have pulled deltas from this node yet. Watermarks are recorded when a peer sends its first delta request.</div></if><if :="peerRows.length > 0"><div style="overflow-x:auto;"><table style="width:100%; border-collapse:collapse; font-size:11px;"><thead><tr style="opacity:.45;"><th style="padding:8px 14px; text-align:left; font-weight:normal; border-bottom:1px solid var(--okdb-app-surface);">Peer</th><th style="padding:8px 14px; text-align:left; font-weight:normal; border-bottom:1px solid var(--okdb-app-surface);">Last seen</th><th style="padding:8px 14px; text-align:right; font-weight:normal; border-bottom:1px solid var(--okdb-app-surface);">Status</th><th style="padding:8px 14px; text-align:right; font-weight:normal; border-bottom:1px solid var(--okdb-app-surface);" title="Highest clock this peer has confirmed receiving from this node, per environment.">Confirmed up to</th><th style="padding:8px 14px; border-bottom:1px solid var(--okdb-app-surface);"/></tr></thead><tbody><tr as="repeat" :of="peerRows" let:item="peer"><td style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); font-family:monospace; color:var(--okdb-sky); max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" :style="isPinnedPeer(peer) ? \'color:var(--okdb-warning);\' : \'\'" :title="peer.peer_id">{{peer.peer_id.slice(0, 16)}}…</td><td style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); opacity:.7;">{{fmtTime(peer.since_ms)}}</td><td style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right;"><span :style="\'font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid \' + statusBadge(peer).color + \'44; color:\' + statusBadge(peer).color + \'; background:\' + statusBadge(peer).bg">{{statusBadge(peer).label}}</span></td><td style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right; font-family:monospace; font-size:10px; opacity:.7;"><for :="Object.entries(peer.envs || {}).filter(([k]) => k !== \'~system\')" let:entry><div>{{entry[0]}}: {{fmtClock(entry[1])}}</div></for></td><td style="padding:8px 14px; border-top:1px solid var(--okdb-app-surface); text-align:right;"><button @click="forceResync(peer.peer_id)" style="font-size:10px; padding:2px 8px; background:var(--okdb-danger-soft-bg); border-color:var(--okdb-danger-soft-border); color:var(--okdb-danger);" title="Reset this peer\'s watermark to zero on this node. Their next connection will require a full snapshot pull from this node.">Force resync</button></td></tr></tbody></table></div></if></div></if><div style="font-size:11px; opacity:.35; line-height:1.6;">The GC horizon is the lowest clock any healthy peer has confirmed receiving from this node, per environment. Entries below it are already in every peer\'s local store and are safe to delete. Stale peers are excluded so an offline node never blocks GC indefinitely. Each cluster node maintains its own watermarks and runs GC on its own schedule.</div></div>'};