beecork 1.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +52 -111
  2. package/dist/index.js +1394 -581
  3. package/package.json +26 -54
  4. package/dist/capabilities/index.d.ts +0 -3
  5. package/dist/capabilities/index.js +0 -2
  6. package/dist/capabilities/manager.d.ts +0 -13
  7. package/dist/capabilities/manager.js +0 -115
  8. package/dist/capabilities/packs.d.ts +0 -2
  9. package/dist/capabilities/packs.js +0 -54
  10. package/dist/capabilities/types.d.ts +0 -21
  11. package/dist/capabilities/types.js +0 -1
  12. package/dist/channels/admin.d.ts +0 -10
  13. package/dist/channels/admin.js +0 -20
  14. package/dist/channels/command-handler.d.ts +0 -19
  15. package/dist/channels/command-handler.js +0 -235
  16. package/dist/channels/discord.d.ts +0 -17
  17. package/dist/channels/discord.js +0 -266
  18. package/dist/channels/index.d.ts +0 -7
  19. package/dist/channels/index.js +0 -6
  20. package/dist/channels/loader.d.ts +0 -7
  21. package/dist/channels/loader.js +0 -87
  22. package/dist/channels/pipeline.d.ts +0 -50
  23. package/dist/channels/pipeline.js +0 -92
  24. package/dist/channels/registry.d.ts +0 -36
  25. package/dist/channels/registry.js +0 -80
  26. package/dist/channels/send-helpers.d.ts +0 -19
  27. package/dist/channels/send-helpers.js +0 -21
  28. package/dist/channels/telegram.d.ts +0 -47
  29. package/dist/channels/telegram.js +0 -649
  30. package/dist/channels/types.d.ts +0 -41
  31. package/dist/channels/types.js +0 -1
  32. package/dist/channels/voice-state.d.ts +0 -29
  33. package/dist/channels/voice-state.js +0 -45
  34. package/dist/channels/webhook.d.ts +0 -16
  35. package/dist/channels/webhook.js +0 -222
  36. package/dist/channels/whatsapp.d.ts +0 -28
  37. package/dist/channels/whatsapp.js +0 -323
  38. package/dist/cli/capabilities.d.ts +0 -3
  39. package/dist/cli/capabilities.js +0 -72
  40. package/dist/cli/channel.d.ts +0 -3
  41. package/dist/cli/channel.js +0 -202
  42. package/dist/cli/commands.d.ts +0 -15
  43. package/dist/cli/commands.js +0 -279
  44. package/dist/cli/doctor.d.ts +0 -1
  45. package/dist/cli/doctor.js +0 -208
  46. package/dist/cli/handoff.d.ts +0 -8
  47. package/dist/cli/handoff.js +0 -33
  48. package/dist/cli/helpers.d.ts +0 -2
  49. package/dist/cli/helpers.js +0 -21
  50. package/dist/cli/mcp.d.ts +0 -3
  51. package/dist/cli/mcp.js +0 -47
  52. package/dist/cli/media.d.ts +0 -2
  53. package/dist/cli/media.js +0 -125
  54. package/dist/cli/setup.d.ts +0 -1
  55. package/dist/cli/setup.js +0 -239
  56. package/dist/cli/store.d.ts +0 -3
  57. package/dist/cli/store.js +0 -108
  58. package/dist/config.d.ts +0 -11
  59. package/dist/config.js +0 -115
  60. package/dist/cron/scheduler.d.ts +0 -22
  61. package/dist/cron/scheduler.js +0 -220
  62. package/dist/cron/store.d.ts +0 -12
  63. package/dist/cron/store.js +0 -83
  64. package/dist/daemon.d.ts +0 -1
  65. package/dist/daemon.js +0 -345
  66. package/dist/dashboard/html.d.ts +0 -1
  67. package/dist/dashboard/html.js +0 -849
  68. package/dist/dashboard/routes.d.ts +0 -17
  69. package/dist/dashboard/routes.js +0 -623
  70. package/dist/dashboard/server.d.ts +0 -1
  71. package/dist/dashboard/server.js +0 -104
  72. package/dist/db/connection.d.ts +0 -29
  73. package/dist/db/connection.js +0 -37
  74. package/dist/db/index.d.ts +0 -14
  75. package/dist/db/index.js +0 -127
  76. package/dist/db/migrations.d.ts +0 -2
  77. package/dist/db/migrations.js +0 -384
  78. package/dist/delegation/manager.d.ts +0 -18
  79. package/dist/delegation/manager.js +0 -89
  80. package/dist/index.d.ts +0 -2
  81. package/dist/knowledge/index.d.ts +0 -2
  82. package/dist/knowledge/index.js +0 -1
  83. package/dist/knowledge/manager.d.ts +0 -17
  84. package/dist/knowledge/manager.js +0 -160
  85. package/dist/knowledge/types.d.ts +0 -7
  86. package/dist/knowledge/types.js +0 -1
  87. package/dist/machines/forwarder.d.ts +0 -7
  88. package/dist/machines/forwarder.js +0 -35
  89. package/dist/machines/index.d.ts +0 -1
  90. package/dist/machines/index.js +0 -1
  91. package/dist/machines/registry.d.ts +0 -15
  92. package/dist/machines/registry.js +0 -46
  93. package/dist/mcp/handlers.d.ts +0 -37
  94. package/dist/mcp/handlers.js +0 -520
  95. package/dist/mcp/server.d.ts +0 -2
  96. package/dist/mcp/server.js +0 -86
  97. package/dist/mcp/tool-definitions.d.ts +0 -1225
  98. package/dist/mcp/tool-definitions.js +0 -412
  99. package/dist/mcp/validate.d.ts +0 -23
  100. package/dist/mcp/validate.js +0 -65
  101. package/dist/media/factory.d.ts +0 -6
  102. package/dist/media/factory.js +0 -36
  103. package/dist/media/generators/dall-e.d.ts +0 -10
  104. package/dist/media/generators/dall-e.js +0 -41
  105. package/dist/media/generators/elevenlabs-music.d.ts +0 -9
  106. package/dist/media/generators/elevenlabs-music.js +0 -34
  107. package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
  108. package/dist/media/generators/elevenlabs-sfx.js +0 -33
  109. package/dist/media/generators/kling.d.ts +0 -9
  110. package/dist/media/generators/kling.js +0 -47
  111. package/dist/media/generators/lyria.d.ts +0 -10
  112. package/dist/media/generators/lyria.js +0 -41
  113. package/dist/media/generators/nano-banana.d.ts +0 -10
  114. package/dist/media/generators/nano-banana.js +0 -45
  115. package/dist/media/generators/poll-util.d.ts +0 -12
  116. package/dist/media/generators/poll-util.js +0 -22
  117. package/dist/media/generators/recraft.d.ts +0 -9
  118. package/dist/media/generators/recraft.js +0 -52
  119. package/dist/media/generators/runway.d.ts +0 -9
  120. package/dist/media/generators/runway.js +0 -49
  121. package/dist/media/generators/stable-diffusion.d.ts +0 -9
  122. package/dist/media/generators/stable-diffusion.js +0 -35
  123. package/dist/media/generators/veo.d.ts +0 -9
  124. package/dist/media/generators/veo.js +0 -32
  125. package/dist/media/index.d.ts +0 -6
  126. package/dist/media/index.js +0 -8
  127. package/dist/media/loader.d.ts +0 -8
  128. package/dist/media/loader.js +0 -46
  129. package/dist/media/store.d.ts +0 -17
  130. package/dist/media/store.js +0 -69
  131. package/dist/media/types.d.ts +0 -43
  132. package/dist/media/types.js +0 -1
  133. package/dist/memory/extractor.d.ts +0 -5
  134. package/dist/memory/extractor.js +0 -157
  135. package/dist/notifications/index.d.ts +0 -4
  136. package/dist/notifications/index.js +0 -21
  137. package/dist/notifications/ntfy.d.ts +0 -9
  138. package/dist/notifications/ntfy.js +0 -25
  139. package/dist/notifications/pushover.d.ts +0 -9
  140. package/dist/notifications/pushover.js +0 -26
  141. package/dist/notifications/types.d.ts +0 -5
  142. package/dist/notifications/types.js +0 -1
  143. package/dist/notifications/webhook-provider.d.ts +0 -9
  144. package/dist/notifications/webhook-provider.js +0 -24
  145. package/dist/observability/analytics.d.ts +0 -30
  146. package/dist/observability/analytics.js +0 -118
  147. package/dist/pipe/anthropic-client.d.ts +0 -9
  148. package/dist/pipe/anthropic-client.js +0 -50
  149. package/dist/pipe/brain.d.ts +0 -16
  150. package/dist/pipe/brain.js +0 -89
  151. package/dist/pipe/memory-store.d.ts +0 -8
  152. package/dist/pipe/memory-store.js +0 -39
  153. package/dist/pipe/project-scanner.d.ts +0 -6
  154. package/dist/pipe/project-scanner.js +0 -26
  155. package/dist/pipe/types.d.ts +0 -34
  156. package/dist/pipe/types.js +0 -1
  157. package/dist/projects/index.d.ts +0 -4
  158. package/dist/projects/index.js +0 -2
  159. package/dist/projects/manager.d.ts +0 -13
  160. package/dist/projects/manager.js +0 -166
  161. package/dist/projects/router.d.ts +0 -17
  162. package/dist/projects/router.js +0 -246
  163. package/dist/projects/types.d.ts +0 -19
  164. package/dist/projects/types.js +0 -1
  165. package/dist/service/install.d.ts +0 -4
  166. package/dist/service/install.js +0 -146
  167. package/dist/service/templates.d.ts +0 -4
  168. package/dist/service/templates.js +0 -52
  169. package/dist/service/windows.d.ts +0 -8
  170. package/dist/service/windows.js +0 -59
  171. package/dist/session/budget-guard.d.ts +0 -20
  172. package/dist/session/budget-guard.js +0 -31
  173. package/dist/session/circuit-breaker.d.ts +0 -19
  174. package/dist/session/circuit-breaker.js +0 -91
  175. package/dist/session/context-compactor.d.ts +0 -32
  176. package/dist/session/context-compactor.js +0 -45
  177. package/dist/session/context-monitor.d.ts +0 -15
  178. package/dist/session/context-monitor.js +0 -44
  179. package/dist/session/handoff.d.ts +0 -21
  180. package/dist/session/handoff.js +0 -50
  181. package/dist/session/manager.d.ts +0 -60
  182. package/dist/session/manager.js +0 -416
  183. package/dist/session/memory-store.d.ts +0 -29
  184. package/dist/session/memory-store.js +0 -45
  185. package/dist/session/message-queue.d.ts +0 -28
  186. package/dist/session/message-queue.js +0 -52
  187. package/dist/session/pending-dispatcher.d.ts +0 -31
  188. package/dist/session/pending-dispatcher.js +0 -120
  189. package/dist/session/pending-store.d.ts +0 -60
  190. package/dist/session/pending-store.js +0 -118
  191. package/dist/session/stale-session.d.ts +0 -31
  192. package/dist/session/stale-session.js +0 -45
  193. package/dist/session/subprocess.d.ts +0 -32
  194. package/dist/session/subprocess.js +0 -230
  195. package/dist/session/tab-store.d.ts +0 -28
  196. package/dist/session/tab-store.js +0 -78
  197. package/dist/session/tool-classifier.d.ts +0 -4
  198. package/dist/session/tool-classifier.js +0 -56
  199. package/dist/tasks/scheduler.d.ts +0 -45
  200. package/dist/tasks/scheduler.js +0 -329
  201. package/dist/tasks/store.d.ts +0 -11
  202. package/dist/tasks/store.js +0 -103
  203. package/dist/timeline/index.d.ts +0 -3
  204. package/dist/timeline/index.js +0 -2
  205. package/dist/timeline/logger.d.ts +0 -8
  206. package/dist/timeline/logger.js +0 -11
  207. package/dist/timeline/query.d.ts +0 -11
  208. package/dist/timeline/query.js +0 -67
  209. package/dist/timeline/types.d.ts +0 -12
  210. package/dist/timeline/types.js +0 -1
  211. package/dist/types.d.ts +0 -225
  212. package/dist/types.js +0 -3
  213. package/dist/users/index.d.ts +0 -2
  214. package/dist/users/index.js +0 -1
  215. package/dist/users/service.d.ts +0 -17
  216. package/dist/users/service.js +0 -46
  217. package/dist/util/auto-heal.d.ts +0 -43
  218. package/dist/util/auto-heal.js +0 -107
  219. package/dist/util/install-info.d.ts +0 -37
  220. package/dist/util/install-info.js +0 -80
  221. package/dist/util/logger.d.ts +0 -18
  222. package/dist/util/logger.js +0 -118
  223. package/dist/util/paths.d.ts +0 -13
  224. package/dist/util/paths.js +0 -55
  225. package/dist/util/platform.d.ts +0 -2
  226. package/dist/util/platform.js +0 -11
  227. package/dist/util/progress.d.ts +0 -34
  228. package/dist/util/progress.js +0 -133
  229. package/dist/util/rate-limiter.d.ts +0 -14
  230. package/dist/util/rate-limiter.js +0 -50
  231. package/dist/util/retry.d.ts +0 -2
  232. package/dist/util/retry.js +0 -18
  233. package/dist/util/text.d.ts +0 -32
  234. package/dist/util/text.js +0 -101
  235. package/dist/version.d.ts +0 -1
  236. package/dist/version.js +0 -4
  237. package/dist/voice/index.d.ts +0 -17
  238. package/dist/voice/index.js +0 -42
  239. package/dist/voice/stt.d.ts +0 -16
  240. package/dist/voice/stt.js +0 -57
  241. package/dist/voice/tts.d.ts +0 -23
  242. package/dist/voice/tts.js +0 -84
  243. package/dist/watchers/evaluator.d.ts +0 -5
  244. package/dist/watchers/evaluator.js +0 -45
  245. package/dist/watchers/index.d.ts +0 -4
  246. package/dist/watchers/index.js +0 -3
  247. package/dist/watchers/scheduler.d.ts +0 -20
  248. package/dist/watchers/scheduler.js +0 -183
  249. package/dist/watchers/store.d.ts +0 -10
  250. package/dist/watchers/store.js +0 -56
  251. package/dist/watchers/types.d.ts +0 -15
  252. package/dist/watchers/types.js +0 -1
  253. package/scripts/postinstall.mjs +0 -50
@@ -1,849 +0,0 @@
1
- export function getDashboardHtml(token) {
2
- return `<!DOCTYPE html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Beecork Dashboard</title>
8
- <style>
9
- /* Compiled Tailwind CSS (v4.2.2) — generated via: npx tailwindcss --content src/dashboard/html.ts --minify */
10
- /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-900:oklch(39.6% .141 25.723);--color-green-200:oklch(92.5% .084 155.995);--color-green-400:oklch(79.2% .209 151.711);--color-green-900:oklch(39.3% .095 152.535);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-900:oklch(37.9% .146 265.522);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-lg:32rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-honey-400:#facc15;--color-honey-500:#eab308;--color-honey-600:#ca8a04;--color-bee-900:#111;--color-bee-850:#181818;--color-bee-800:#1e1e1e;--color-bee-750:#252525;--color-bee-700:#2e2e2e}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-48{height:calc(var(--spacing) * 48)}.h-\[var\(--header-height\)\]{height:var(--header-height)}.h-full{height:100%}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-bee-700{border-color:var(--color-bee-700)}.border-honey-500{border-color:var(--color-honey-500)}.border-transparent{border-color:#0000}.bg-bee-800{background-color:var(--color-bee-800)}.bg-bee-850{background-color:var(--color-bee-850)}.bg-bee-900{background-color:var(--color-bee-900)}.bg-blue-900\/50{background-color:#1c398e80}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/50{background-color:color-mix(in oklab, var(--color-blue-900) 50%, transparent)}}.bg-green-900\/90{background-color:#0d542be6}@supports (color:color-mix(in lab, red, red)){.bg-green-900\/90{background-color:color-mix(in oklab, var(--color-green-900) 90%, transparent)}}.bg-honey-600{background-color:var(--color-honey-600)}.bg-purple-900\/50{background-color:#59168b80}@supports (color:color-mix(in lab, red, red)){.bg-purple-900\/50{background-color:color-mix(in oklab, var(--color-purple-900) 50%, transparent)}}.bg-red-900\/90{background-color:#82181ae6}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/90{background-color:color-mix(in oklab, var(--color-red-900) 90%, transparent)}}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-16{padding-block:calc(var(--spacing) * 16)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-black{color:var(--color-black)}.text-blue-400{color:var(--color-blue-400)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-green-200{color:var(--color-green-200)}.text-green-400{color:var(--color-green-400)}.text-honey-400{color:var(--color-honey-400)}.text-purple-400{color:var(--color-purple-400)}.text-red-200{color:var(--color-red-200)}.text-red-400{color:var(--color-red-400)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.opacity-0{opacity:0}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-bee-750:hover{background-color:var(--color-bee-750)}.hover\:bg-honey-500:hover{background-color:var(--color-honey-500)}.hover\:text-white:hover{color:var(--color-white)}}@media (min-width:40rem){.sm\:block{display:block}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-auto{width:auto}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
11
-
12
- :root {
13
- --sidebar-width: 280px;
14
- --header-height: 52px;
15
- --input-height: 64px;
16
- }
17
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; overflow: hidden; }
18
- .msg-user { background: #252525; border-left: 3px solid #facc15; }
19
- .msg-assistant { background: #1a1a1a; border-left: 3px solid #555; }
20
- .tab-item { transition: background 0.15s; }
21
- .tab-item:hover { background: #2e2e2e; }
22
- .tab-item.active { background: rgba(250,204,21,0.08); border-left: 3px solid #facc15; }
23
- .tab-item:not(.active) { border-left: 3px solid transparent; }
24
- .cost-bar { background: linear-gradient(to top, #facc15, #eab308); border-radius: 2px 2px 0 0; }
25
- .panel-card { background: #1e1e1e; border: 1px solid #2e2e2e; border-radius: 8px; }
26
- .btn-primary { background: #eab308; color: #111; font-weight: 600; border: none; border-radius: 6px; cursor: pointer; transition: background 0.15s; }
27
- .btn-primary:hover { background: #facc15; }
28
- .btn-danger { background: #7f1d1d; color: #fca5a5; font-weight: 500; border: none; border-radius: 6px; cursor: pointer; transition: background 0.15s; }
29
- .btn-danger:hover { background: #991b1b; }
30
- .btn-ghost { background: transparent; color: #9ca3af; border: 1px solid #3a3a3a; border-radius: 6px; cursor: pointer; transition: all 0.15s; }
31
- .btn-ghost:hover { background: #2e2e2e; color: #e5e7eb; }
32
- .input-field { background: #111; border: 1px solid #3a3a3a; border-radius: 6px; color: #e5e7eb; outline: none; transition: border-color 0.15s; }
33
- .input-field:focus { border-color: #eab308; }
34
- .input-field::placeholder { color: #555; }
35
- .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
36
- .status-running { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.4); }
37
- .status-idle { background: #6b7280; }
38
- .status-error { background: #ef4444; }
39
- .status-stopped { background: #4b5563; }
40
- .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: flex; align-items: center; justify-content: center; }
41
- .modal-content { background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 12px; padding: 24px; min-width: 400px; max-width: 500px; }
42
- .sse-indicator { width: 6px; height: 6px; border-radius: 50%; }
43
- .sse-connected { background: #22c55e; animation: pulse-green 2s infinite; }
44
- .sse-disconnected { background: #ef4444; }
45
- @keyframes pulse-green { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
46
- ::-webkit-scrollbar { width: 6px; }
47
- ::-webkit-scrollbar-track { background: #111; }
48
- ::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 3px; }
49
- ::-webkit-scrollbar-thumb:hover { background: #555; }
50
-
51
- @media (max-width: 768px) {
52
- :root { --sidebar-width: 100%; }
53
- .layout { flex-direction: column; }
54
- .sidebar { width: 100% !important; max-height: 40vh; border-right: none !important; border-bottom: 1px solid #2e2e2e; }
55
- .main-area { width: 100% !important; }
56
- }
57
- </style>
58
- </head>
59
- <body class="bg-bee-900 text-gray-200">
60
-
61
- <!-- Header -->
62
- <header class="h-[var(--header-height)] bg-bee-800 border-b border-bee-700 px-4 flex items-center justify-between shrink-0" style="height:var(--header-height)">
63
- <div class="flex items-center gap-3">
64
- <span class="text-xl">&#x1F41D;</span>
65
- <h1 class="text-base font-semibold text-white">Beecork</h1>
66
- <span id="version" class="text-xs text-gray-500 font-mono"></span>
67
- </div>
68
- <div class="flex items-center gap-4">
69
- <div class="flex items-center gap-2">
70
- <span id="sse-dot" class="sse-indicator sse-disconnected"></span>
71
- <span id="sse-label" class="text-xs text-gray-500">connecting</span>
72
- </div>
73
- <div class="flex items-center gap-2">
74
- <span id="daemon-dot" class="status-dot status-idle"></span>
75
- <span id="daemon-status" class="text-xs text-gray-400">checking...</span>
76
- </div>
77
- <div class="text-xs text-gray-500 font-mono hidden sm:block" id="stats"></div>
78
- </div>
79
- </header>
80
-
81
- <!-- Navigation -->
82
- <nav class="bg-bee-800 border-b border-bee-700 px-4 shrink-0">
83
- <div class="flex gap-1">
84
- <button onclick="showPanel('tabs')" data-panel="tabs" class="nav-btn px-4 py-2 text-sm border-b-2 border-honey-500 text-white">Tabs</button>
85
- <button onclick="showPanel('memories')" data-panel="memories" class="nav-btn px-4 py-2 text-sm border-b-2 border-transparent text-gray-400 hover:text-white">Memories</button>
86
- <button onclick="showPanel('crons')" data-panel="crons" class="nav-btn px-4 py-2 text-sm border-b-2 border-transparent text-gray-400 hover:text-white">Tasks</button>
87
- <button onclick="showPanel('watchers')" data-panel="watchers" class="nav-btn px-4 py-2 text-sm border-b-2 border-transparent text-gray-400 hover:text-white">Watchers</button>
88
- <button onclick="showPanel('costs')" data-panel="costs" class="nav-btn px-4 py-2 text-sm border-b-2 border-transparent text-gray-400 hover:text-white">Costs</button>
89
- <button onclick="showPanel('update')" data-panel="update" class="nav-btn px-4 py-2 text-sm border-b-2 border-transparent text-gray-400 hover:text-white">Update</button>
90
- </div>
91
- </nav>
92
-
93
- <!-- Main content area -->
94
- <div id="app" style="height: calc(100vh - var(--header-height) - 41px); overflow: hidden;">
95
-
96
- <!-- Tabs Panel -->
97
- <div id="panel-tabs" class="panel h-full">
98
- <div class="layout flex h-full">
99
- <!-- Sidebar: tab list -->
100
- <div class="sidebar bg-bee-850 border-r border-bee-700 flex flex-col" style="width: var(--sidebar-width); min-width: var(--sidebar-width);">
101
- <div class="px-3 py-2 border-b border-bee-700 flex items-center justify-between shrink-0">
102
- <div class="flex items-center gap-2">
103
- <span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Tabs</span>
104
- <span id="tab-count" class="text-xs text-gray-600"></span>
105
- </div>
106
- <button onclick="showCreateTabModal()" class="btn-ghost px-2 py-1 text-xs">+ New</button>
107
- </div>
108
- <div id="tab-list" class="flex-1 overflow-y-auto"></div>
109
- </div>
110
- <!-- Main: messages + input -->
111
- <div class="main-area flex flex-col flex-1 min-w-0">
112
- <div class="px-4 py-2 border-b border-bee-700 flex items-center justify-between shrink-0 bg-bee-800">
113
- <div class="flex items-center gap-2">
114
- <h2 class="text-sm font-semibold text-gray-300" id="msg-title">Select a tab</h2>
115
- <span id="msg-tab-status" class="text-xs text-gray-500"></span>
116
- </div>
117
- <div class="flex items-center gap-3">
118
- <span id="msg-count" class="text-xs text-gray-500"></span>
119
- <button id="btn-delete-tab" class="btn-danger px-2 py-1 text-xs hidden" onclick="deleteSelectedTab()">Delete</button>
120
- </div>
121
- </div>
122
- <div id="msg-list" class="flex-1 overflow-y-auto p-4 space-y-3 bg-bee-900">
123
- <p class="text-gray-500 text-sm text-center py-16">Select a tab to view messages</p>
124
- </div>
125
- <!-- Message input -->
126
- <div id="msg-input-area" class="hidden shrink-0 border-t border-bee-700 bg-bee-800 p-3">
127
- <form id="send-form" onsubmit="sendMessage(event)" class="flex gap-2">
128
- <label for="msg-input" class="sr-only">Message</label>
129
- <input id="msg-input" type="text" placeholder="Send a message to this tab..."
130
- class="input-field flex-1 px-3 py-2 text-sm" autocomplete="off" aria-label="Message">
131
- <button type="submit" class="btn-primary px-4 py-2 text-sm">Send</button>
132
- </form>
133
- </div>
134
- </div>
135
- </div>
136
- </div>
137
-
138
- <!-- Memories Panel -->
139
- <div id="panel-memories" class="panel hidden h-full overflow-y-auto p-4">
140
- <div class="panel-card overflow-hidden">
141
- <div class="px-4 py-3 border-b border-bee-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
142
- <h2 class="text-sm font-semibold text-gray-300">Memories</h2>
143
- <div class="flex items-center gap-3 w-full sm:w-auto">
144
- <label for="memory-search" class="sr-only">Search memories</label>
145
- <input id="memory-search" type="text" placeholder="Search..."
146
- class="input-field px-3 py-1.5 text-sm flex-1 sm:w-48" oninput="debounceMemorySearch()" aria-label="Search memories">
147
- <button onclick="showCreateMemoryModal()" class="btn-ghost px-2 py-1 text-xs whitespace-nowrap">+ Add</button>
148
- <span id="memory-count" class="text-xs text-gray-500 whitespace-nowrap"></span>
149
- </div>
150
- </div>
151
- <div id="memory-list" class="max-h-[calc(100vh-240px)] overflow-y-auto"></div>
152
- </div>
153
- </div>
154
-
155
- <!-- Tasks Panel -->
156
- <div id="panel-crons" class="panel hidden h-full overflow-y-auto p-4">
157
- <div class="panel-card overflow-hidden">
158
- <div class="px-4 py-3 border-b border-bee-700 flex items-center justify-between">
159
- <h2 class="text-sm font-semibold text-gray-300">Tasks</h2>
160
- <button onclick="showCreateCronModal()" class="btn-ghost px-2 py-1 text-xs">+ Create</button>
161
- </div>
162
- <div id="cron-list" class="max-h-[calc(100vh-240px)] overflow-y-auto"></div>
163
- </div>
164
- </div>
165
-
166
- <!-- Watchers Panel -->
167
- <div id="panel-watchers" class="panel hidden h-full overflow-y-auto p-4">
168
- <div class="panel-card overflow-hidden">
169
- <div class="px-4 py-3 border-b border-bee-700 flex items-center justify-between">
170
- <h2 class="text-sm font-semibold text-gray-300">Watchers</h2>
171
- </div>
172
- <div id="watcher-list" class="max-h-[calc(100vh-240px)] overflow-y-auto"></div>
173
- </div>
174
- </div>
175
-
176
- <!-- Costs Panel -->
177
- <div id="panel-costs" class="panel hidden h-full overflow-y-auto p-4">
178
- <div class="panel-card overflow-hidden">
179
- <div class="px-4 py-3 border-b border-bee-700 flex items-center justify-between">
180
- <h2 class="text-sm font-semibold text-gray-300">API Costs (Last 30 Days)</h2>
181
- <span id="total-cost" class="text-sm font-mono text-honey-400"></span>
182
- </div>
183
- <div id="cost-chart" class="p-6"></div>
184
- </div>
185
- </div>
186
-
187
- <!-- Update Panel — must live inside #app so it inherits the height/scroll setup -->
188
- <div id="panel-update" class="panel hidden h-full overflow-y-auto p-4">
189
- <div class="max-w-lg space-y-3">
190
- <div id="update-packages" class="space-y-3 text-sm text-gray-400">Checking for updates...</div>
191
- <div id="update-log" class="hidden bg-bee-900 border border-bee-700 rounded p-3 text-xs font-mono text-gray-300 whitespace-pre-wrap max-h-48 overflow-y-auto"></div>
192
- </div>
193
- </div>
194
- </div>
195
-
196
- <!-- Modal container -->
197
- <div id="modal" class="hidden"></div>
198
-
199
- <script>
200
- const API_TOKEN = ${JSON.stringify(token)};
201
-
202
- // Scrub the auth token out of the URL bar once the cookie is set. The token
203
- // is required for first-load but lingers in browser history / referrer-able
204
- // copies of the URL otherwise. Referrer-Policy: no-referrer prevents the
205
- // worst case; this is defense-in-depth.
206
- if (location.search.includes('token=')) {
207
- try { history.replaceState(null, '', location.pathname); } catch {}
208
- }
209
-
210
- // State
211
- let selectedTab = null;
212
- let memorySearchTimer = null;
213
- let tabsData = [];
214
- // Track last-seen message count per tab so the background refresh can skip
215
- // the DOM rebuild entirely when nothing changed (M23 — fixes scroll jumping).
216
- const lastMessageTotals = new Map();
217
-
218
- // Toast notifications
219
- function showToast(msg, isError) {
220
- const toast = document.createElement('div');
221
- toast.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-lg text-sm z-50 ' + (isError ? 'bg-red-900/90 text-red-200' : 'bg-green-900/90 text-green-200');
222
- toast.textContent = msg;
223
- document.body.appendChild(toast);
224
- setTimeout(() => toast.remove(), 4000);
225
- }
226
-
227
- // API helpers
228
- async function api(path, opts) {
229
- const headers = { 'Authorization': 'Bearer ' + API_TOKEN };
230
- if (opts && opts.body) headers['Content-Type'] = 'application/json';
231
- try {
232
- const res = await fetch(path, { headers, ...opts });
233
- if (!res.ok) {
234
- const err = await res.json().catch(() => ({ error: res.statusText }));
235
- throw new Error(err.error || 'Request failed');
236
- }
237
- return res.json();
238
- } catch (err) {
239
- showToast('API error: ' + err.message, true);
240
- throw err;
241
- }
242
- }
243
-
244
- function timeAgo(iso) {
245
- if (!iso) return 'never';
246
- const diff = Date.now() - new Date(iso).getTime();
247
- const mins = Math.floor(diff / 60000);
248
- if (mins < 1) return 'just now';
249
- if (mins < 60) return mins + 'm ago';
250
- const hours = Math.floor(mins / 60);
251
- if (hours < 24) return hours + 'h ago';
252
- return Math.floor(hours / 24) + 'd ago';
253
- }
254
-
255
- function esc(s) {
256
- if (!s) return '';
257
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
258
- }
259
-
260
- // --- SSE ---
261
- function connectSSE() {
262
- const es = new EventSource('/api/events');
263
- es.onopen = () => {
264
- document.getElementById('sse-dot').className = 'sse-indicator sse-connected';
265
- document.getElementById('sse-label').textContent = 'live';
266
- };
267
- es.onmessage = (e) => {
268
- try {
269
- const data = JSON.parse(e.data);
270
- if (data.type === 'update') {
271
- updateTabStatuses(data.tabs);
272
- }
273
- } catch {}
274
- };
275
- es.onerror = () => {
276
- document.getElementById('sse-dot').className = 'sse-indicator sse-disconnected';
277
- document.getElementById('sse-label').textContent = 'reconnecting';
278
- };
279
- }
280
-
281
- function updateTabStatuses(sseTabs) {
282
- // Update status indicators in the sidebar without full reload
283
- if (!sseTabs) return;
284
- sseTabs.forEach(st => {
285
- const el = document.querySelector('[data-tab-name="' + CSS.escape(st.name) + '"] .tab-status-dot');
286
- if (el) {
287
- el.className = 'status-dot tab-status-dot status-' + st.status;
288
- }
289
- const label = document.querySelector('[data-tab-name="' + CSS.escape(st.name) + '"] .tab-status-label');
290
- if (label) {
291
- label.textContent = st.status;
292
- }
293
- });
294
- }
295
-
296
- // --- Panel switching ---
297
- function showPanel(name) {
298
- document.querySelectorAll('.panel').forEach(p => p.classList.add('hidden'));
299
- document.getElementById('panel-' + name).classList.remove('hidden');
300
- document.querySelectorAll('.nav-btn').forEach(b => {
301
- b.classList.remove('border-honey-500', 'text-white');
302
- b.classList.add('border-transparent', 'text-gray-400');
303
- });
304
- const btn = document.querySelector('[data-panel="' + name + '"]');
305
- btn.classList.add('border-honey-500', 'text-white');
306
- btn.classList.remove('border-transparent', 'text-gray-400');
307
-
308
- if (name === 'memories') loadMemories();
309
- if (name === 'crons') loadCrons();
310
- if (name === 'watchers') loadWatchers();
311
- if (name === 'costs') loadCosts();
312
- if (name === 'update') loadUpdateStatus();
313
- }
314
-
315
- // --- Status ---
316
- async function loadStatus() {
317
- try {
318
- const s = await api('/api/status');
319
- document.getElementById('version').textContent = 'v' + s.version;
320
- const dot = document.getElementById('daemon-dot');
321
- const status = document.getElementById('daemon-status');
322
- if (s.daemonPid) {
323
- dot.className = 'status-dot status-running';
324
- status.textContent = 'PID ' + s.daemonPid;
325
- } else {
326
- dot.className = 'status-dot status-error';
327
- status.textContent = 'stopped';
328
- }
329
- // Server returns both new "tasks" and legacy "cronJobs" — prefer the new one.
330
- const taskCount = s.tasks ?? s.cronJobs ?? 0;
331
- document.getElementById('stats').textContent =
332
- s.tabs + ' tabs | ' + taskCount + ' tasks | ' + s.memories + ' mem';
333
- } catch {}
334
- }
335
-
336
- // --- Tabs ---
337
- async function loadTabs() {
338
- try { tabsData = await api('/api/tabs'); }
339
- catch (err) {
340
- const list = document.getElementById('tab-list');
341
- if (list) list.innerHTML = '<p class="text-red-400 text-xs text-center py-8">Failed to load tabs (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
342
- return;
343
- }
344
- const list = document.getElementById('tab-list');
345
- document.getElementById('tab-count').textContent = tabsData.length.toString();
346
-
347
- if (tabsData.length === 0) {
348
- list.innerHTML = '<p class="text-gray-600 text-xs text-center py-8">No tabs yet</p>';
349
- return;
350
- }
351
-
352
- list.innerHTML = tabsData.map(t => {
353
- const isActive = selectedTab === t.name ? ' active' : '';
354
- const cost = t.total_cost > 0 ? '$' + t.total_cost.toFixed(4) : '';
355
- // esc() already replaces ' with &#39;, and tab names are regex-validated
356
- // to [a-zA-Z0-9-] anyway, so the previous chained .replace(/'/g, ...) was
357
- // dead code. Drop it for clarity.
358
- return '<div class="tab-item px-3 py-2.5 cursor-pointer' + isActive + '" data-tab-name="' + esc(t.name) + '" role="button" tabindex="0" onclick="selectTab(\\'' + esc(t.name) + '\\')" onkeydown="if(event.key===\\'Enter\\')selectTab(\\'' + esc(t.name) + '\\')">' +
359
- '<div class="flex items-center justify-between">' +
360
- '<div class="flex items-center gap-2 min-w-0">' +
361
- '<span class="status-dot tab-status-dot status-' + esc(t.status) + '"></span>' +
362
- '<span class="text-sm font-medium text-gray-200 truncate">' + esc(t.name) + '</span>' +
363
- '</div>' +
364
- '<span class="text-xs tab-status-label text-gray-500 shrink-0">' + esc(t.status) + '</span>' +
365
- '</div>' +
366
- '<div class="flex items-center justify-between mt-0.5 pl-5">' +
367
- '<span class="text-xs text-gray-600">' + t.message_count + ' msgs' + (cost ? ' &middot; ' + cost : '') + '</span>' +
368
- '<span class="text-xs text-gray-700">' + timeAgo(t.last_activity_at) + '</span>' +
369
- '</div>' +
370
- '</div>';
371
- }).join('');
372
- }
373
-
374
- async function selectTab(name, opts) {
375
- const fromUser = !opts || opts.fromUser !== false;
376
- selectedTab = name;
377
- document.getElementById('msg-title').textContent = name;
378
- document.getElementById('btn-delete-tab').classList.remove('hidden');
379
- document.getElementById('msg-input-area').classList.remove('hidden');
380
-
381
- // Update active tab highlight
382
- document.querySelectorAll('.tab-item').forEach(el => {
383
- el.classList.toggle('active', el.getAttribute('data-tab-name') === name);
384
- });
385
-
386
- const tab = tabsData.find(t => t.name === name);
387
- if (tab) {
388
- document.getElementById('msg-tab-status').textContent = tab.status;
389
- }
390
-
391
- let data; try { data = await api('/api/tabs/' + encodeURIComponent(name) + '/messages?limit=100'); } catch (err) {
392
- const list = document.getElementById('msg-list');
393
- if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load messages (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
394
- return;
395
- }
396
- const list = document.getElementById('msg-list');
397
- document.getElementById('msg-count').textContent = data.total + ' messages';
398
-
399
- // Skip the full DOM rebuild when nothing changed — preserves user's scroll
400
- // position, expanded <details> blocks, text selection. Without this guard
401
- // the 8s background refresh kept teleporting the user to the top of the
402
- // message list every tick.
403
- const prevTotal = lastMessageTotals.get(name);
404
- if (!fromUser && prevTotal === data.total) return;
405
- lastMessageTotals.set(name, data.total);
406
-
407
- if (data.messages.length === 0) {
408
- list.innerHTML = '<p class="text-gray-600 text-sm text-center py-16">No messages in this tab</p>';
409
- return;
410
- }
411
-
412
- // Capture scroll position relative to the bottom so we can restore after
413
- // the rewrite. distanceFromBottom = scrollHeight - scrollTop - clientHeight
414
- const wasNearBottom = !fromUser && (list.scrollHeight - list.scrollTop - list.clientHeight < 60);
415
- const distanceFromBottom = list.scrollHeight - list.scrollTop;
416
-
417
- list.innerHTML = data.messages.map(m => {
418
- const cls = m.role === 'user' ? 'msg-user' : 'msg-assistant';
419
- const label = m.role === 'user' ? 'You' : 'Claude';
420
- const meta = [];
421
- if (m.cost_usd > 0) meta.push('$' + m.cost_usd.toFixed(4));
422
- if (m.tokens_in) meta.push(m.tokens_in.toLocaleString() + ' in');
423
- if (m.tokens_out) meta.push(m.tokens_out.toLocaleString() + ' out');
424
- const metaStr = meta.length ? '<span class="text-xs text-gray-600 ml-2">' + meta.join(' | ') + '</span>' : '';
425
- const truncated = m.content.length > 2000;
426
- const preview = truncated ? m.content.slice(0, 2000) : m.content;
427
- const rest = truncated ? m.content.slice(2000) : '';
428
- const body = truncated
429
- ? esc(preview) + '<details class="mt-1"><summary class="text-xs text-honey-400 cursor-pointer">Show ' + (m.content.length - 2000).toLocaleString() + ' more chars</summary>' + esc(rest) + '</details>'
430
- : esc(preview);
431
-
432
- return '<div class="' + cls + ' rounded-lg p-3">' +
433
- '<div class="flex items-center justify-between mb-1">' +
434
- '<span class="text-xs font-semibold ' + (m.role === 'user' ? 'text-honey-400' : 'text-gray-400') + '">' + label + metaStr + '</span>' +
435
- '<span class="text-xs text-gray-600">' + timeAgo(m.created_at) + '</span>' +
436
- '</div>' +
437
- '<pre class="text-sm text-gray-300 whitespace-pre-wrap break-words font-sans leading-relaxed">' + body + '</pre>' +
438
- '</div>';
439
- }).join('');
440
-
441
- // Only jump to bottom + steal focus on user-initiated selection,
442
- // not on the 8s background refresh, so typing isn't interrupted.
443
- if (fromUser) {
444
- list.scrollTop = list.scrollHeight;
445
- document.getElementById('msg-input').focus();
446
- } else if (wasNearBottom) {
447
- // User was reading the latest messages — keep them pinned to the bottom.
448
- list.scrollTop = list.scrollHeight;
449
- } else {
450
- // User was scrolled up reading older messages — restore their position
451
- // relative to the bottom so new messages don't yank them around.
452
- list.scrollTop = list.scrollHeight - distanceFromBottom;
453
- }
454
- }
455
-
456
- // --- Send message ---
457
- async function sendMessage(e) {
458
- e.preventDefault();
459
- const input = document.getElementById('msg-input');
460
- const message = input.value.trim();
461
- if (!message || !selectedTab) return;
462
-
463
- input.value = '';
464
- input.disabled = true;
465
-
466
- try {
467
- await api('/api/tabs/' + encodeURIComponent(selectedTab) + '/send', {
468
- method: 'POST',
469
- body: JSON.stringify({ message })
470
- });
471
- // Append optimistic message to UI
472
- const list = document.getElementById('msg-list');
473
- list.innerHTML += '<div class="msg-user rounded-lg p-3">' +
474
- '<div class="flex items-center justify-between mb-1">' +
475
- '<span class="text-xs font-semibold text-honey-400">You (queued)</span>' +
476
- '<span class="text-xs text-gray-600">just now</span>' +
477
- '</div>' +
478
- '<pre class="text-sm text-gray-300 whitespace-pre-wrap break-words font-sans leading-relaxed">' + esc(message) + '</pre>' +
479
- '</div>';
480
- list.scrollTop = list.scrollHeight;
481
- } catch (err) {
482
- alert('Failed to send: ' + err.message);
483
- }
484
- input.disabled = false;
485
- input.focus();
486
- }
487
-
488
- // --- Delete tab ---
489
- async function deleteSelectedTab() {
490
- if (!selectedTab) return;
491
- if (!confirm('Delete tab "' + selectedTab + '" and all its messages?')) return;
492
- try { await api('/api/tabs/' + encodeURIComponent(selectedTab), { method: 'DELETE' }); } catch { return; }
493
- selectedTab = null;
494
- document.getElementById('msg-title').textContent = 'Select a tab';
495
- document.getElementById('msg-tab-status').textContent = '';
496
- document.getElementById('msg-count').textContent = '';
497
- document.getElementById('msg-list').innerHTML = '<p class="text-gray-600 text-sm text-center py-16">Select a tab to view messages</p>';
498
- document.getElementById('btn-delete-tab').classList.add('hidden');
499
- document.getElementById('msg-input-area').classList.add('hidden');
500
- loadTabs();
501
- }
502
-
503
- // --- Create tab modal ---
504
- function showCreateTabModal() {
505
- document.getElementById('modal').innerHTML = '<div class="modal-overlay" onclick="closeModal(event)">' +
506
- '<div class="modal-content" onclick="event.stopPropagation()">' +
507
- '<h3 class="text-base font-semibold text-white mb-4">Create Tab</h3>' +
508
- '<div class="space-y-3">' +
509
- '<div><label class="text-xs text-gray-400 mb-1 block">Name *</label>' +
510
- '<input id="modal-tab-name" class="input-field w-full px-3 py-2 text-sm" placeholder="my-tab"></div>' +
511
- '<div><label class="text-xs text-gray-400 mb-1 block">Working Directory</label>' +
512
- '<input id="modal-tab-dir" class="input-field w-full px-3 py-2 text-sm" placeholder="~"></div>' +
513
- '<div><label class="text-xs text-gray-400 mb-1 block">System Prompt</label>' +
514
- '<textarea id="modal-tab-prompt" class="input-field w-full px-3 py-2 text-sm h-20 resize-none" placeholder="Optional"></textarea></div>' +
515
- '</div>' +
516
- '<div class="flex justify-end gap-2 mt-5">' +
517
- '<button class="btn-ghost px-4 py-2 text-sm" onclick="closeModal()">Cancel</button>' +
518
- '<button class="btn-primary px-4 py-2 text-sm" onclick="createTab()">Create</button>' +
519
- '</div>' +
520
- '</div>' +
521
- '</div>';
522
- document.getElementById('modal').classList.remove('hidden');
523
- setTimeout(() => document.getElementById('modal-tab-name').focus(), 100);
524
- }
525
-
526
- async function createTab() {
527
- const name = document.getElementById('modal-tab-name').value.trim();
528
- if (!name) return;
529
- const workingDir = document.getElementById('modal-tab-dir').value.trim() || undefined;
530
- const systemPrompt = document.getElementById('modal-tab-prompt').value.trim() || undefined;
531
- try { await api('/api/tabs', { method: 'POST', body: JSON.stringify({ name, workingDir, systemPrompt }) }); } catch { return; }
532
- closeModal();
533
- loadTabs();
534
- }
535
-
536
- // --- Memories ---
537
- async function loadMemories(query) {
538
- const q = query || document.getElementById('memory-search').value || '';
539
- let data;
540
- try { data = await api('/api/memories?limit=100&q=' + encodeURIComponent(q)); }
541
- catch (err) {
542
- const list = document.getElementById('memory-list');
543
- if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load memories (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
544
- return;
545
- }
546
- const list = document.getElementById('memory-list');
547
- document.getElementById('memory-count').textContent = data.total + ' total';
548
-
549
- if (data.memories.length === 0) {
550
- list.innerHTML = '<p class="text-gray-600 text-sm text-center py-8">No memories' + (q ? ' matching "' + esc(q) + '"' : '') + '</p>';
551
- return;
552
- }
553
-
554
- list.innerHTML = data.memories.map(m => {
555
- const scope = m.tab_name ? 'tab:' + esc(m.tab_name) : 'global';
556
- return '<div class="px-4 py-3 border-b border-bee-700 group hover:bg-bee-750">' +
557
- '<div class="flex items-center justify-between mb-1">' +
558
- '<div class="flex items-center gap-2">' +
559
- '<span class="text-xs px-1.5 py-0.5 rounded ' + (m.source === 'auto' ? 'bg-blue-900/50 text-blue-400' : 'bg-purple-900/50 text-purple-400') + '">' + m.source + '</span>' +
560
- '<span class="text-xs text-gray-600">' + scope + '</span>' +
561
- '</div>' +
562
- '<div class="flex items-center gap-2">' +
563
- '<span class="text-xs text-gray-700">' + timeAgo(m.created_at) + '</span>' +
564
- '<button class="btn-danger px-1.5 py-0.5 text-xs opacity-0 group-hover:opacity-100" aria-label="Delete memory" onclick="deleteMemory(' + m.id + ')">x</button>' +
565
- '</div>' +
566
- '</div>' +
567
- '<p class="text-sm text-gray-300 leading-relaxed">' + esc(m.content) + '</p>' +
568
- '</div>';
569
- }).join('');
570
- }
571
-
572
- function debounceMemorySearch() {
573
- clearTimeout(memorySearchTimer);
574
- memorySearchTimer = setTimeout(() => loadMemories(), 300);
575
- }
576
-
577
- async function deleteMemory(id) {
578
- try { await api('/api/memories/' + id, { method: 'DELETE' }); } catch { return; }
579
- loadMemories();
580
- }
581
-
582
- function showCreateMemoryModal() {
583
- document.getElementById('modal').innerHTML = '<div class="modal-overlay" onclick="closeModal(event)">' +
584
- '<div class="modal-content" onclick="event.stopPropagation()">' +
585
- '<h3 class="text-base font-semibold text-white mb-4">Add Memory</h3>' +
586
- '<div class="space-y-3">' +
587
- '<div><label class="text-xs text-gray-400 mb-1 block">Content *</label>' +
588
- '<textarea id="modal-mem-content" class="input-field w-full px-3 py-2 text-sm h-24 resize-none" placeholder="What to remember..."></textarea></div>' +
589
- '<div><label class="text-xs text-gray-400 mb-1 block">Tab (optional)</label>' +
590
- '<input id="modal-mem-tab" class="input-field w-full px-3 py-2 text-sm" placeholder="global"></div>' +
591
- '</div>' +
592
- '<div class="flex justify-end gap-2 mt-5">' +
593
- '<button class="btn-ghost px-4 py-2 text-sm" onclick="closeModal()">Cancel</button>' +
594
- '<button class="btn-primary px-4 py-2 text-sm" onclick="createMemory()">Save</button>' +
595
- '</div>' +
596
- '</div>' +
597
- '</div>';
598
- document.getElementById('modal').classList.remove('hidden');
599
- setTimeout(() => document.getElementById('modal-mem-content').focus(), 100);
600
- }
601
-
602
- async function createMemory() {
603
- const content = document.getElementById('modal-mem-content').value.trim();
604
- if (!content) return;
605
- const tabName = document.getElementById('modal-mem-tab').value.trim() || undefined;
606
- try { await api('/api/memories', { method: 'POST', body: JSON.stringify({ content, tabName }) }); } catch { return; }
607
- closeModal();
608
- loadMemories();
609
- }
610
-
611
- // --- Tasks (formerly Crons) ---
612
- async function loadCrons() {
613
- let crons;
614
- try { crons = await api('/api/tasks'); }
615
- catch (err) {
616
- const list = document.getElementById('cron-list');
617
- if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load tasks (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
618
- return;
619
- }
620
- const list = document.getElementById('cron-list');
621
-
622
- if (crons.length === 0) {
623
- list.innerHTML = '<p class="text-gray-600 text-sm text-center py-8">No tasks</p>';
624
- return;
625
- }
626
-
627
- list.innerHTML = crons.map(c => {
628
- const enabled = c.enabled === 1;
629
- return '<div class="px-4 py-3 border-b border-bee-700 group hover:bg-bee-750">' +
630
- '<div class="flex items-center justify-between">' +
631
- '<div class="flex items-center gap-2">' +
632
- '<span class="status-dot ' + (enabled ? 'status-running' : 'status-idle') + '"></span>' +
633
- '<span class="text-sm font-medium text-white">' + esc(c.name) + '</span>' +
634
- '</div>' +
635
- '<div class="flex items-center gap-2">' +
636
- '<span class="text-xs font-mono text-gray-400">' + esc(c.schedule_type) + ': ' + esc(c.schedule) + '</span>' +
637
- '<button class="btn-danger px-1.5 py-0.5 text-xs opacity-0 group-hover:opacity-100" aria-label="Delete task" onclick="deleteCron(\\'' + esc(c.id) + '\\')">x</button>' +
638
- '</div>' +
639
- '</div>' +
640
- '<div class="flex items-center justify-between mt-1 pl-5">' +
641
- '<span class="text-xs text-gray-500">tab: ' + esc(c.tab_name) + ' &middot; ' + esc((c.message || '').slice(0, 80)) + '</span>' +
642
- '<span class="text-xs text-gray-700">last: ' + timeAgo(c.last_run_at) + '</span>' +
643
- '</div>' +
644
- '</div>';
645
- }).join('');
646
- }
647
-
648
- async function deleteCron(id) {
649
- if (!confirm('Delete this task?')) return;
650
- try { await api('/api/tasks/' + encodeURIComponent(id), { method: 'DELETE' }); } catch { return; }
651
- loadCrons();
652
- }
653
-
654
- function showCreateCronModal() {
655
- document.getElementById('modal').innerHTML = '<div class="modal-overlay" onclick="closeModal(event)">' +
656
- '<div class="modal-content" onclick="event.stopPropagation()">' +
657
- '<h3 class="text-base font-semibold text-white mb-4">Create Task</h3>' +
658
- '<div class="space-y-3">' +
659
- '<div><label class="text-xs text-gray-400 mb-1 block">Name *</label>' +
660
- '<input id="modal-cron-name" class="input-field w-full px-3 py-2 text-sm" placeholder="daily-report"></div>' +
661
- '<div class="grid grid-cols-2 gap-3">' +
662
- '<div><label class="text-xs text-gray-400 mb-1 block">Schedule Type</label>' +
663
- '<select id="modal-cron-type" class="input-field w-full px-3 py-2 text-sm"><option value="every">every</option><option value="cron">cron</option></select></div>' +
664
- '<div><label class="text-xs text-gray-400 mb-1 block">Schedule *</label>' +
665
- '<input id="modal-cron-schedule" class="input-field w-full px-3 py-2 text-sm" placeholder="30m or */5 * * * *"></div>' +
666
- '</div>' +
667
- '<div><label class="text-xs text-gray-400 mb-1 block">Tab Name</label>' +
668
- '<input id="modal-cron-tab" class="input-field w-full px-3 py-2 text-sm" placeholder="default"></div>' +
669
- '<div><label class="text-xs text-gray-400 mb-1 block">Message *</label>' +
670
- '<textarea id="modal-cron-msg" class="input-field w-full px-3 py-2 text-sm h-20 resize-none" placeholder="What to tell the tab..."></textarea></div>' +
671
- '</div>' +
672
- '<div class="flex justify-end gap-2 mt-5">' +
673
- '<button class="btn-ghost px-4 py-2 text-sm" onclick="closeModal()">Cancel</button>' +
674
- '<button class="btn-primary px-4 py-2 text-sm" onclick="createCron()">Create</button>' +
675
- '</div>' +
676
- '</div>' +
677
- '</div>';
678
- document.getElementById('modal').classList.remove('hidden');
679
- setTimeout(() => document.getElementById('modal-cron-name').focus(), 100);
680
- }
681
-
682
- async function createCron() {
683
- const name = document.getElementById('modal-cron-name').value.trim();
684
- const scheduleType = document.getElementById('modal-cron-type').value;
685
- const schedule = document.getElementById('modal-cron-schedule').value.trim();
686
- const tabName = document.getElementById('modal-cron-tab').value.trim() || 'default';
687
- const message = document.getElementById('modal-cron-msg').value.trim();
688
- if (!name || !schedule || !message) return;
689
- try { await api('/api/tasks', { method: 'POST', body: JSON.stringify({ name, scheduleType, schedule, tabName, message }) }); } catch { return; }
690
- closeModal();
691
- loadCrons();
692
- }
693
-
694
- // --- Watchers ---
695
- async function loadWatchers() {
696
- let watchers;
697
- try { watchers = await api('/api/watchers'); }
698
- catch (err) {
699
- const list = document.getElementById('watcher-list');
700
- if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load watchers (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
701
- return;
702
- }
703
- const list = document.getElementById('watcher-list');
704
-
705
- if (watchers.length === 0) {
706
- list.innerHTML = '<p class="text-gray-600 text-sm text-center py-8">No watchers configured</p>';
707
- return;
708
- }
709
-
710
- list.innerHTML = watchers.map(w => {
711
- const enabled = w.enabled === 1;
712
- return '<div class="px-4 py-3 border-b border-bee-700 group hover:bg-bee-750">' +
713
- '<div class="flex items-center justify-between">' +
714
- '<div class="flex items-center gap-2">' +
715
- '<span class="status-dot ' + (enabled ? 'status-running' : 'status-idle') + '"></span>' +
716
- '<span class="text-sm font-medium text-white">' + esc(w.name) + '</span>' +
717
- '</div>' +
718
- '<div class="flex items-center gap-2">' +
719
- '<span class="text-xs font-mono text-gray-400">' + esc(w.schedule) + '</span>' +
720
- '<span class="text-xs text-gray-500">action: ' + esc(w.action) + '</span>' +
721
- '<button class="btn-danger px-1.5 py-0.5 text-xs opacity-0 group-hover:opacity-100" aria-label="Delete watcher" onclick="deleteWatcher(\\'' + esc(w.id) + '\\')">x</button>' +
722
- '</div>' +
723
- '</div>' +
724
- '<div class="flex items-center justify-between mt-1 pl-5">' +
725
- '<span class="text-xs text-gray-500">' + esc(w.condition) + ' &middot; triggers: ' + w.trigger_count + '</span>' +
726
- '<span class="text-xs text-gray-700">last check: ' + timeAgo(w.last_check_at) + '</span>' +
727
- '</div>' +
728
- '</div>';
729
- }).join('');
730
- }
731
-
732
- async function deleteWatcher(id) {
733
- if (!confirm('Delete this watcher?')) return;
734
- try { await api('/api/watchers/' + encodeURIComponent(id), { method: 'DELETE' }); } catch { return; }
735
- loadWatchers();
736
- }
737
-
738
- // --- Costs ---
739
- async function loadCosts() {
740
- let costs;
741
- try { costs = await api('/api/costs'); }
742
- catch (err) {
743
- const chart = document.getElementById('cost-chart');
744
- if (chart) chart.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load costs (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
745
- return;
746
- }
747
- const chart = document.getElementById('cost-chart');
748
-
749
- if (costs.length === 0) {
750
- chart.innerHTML = '<p class="text-gray-600 text-sm text-center py-8">No cost data yet</p>';
751
- document.getElementById('total-cost').textContent = '';
752
- return;
753
- }
754
-
755
- const total = costs.reduce((s, c) => s + c.total_cost, 0);
756
- document.getElementById('total-cost').textContent = 'Total: $' + total.toFixed(4);
757
- const maxCost = Math.max(...costs.map(c => c.total_cost));
758
-
759
- chart.innerHTML =
760
- '<div class="flex items-end gap-1 h-48">' +
761
- costs.map(c => {
762
- const pct = maxCost > 0 ? (c.total_cost / maxCost * 100) : 0;
763
- const day = c.day.slice(5);
764
- return '<div class="flex-1 flex flex-col items-center gap-1">' +
765
- '<span class="text-xs text-gray-500 font-mono">$' + c.total_cost.toFixed(3) + '</span>' +
766
- '<div class="w-full cost-bar" style="height:' + Math.max(pct, 2) + '%" title="' + esc(c.day) + ': $' + c.total_cost.toFixed(4) + ' (' + c.message_count + ' msgs)"></div>' +
767
- '<span class="text-xs text-gray-600 font-mono">' + day + '</span>' +
768
- '</div>';
769
- }).join('') +
770
- '</div>';
771
- }
772
-
773
- // --- Update ---
774
- const PACKAGE_LABELS = {
775
- 'beecork': 'Beecork',
776
- '@anthropic-ai/claude-code': 'Claude Code',
777
- };
778
-
779
- async function loadUpdateStatus() {
780
- const el = document.getElementById('update-packages');
781
- try {
782
- const data = await api('/api/update/status');
783
- el.innerHTML = data.packages.map(function(p) {
784
- const label = PACKAGE_LABELS[p.name] || p.name;
785
- const installed = p.installed || 'not installed';
786
- const latest = p.latest || '?';
787
- const badge = p.updateAvailable
788
- ? '<span class="text-honey-400 text-xs ml-2">update available</span>'
789
- : '<span class="text-green-400 text-xs ml-2">up to date</span>';
790
- const btn = p.updateAvailable
791
- ? ' <button onclick="doUpdate(\\''+esc(p.name)+'\\')" class="update-pkg-btn bg-honey-600 hover:bg-honey-500 text-black font-semibold px-3 py-1 rounded text-xs ml-2">Update</button>'
792
- : '';
793
- return '<div class="panel-card p-3 flex items-center justify-between">' +
794
- '<div>' +
795
- '<div class="text-white font-medium">' + esc(label) + badge + '</div>' +
796
- '<div class="text-xs text-gray-500 font-mono mt-1">' + esc(installed) + (p.updateAvailable ? ' → ' + esc(latest) : '') + '</div>' +
797
- '</div>' +
798
- '<div>' + btn + '</div>' +
799
- '</div>';
800
- }).join('');
801
- } catch {
802
- el.innerHTML = '<span class="text-red-400">Failed to check for updates</span>';
803
- }
804
- }
805
-
806
- async function doUpdate(pkgName) {
807
- const log = document.getElementById('update-log');
808
- const btns = document.querySelectorAll('.update-pkg-btn');
809
- btns.forEach(function(b) { b.disabled = true; b.textContent = 'Updating...'; });
810
- log.classList.remove('hidden');
811
- log.textContent = 'Updating ' + pkgName + '...\\n';
812
-
813
- try {
814
- const result = await api('/api/update/' + encodeURIComponent(pkgName), {
815
- method: 'POST',
816
- headers: { 'Content-Type': 'application/json' },
817
- body: '{}',
818
- });
819
- if (result.success) {
820
- log.textContent += 'Done!\\n\\nRestart daemon to use the new version:\\n beecork stop && beecork start';
821
- loadUpdateStatus();
822
- } else {
823
- log.textContent += 'Error: ' + (result.error || 'Unknown error');
824
- btns.forEach(function(b) { b.disabled = false; b.textContent = 'Update'; });
825
- }
826
- } catch (err) {
827
- log.textContent += 'Failed: ' + err.message;
828
- btns.forEach(function(b) { b.disabled = false; b.textContent = 'Update'; });
829
- }
830
- }
831
-
832
- // --- Modal helpers ---
833
- function closeModal(e) {
834
- if (e && e.target !== e.currentTarget) return;
835
- document.getElementById('modal').classList.add('hidden');
836
- document.getElementById('modal').innerHTML = '';
837
- }
838
-
839
- // --- Init ---
840
- connectSSE();
841
- loadStatus();
842
- loadTabs();
843
- setInterval(loadStatus, 10000);
844
- // Periodically reload messages for selected tab
845
- setInterval(() => { if (selectedTab) selectTab(selectedTab, { fromUser: false }); }, 8000);
846
- </script>
847
- </body>
848
- </html>`;
849
- }