watch-tail 0.3.1 → 0.5.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 (382) hide show
  1. package/README.md +157 -3
  2. package/build/client/_app/immutable/assets/0.BQQ1YLPc.css +2 -0
  3. package/build/client/_app/immutable/assets/0.BQQ1YLPc.css.br +0 -0
  4. package/build/client/_app/immutable/assets/0.BQQ1YLPc.css.gz +0 -0
  5. package/build/client/_app/immutable/assets/BrushContext.2tkUc39H.css +1 -0
  6. package/build/client/_app/immutable/assets/BrushContext.2tkUc39H.css.br +0 -0
  7. package/build/client/_app/immutable/assets/BrushContext.2tkUc39H.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/ChartAnnotations.Bw8CGteN.css +1 -0
  9. package/build/client/_app/immutable/assets/ChartAnnotations.Bw8CGteN.css.br +0 -0
  10. package/build/client/_app/immutable/assets/ChartAnnotations.Bw8CGteN.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
  12. package/build/client/_app/immutable/assets/Circle.C5MKzgk2.css.br +1 -0
  13. package/build/client/_app/immutable/assets/Circle.C5MKzgk2.css.gz +0 -0
  14. package/build/client/_app/immutable/assets/DefaultTooltip.CGOVIm3D.css +1 -0
  15. package/build/client/_app/immutable/assets/DefaultTooltip.CGOVIm3D.css.br +0 -0
  16. package/build/client/_app/immutable/assets/DefaultTooltip.CGOVIm3D.css.gz +0 -0
  17. package/build/client/_app/immutable/assets/EventScatter.DA5sZ1c0.css +1 -0
  18. package/build/client/_app/immutable/assets/EventScatter.DA5sZ1c0.css.br +0 -0
  19. package/build/client/_app/immutable/assets/EventScatter.DA5sZ1c0.css.gz +0 -0
  20. package/build/client/_app/immutable/assets/Labels.sEaJtb97.css +1 -0
  21. package/build/client/_app/immutable/assets/Labels.sEaJtb97.css.br +0 -0
  22. package/build/client/_app/immutable/assets/Labels.sEaJtb97.css.gz +0 -0
  23. package/build/client/_app/immutable/assets/Legend.DnhxhJ2I.css +1 -0
  24. package/build/client/_app/immutable/assets/Legend.DnhxhJ2I.css.br +0 -0
  25. package/build/client/_app/immutable/assets/Legend.DnhxhJ2I.css.gz +0 -0
  26. package/build/client/_app/immutable/assets/Path.B0wDzTrn.css +1 -0
  27. package/build/client/_app/immutable/assets/Path.B0wDzTrn.css.br +0 -0
  28. package/build/client/_app/immutable/assets/Path.B0wDzTrn.css.gz +0 -0
  29. package/build/client/_app/immutable/assets/TransformContext.1cjyViIk.css +1 -0
  30. package/build/client/_app/immutable/assets/TransformContext.1cjyViIk.css.br +0 -0
  31. package/build/client/_app/immutable/assets/TransformContext.1cjyViIk.css.gz +0 -0
  32. package/build/client/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
  33. package/build/client/_app/immutable/assets/Voronoi.ce7atosu.css.br +0 -0
  34. package/build/client/_app/immutable/assets/Voronoi.ce7atosu.css.gz +0 -0
  35. package/build/client/_app/immutable/chunks/AUlz4-kZ.js +1 -0
  36. package/build/client/_app/immutable/chunks/AUlz4-kZ.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/AUlz4-kZ.js.gz +0 -0
  38. package/build/client/_app/immutable/chunks/B1t8v2wP.js +1 -0
  39. package/build/client/_app/immutable/chunks/B1t8v2wP.js.br +0 -0
  40. package/build/client/_app/immutable/chunks/B1t8v2wP.js.gz +0 -0
  41. package/build/client/_app/immutable/chunks/B6nyufJR.js +1 -0
  42. package/build/client/_app/immutable/chunks/B6nyufJR.js.br +0 -0
  43. package/build/client/_app/immutable/chunks/B6nyufJR.js.gz +0 -0
  44. package/build/client/_app/immutable/chunks/BR2LaRms.js +1 -0
  45. package/build/client/_app/immutable/chunks/BR2LaRms.js.br +1 -0
  46. package/build/client/_app/immutable/chunks/BR2LaRms.js.gz +0 -0
  47. package/build/client/_app/immutable/chunks/BR7qH0F5.js +1 -0
  48. package/build/client/_app/immutable/chunks/BR7qH0F5.js.br +0 -0
  49. package/build/client/_app/immutable/chunks/BR7qH0F5.js.gz +0 -0
  50. package/build/client/_app/immutable/chunks/BkXArOra.js +1 -0
  51. package/build/client/_app/immutable/chunks/BkXArOra.js.br +0 -0
  52. package/build/client/_app/immutable/chunks/BkXArOra.js.gz +0 -0
  53. package/build/client/_app/immutable/chunks/Bl052uUt.js +1 -0
  54. package/build/client/_app/immutable/chunks/Bl052uUt.js.br +0 -0
  55. package/build/client/_app/immutable/chunks/Bl052uUt.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/Bmk_Voz6.js +1 -0
  57. package/build/client/_app/immutable/chunks/Bmk_Voz6.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/Bmk_Voz6.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/Bp63j1zN.js +1 -0
  60. package/build/client/_app/immutable/chunks/Bp63j1zN.js.br +0 -0
  61. package/build/client/_app/immutable/chunks/Bp63j1zN.js.gz +0 -0
  62. package/build/client/_app/immutable/chunks/BwntJtbK.js +1 -0
  63. package/build/client/_app/immutable/chunks/BwntJtbK.js.br +0 -0
  64. package/build/client/_app/immutable/chunks/BwntJtbK.js.gz +0 -0
  65. package/build/client/_app/immutable/chunks/C20WvNRq.js +1 -0
  66. package/build/client/_app/immutable/chunks/C20WvNRq.js.br +0 -0
  67. package/build/client/_app/immutable/chunks/C20WvNRq.js.gz +0 -0
  68. package/build/client/_app/immutable/chunks/C58PZtCD.js +4 -0
  69. package/build/client/_app/immutable/chunks/C58PZtCD.js.br +0 -0
  70. package/build/client/_app/immutable/chunks/C58PZtCD.js.gz +0 -0
  71. package/build/client/_app/immutable/chunks/CCch3uox.js +1 -0
  72. package/build/client/_app/immutable/chunks/CCch3uox.js.br +0 -0
  73. package/build/client/_app/immutable/chunks/CCch3uox.js.gz +0 -0
  74. package/build/client/_app/immutable/chunks/CDbeYGlD.js +1 -0
  75. package/build/client/_app/immutable/chunks/CDbeYGlD.js.br +0 -0
  76. package/build/client/_app/immutable/chunks/CDbeYGlD.js.gz +0 -0
  77. package/build/client/_app/immutable/chunks/CJFHBop_.js +1 -0
  78. package/build/client/_app/immutable/chunks/CJFHBop_.js.br +0 -0
  79. package/build/client/_app/immutable/chunks/CJFHBop_.js.gz +0 -0
  80. package/build/client/_app/immutable/chunks/CYw9ugj1.js +1 -0
  81. package/build/client/_app/immutable/chunks/CYw9ugj1.js.br +0 -0
  82. package/build/client/_app/immutable/chunks/CYw9ugj1.js.gz +0 -0
  83. package/build/client/_app/immutable/chunks/CjJd0uRB.js +1 -0
  84. package/build/client/_app/immutable/chunks/CjJd0uRB.js.br +1 -0
  85. package/build/client/_app/immutable/chunks/CjJd0uRB.js.gz +0 -0
  86. package/build/client/_app/immutable/chunks/CoEFQJcr.js +1 -0
  87. package/build/client/_app/immutable/chunks/CoEFQJcr.js.br +0 -0
  88. package/build/client/_app/immutable/chunks/CoEFQJcr.js.gz +0 -0
  89. package/build/client/_app/immutable/chunks/D-JtVVAo.js +1 -0
  90. package/build/client/_app/immutable/chunks/D-JtVVAo.js.br +0 -0
  91. package/build/client/_app/immutable/chunks/D-JtVVAo.js.gz +0 -0
  92. package/build/client/_app/immutable/chunks/DJDlf6lC.js +1 -0
  93. package/build/client/_app/immutable/chunks/DJDlf6lC.js.br +0 -0
  94. package/build/client/_app/immutable/chunks/DJDlf6lC.js.gz +0 -0
  95. package/build/client/_app/immutable/chunks/DJLYkl5w.js +1 -0
  96. package/build/client/_app/immutable/chunks/DJLYkl5w.js.br +0 -0
  97. package/build/client/_app/immutable/chunks/DJLYkl5w.js.gz +0 -0
  98. package/build/client/_app/immutable/chunks/DPKdNiYi.js +1 -0
  99. package/build/client/_app/immutable/chunks/DPKdNiYi.js.br +0 -0
  100. package/build/client/_app/immutable/chunks/DPKdNiYi.js.gz +0 -0
  101. package/build/client/_app/immutable/chunks/DQi7bc16.js +1 -0
  102. package/build/client/_app/immutable/chunks/DQi7bc16.js.br +0 -0
  103. package/build/client/_app/immutable/chunks/DQi7bc16.js.gz +0 -0
  104. package/build/client/_app/immutable/chunks/DVTgeA2y.js +2 -0
  105. package/build/client/_app/immutable/chunks/DVTgeA2y.js.br +0 -0
  106. package/build/client/_app/immutable/chunks/DVTgeA2y.js.gz +0 -0
  107. package/build/client/_app/immutable/chunks/Db0q5_zr.js +1 -0
  108. package/build/client/_app/immutable/chunks/Db0q5_zr.js.br +0 -0
  109. package/build/client/_app/immutable/chunks/Db0q5_zr.js.gz +0 -0
  110. package/build/client/_app/immutable/chunks/Dfvzj6n2.js +1 -0
  111. package/build/client/_app/immutable/chunks/Dfvzj6n2.js.br +0 -0
  112. package/build/client/_app/immutable/chunks/Dfvzj6n2.js.gz +0 -0
  113. package/build/client/_app/immutable/chunks/Di37PX72.js +1 -0
  114. package/build/client/_app/immutable/chunks/Di37PX72.js.br +0 -0
  115. package/build/client/_app/immutable/chunks/Di37PX72.js.gz +0 -0
  116. package/build/client/_app/immutable/chunks/{DyIYFD7i.js → DkyYNO4U.js} +1 -1
  117. package/build/client/_app/immutable/chunks/DkyYNO4U.js.br +0 -0
  118. package/build/client/_app/immutable/chunks/DkyYNO4U.js.gz +0 -0
  119. package/build/client/_app/immutable/chunks/DmRhEzIa.js +1 -0
  120. package/build/client/_app/immutable/chunks/DmRhEzIa.js.br +0 -0
  121. package/build/client/_app/immutable/chunks/DmRhEzIa.js.gz +0 -0
  122. package/build/client/_app/immutable/chunks/Dp8S9ymS.js +1 -0
  123. package/build/client/_app/immutable/chunks/Dp8S9ymS.js.br +0 -0
  124. package/build/client/_app/immutable/chunks/Dp8S9ymS.js.gz +0 -0
  125. package/build/client/_app/immutable/chunks/DslUcEd-.js +1 -0
  126. package/build/client/_app/immutable/chunks/DslUcEd-.js.br +0 -0
  127. package/build/client/_app/immutable/chunks/DslUcEd-.js.gz +0 -0
  128. package/build/client/_app/immutable/chunks/Dt_Zd6U2.js +1 -0
  129. package/build/client/_app/immutable/chunks/Dt_Zd6U2.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/Dt_Zd6U2.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/DwezeuNE.js +5 -0
  132. package/build/client/_app/immutable/chunks/DwezeuNE.js.br +0 -0
  133. package/build/client/_app/immutable/chunks/DwezeuNE.js.gz +0 -0
  134. package/build/client/_app/immutable/chunks/HclGiUj8.js +1 -0
  135. package/build/client/_app/immutable/chunks/HclGiUj8.js.br +0 -0
  136. package/build/client/_app/immutable/chunks/HclGiUj8.js.gz +0 -0
  137. package/build/client/_app/immutable/chunks/PYzEcuyd.js +10 -0
  138. package/build/client/_app/immutable/chunks/PYzEcuyd.js.br +0 -0
  139. package/build/client/_app/immutable/chunks/PYzEcuyd.js.gz +0 -0
  140. package/build/client/_app/immutable/chunks/RobXhXPM.js +1 -0
  141. package/build/client/_app/immutable/chunks/RobXhXPM.js.br +1 -0
  142. package/build/client/_app/immutable/chunks/RobXhXPM.js.gz +0 -0
  143. package/build/client/_app/immutable/chunks/UySfXNq7.js +1 -0
  144. package/build/client/_app/immutable/chunks/UySfXNq7.js.br +0 -0
  145. package/build/client/_app/immutable/chunks/UySfXNq7.js.gz +0 -0
  146. package/build/client/_app/immutable/chunks/Y5urAfNy.js +1 -0
  147. package/build/client/_app/immutable/chunks/Y5urAfNy.js.br +0 -0
  148. package/build/client/_app/immutable/chunks/Y5urAfNy.js.gz +0 -0
  149. package/build/client/_app/immutable/chunks/_Zq76xnA.js +15 -0
  150. package/build/client/_app/immutable/chunks/_Zq76xnA.js.br +0 -0
  151. package/build/client/_app/immutable/chunks/_Zq76xnA.js.gz +0 -0
  152. package/build/client/_app/immutable/chunks/dbhDrJmm.js +1 -0
  153. package/build/client/_app/immutable/chunks/dbhDrJmm.js.br +0 -0
  154. package/build/client/_app/immutable/chunks/dbhDrJmm.js.gz +0 -0
  155. package/build/client/_app/immutable/chunks/pqiPC7Y8.js +3 -0
  156. package/build/client/_app/immutable/chunks/pqiPC7Y8.js.br +0 -0
  157. package/build/client/_app/immutable/chunks/pqiPC7Y8.js.gz +0 -0
  158. package/build/client/_app/immutable/chunks/yCfI4PCa.js +1 -0
  159. package/build/client/_app/immutable/chunks/yCfI4PCa.js.br +0 -0
  160. package/build/client/_app/immutable/chunks/yCfI4PCa.js.gz +0 -0
  161. package/build/client/_app/immutable/entry/app.vMZB8_Zr.js +2 -0
  162. package/build/client/_app/immutable/entry/app.vMZB8_Zr.js.br +0 -0
  163. package/build/client/_app/immutable/entry/app.vMZB8_Zr.js.gz +0 -0
  164. package/build/client/_app/immutable/entry/start.pM5lNk_5.js +1 -0
  165. package/build/client/_app/immutable/entry/start.pM5lNk_5.js.br +0 -0
  166. package/build/client/_app/immutable/entry/start.pM5lNk_5.js.gz +0 -0
  167. package/build/client/_app/immutable/nodes/0.CZO0sIxX.js +1 -0
  168. package/build/client/_app/immutable/nodes/0.CZO0sIxX.js.br +0 -0
  169. package/build/client/_app/immutable/nodes/0.CZO0sIxX.js.gz +0 -0
  170. package/build/client/_app/immutable/nodes/1.zcN3R_zF.js +1 -0
  171. package/build/client/_app/immutable/nodes/1.zcN3R_zF.js.br +0 -0
  172. package/build/client/_app/immutable/nodes/1.zcN3R_zF.js.gz +0 -0
  173. package/build/client/_app/immutable/nodes/2.DxevVqut.js +2 -0
  174. package/build/client/_app/immutable/nodes/2.DxevVqut.js.br +0 -0
  175. package/build/client/_app/immutable/nodes/2.DxevVqut.js.gz +0 -0
  176. package/build/client/_app/version.json +1 -1
  177. package/build/client/_app/version.json.br +0 -0
  178. package/build/client/_app/version.json.gz +0 -0
  179. package/build/handler.js +9 -7
  180. package/build/handler.js.map +1 -1
  181. package/build/index.js +9 -7
  182. package/build/index.js.map +1 -1
  183. package/build/server/chunks/chunks/Arc.canvas.js-D6g58nSp.js +701 -0
  184. package/build/server/chunks/chunks/Arc.canvas.js-D6g58nSp.js.map +1 -0
  185. package/build/server/chunks/chunks/Arc.js-D78Cf_4M.js +31 -0
  186. package/build/server/chunks/chunks/Arc.js-D78Cf_4M.js.map +1 -0
  187. package/build/server/chunks/chunks/Bar.js-Cv73T8_D.js +448 -0
  188. package/build/server/chunks/chunks/Bar.js-Cv73T8_D.js.map +1 -0
  189. package/build/server/chunks/chunks/ChartAnnotations.js-Ci22VB_6.js +983 -0
  190. package/build/server/chunks/chunks/ChartAnnotations.js-Ci22VB_6.js.map +1 -0
  191. package/build/server/chunks/chunks/Circle.html.js-BjwyNRG3.js +355 -0
  192. package/build/server/chunks/chunks/Circle.html.js-BjwyNRG3.js.map +1 -0
  193. package/build/server/chunks/chunks/ClipPath.canvas.js-CFTBDWVU.js +170 -0
  194. package/build/server/chunks/chunks/ClipPath.canvas.js-CFTBDWVU.js.map +1 -0
  195. package/build/server/chunks/chunks/DefaultTooltip.js-0r5rbE-1.js +352 -0
  196. package/build/server/chunks/chunks/DefaultTooltip.js-0r5rbE-1.js.map +1 -0
  197. package/build/server/chunks/chunks/EventScatter.js-DqCMPeHG.js +7304 -0
  198. package/build/server/chunks/chunks/EventScatter.js-DqCMPeHG.js.map +1 -0
  199. package/build/server/chunks/chunks/Labels.js-DpagpV6d.js +497 -0
  200. package/build/server/chunks/chunks/Labels.js-DpagpV6d.js.map +1 -0
  201. package/build/server/chunks/chunks/Legend.js-BOpwpe5s.js +343 -0
  202. package/build/server/chunks/chunks/Legend.js-BOpwpe5s.js.map +1 -0
  203. package/build/server/chunks/chunks/Line.html.js-BM5tR11G.js +423 -0
  204. package/build/server/chunks/chunks/Line.html.js-BM5tR11G.js.map +1 -0
  205. package/build/server/chunks/chunks/Link.canvas.js-P8ySzEAw.js +756 -0
  206. package/build/server/chunks/chunks/Link.canvas.js-P8ySzEAw.js.map +1 -0
  207. package/build/server/chunks/chunks/Path.canvas.js-DNAtnEBt.js +1259 -0
  208. package/build/server/chunks/chunks/Path.canvas.js-DNAtnEBt.js.map +1 -0
  209. package/build/server/chunks/chunks/Points.html.js-CLbHD6OP.js +228 -0
  210. package/build/server/chunks/chunks/Points.html.js-CLbHD6OP.js.map +1 -0
  211. package/build/server/chunks/chunks/Points.js-e6g0Re6H.js +14 -0
  212. package/build/server/chunks/chunks/Points.js-e6g0Re6H.js.map +1 -0
  213. package/build/server/chunks/chunks/Points2.js-BMGdkzTE.js +25 -0
  214. package/build/server/chunks/chunks/Points2.js-BMGdkzTE.js.map +1 -0
  215. package/build/server/chunks/chunks/Rect.html.js-Q3UvRwNn.js +547 -0
  216. package/build/server/chunks/chunks/Rect.html.js-Q3UvRwNn.js.map +1 -0
  217. package/build/server/chunks/chunks/Spline.js-BuZPz2PJ.js +516 -0
  218. package/build/server/chunks/chunks/Spline.js-BuZPz2PJ.js.map +1 -0
  219. package/build/server/chunks/chunks/Text.html.js-C8xO5Nz6.js +847 -0
  220. package/build/server/chunks/chunks/Text.html.js-C8xO5Nz6.js.map +1 -0
  221. package/build/server/chunks/chunks/TooltipItem.js-T0cKLjLu.js +314 -0
  222. package/build/server/chunks/chunks/TooltipItem.js-T0cKLjLu.js.map +1 -0
  223. package/build/server/chunks/chunks/Voronoi.js-DevTsXQE.js +2052 -0
  224. package/build/server/chunks/chunks/Voronoi.js-DevTsXQE.js.map +1 -0
  225. package/build/server/chunks/chunks/archive.js-C6rz09rJ.js +584 -0
  226. package/build/server/chunks/chunks/archive.js-C6rz09rJ.js.map +1 -0
  227. package/build/server/chunks/chunks/array.js-CBp0H4t3.js +8 -0
  228. package/build/server/chunks/chunks/array.js-CBp0H4t3.js.map +1 -0
  229. package/build/server/chunks/chunks/attributes.js-BVWo7wbg.js +25 -0
  230. package/build/server/chunks/chunks/attributes.js-BVWo7wbg.js.map +1 -0
  231. package/build/server/chunks/chunks/{env2.js-D7xrfc37.js → aws.js-DkPqAybc.js} +13 -64
  232. package/build/server/chunks/chunks/aws.js-DkPqAybc.js.map +1 -0
  233. package/build/server/chunks/chunks/band.js-C7izXphE.js +155 -0
  234. package/build/server/chunks/chunks/band.js-C7izXphE.js.map +1 -0
  235. package/build/server/chunks/chunks/centroid.js-BnGN3LH0.js +1356 -0
  236. package/build/server/chunks/chunks/centroid.js-BnGN3LH0.js.map +1 -0
  237. package/build/server/chunks/chunks/common.js-BWQB8Die.js +5760 -0
  238. package/build/server/chunks/chunks/common.js-BWQB8Die.js.map +1 -0
  239. package/build/server/chunks/chunks/env2.js-CcIskxOZ.js +60 -0
  240. package/build/server/chunks/chunks/env2.js-CcIskxOZ.js.map +1 -0
  241. package/build/server/chunks/chunks/{exports.js-CvFPPbaH.js → exports.js-ClPPfcHj.js} +6 -1
  242. package/build/server/chunks/chunks/exports.js-ClPPfcHj.js.map +1 -0
  243. package/build/server/chunks/chunks/group.js-DBFzWAox.js +30 -0
  244. package/build/server/chunks/chunks/group.js-DBFzWAox.js.map +1 -0
  245. package/build/server/chunks/chunks/index-server.js-DNLOKvM0.js +248 -0
  246. package/build/server/chunks/chunks/index-server.js-DNLOKvM0.js.map +1 -0
  247. package/build/server/chunks/chunks/index-server2.js-l0sNRNKZ.js +2 -0
  248. package/build/server/chunks/chunks/index-server2.js-l0sNRNKZ.js.map +1 -0
  249. package/build/server/chunks/chunks/{internal2.js-DZ8ZzzjN.js → internal.js-DZ8ZzzjN.js} +1 -1
  250. package/build/server/chunks/chunks/internal.js-DZ8ZzzjN.js.map +1 -0
  251. package/build/server/chunks/chunks/internal2.js-DDcktblR.js +188 -0
  252. package/build/server/chunks/chunks/internal2.js-DDcktblR.js.map +1 -0
  253. package/build/server/chunks/chunks/key.svelte.js-CWIvXc6L.js +695 -0
  254. package/build/server/chunks/chunks/key.svelte.js-CWIvXc6L.js.map +1 -0
  255. package/build/server/chunks/chunks/{internal.js-B-DZpK-L.js → legacy-client.js-DJmDw3Zx.js} +13 -439
  256. package/build/server/chunks/chunks/legacy-client.js-DJmDw3Zx.js.map +1 -0
  257. package/build/server/chunks/chunks/level-filter.js-H_AdNbCw.js +297 -0
  258. package/build/server/chunks/chunks/level-filter.js-H_AdNbCw.js.map +1 -0
  259. package/build/server/chunks/chunks/lineRadial.js-Dm0GMqo3.js +126 -0
  260. package/build/server/chunks/chunks/lineRadial.js-Dm0GMqo3.js.map +1 -0
  261. package/build/server/chunks/chunks/linear.js-CTOKrHn9.js +1046 -0
  262. package/build/server/chunks/chunks/linear.js-CTOKrHn9.js.map +1 -0
  263. package/build/server/chunks/chunks/log-buffer.js-CpI7gyom.js +471 -0
  264. package/build/server/chunks/chunks/log-buffer.js-CpI7gyom.js.map +1 -0
  265. package/build/server/chunks/chunks/math.js-BqujbaBs.js +50 -0
  266. package/build/server/chunks/chunks/math.js-BqujbaBs.js.map +1 -0
  267. package/build/server/chunks/chunks/max.js-CuS0BpRI.js +11 -0
  268. package/build/server/chunks/chunks/max.js-CuS0BpRI.js.map +1 -0
  269. package/build/server/chunks/chunks/min.js-DZWt5tPl.js +11 -0
  270. package/build/server/chunks/chunks/min.js-DZWt5tPl.js.map +1 -0
  271. package/build/server/chunks/chunks/motion.svelte.js-DCHSFMu-.js +677 -0
  272. package/build/server/chunks/chunks/motion.svelte.js-DCHSFMu-.js.map +1 -0
  273. package/build/server/chunks/chunks/occlusion.js-DJ-AdK9u.js +33 -0
  274. package/build/server/chunks/chunks/occlusion.js-DJ-AdK9u.js.map +1 -0
  275. package/build/server/chunks/chunks/path.js-BizMrlzb.js +488 -0
  276. package/build/server/chunks/chunks/path.js-BizMrlzb.js.map +1 -0
  277. package/build/server/chunks/chunks/path2.js-pquT2Kg6.js +27 -0
  278. package/build/server/chunks/chunks/path2.js-pquT2Kg6.js.map +1 -0
  279. package/build/server/chunks/chunks/pointRadial.js-Deeu1LXx.js +7 -0
  280. package/build/server/chunks/chunks/pointRadial.js-Deeu1LXx.js.map +1 -0
  281. package/build/server/chunks/chunks/rect.svelte.js-zeS7FWjB.js +187 -0
  282. package/build/server/chunks/chunks/rect.svelte.js-zeS7FWjB.js.map +1 -0
  283. package/build/server/chunks/chunks/scales.svelte.js-D_1popPi.js +276 -0
  284. package/build/server/chunks/chunks/scales.svelte.js-D_1popPi.js.map +1 -0
  285. package/build/server/chunks/chunks/series-buckets.js-CX_rWCeH.js +123 -0
  286. package/build/server/chunks/chunks/series-buckets.js-CX_rWCeH.js.map +1 -0
  287. package/build/server/chunks/chunks/{server.js-ByNB8uti.js → server.js-CAEBuMIH.js} +167 -2
  288. package/build/server/chunks/chunks/server.js-CAEBuMIH.js.map +1 -0
  289. package/build/server/chunks/chunks/source.js-DZEVvuho.js +21 -0
  290. package/build/server/chunks/chunks/source.js-DZEVvuho.js.map +1 -0
  291. package/build/server/chunks/chunks/{state.js-V2NYGhy_.js → state.js-Dves8H8h.js} +5 -4
  292. package/build/server/chunks/chunks/state.js-Dves8H8h.js.map +1 -0
  293. package/build/server/chunks/chunks/types.js-x3GyGPOz.js +11 -0
  294. package/build/server/chunks/chunks/types.js-x3GyGPOz.js.map +1 -0
  295. package/build/server/chunks/entries/endpoints/api/archive/_server.ts.js-sNd3lS0y.js +39 -0
  296. package/build/server/chunks/entries/endpoints/api/archive/_server.ts.js-sNd3lS0y.js.map +1 -0
  297. package/build/server/chunks/entries/endpoints/api/health/{_server.ts.js-aiNZfkco.js → _server.ts.js-VctnBhIJ.js} +5 -3
  298. package/build/server/chunks/entries/endpoints/api/health/_server.ts.js-VctnBhIJ.js.map +1 -0
  299. package/build/server/chunks/entries/endpoints/api/identity/_server.ts.js-Dt540Sio.js +73 -0
  300. package/build/server/chunks/entries/endpoints/api/identity/_server.ts.js-Dt540Sio.js.map +1 -0
  301. package/build/server/chunks/entries/endpoints/api/log-groups/{_server.ts.js-D143-vRl.js → _server.ts.js-BGyWbj78.js} +57 -7
  302. package/build/server/chunks/entries/endpoints/api/log-groups/_server.ts.js-BGyWbj78.js.map +1 -0
  303. package/build/server/chunks/entries/endpoints/api/regions/{_server.ts.js-CDNL7msU.js → _server.ts.js-CBqfN6Pg.js} +5 -3
  304. package/build/server/chunks/entries/endpoints/api/regions/_server.ts.js-CBqfN6Pg.js.map +1 -0
  305. package/build/server/chunks/entries/endpoints/api/series/_server.ts.js-DjGMqXKo.js +194 -0
  306. package/build/server/chunks/entries/endpoints/api/series/_server.ts.js-DjGMqXKo.js.map +1 -0
  307. package/build/server/chunks/entries/endpoints/api/stream/{_server.ts.js-D_i-xW6_.js → _server.ts.js-C-XDemlx.js} +324 -222
  308. package/build/server/chunks/entries/endpoints/api/stream/_server.ts.js-C-XDemlx.js.map +1 -0
  309. package/build/server/chunks/entries/fallbacks/{error.svelte.js-CAasf3PF.js → error.svelte.js-DdUYlUJV.js} +7 -5
  310. package/build/server/chunks/entries/fallbacks/{error.svelte.js-CAasf3PF.js.map → error.svelte.js-DdUYlUJV.js.map} +1 -1
  311. package/build/server/chunks/entries/pages/_layout.svelte.js-CBQpEToy.js +22 -0
  312. package/build/server/chunks/entries/pages/_layout.svelte.js-CBQpEToy.js.map +1 -0
  313. package/build/server/chunks/entries/pages/{_page.svelte.js-DzAAqWz2.js → _page.svelte.js-B-kVwiNh.js} +563 -349
  314. package/build/server/chunks/entries/pages/_page.svelte.js-B-kVwiNh.js.map +1 -0
  315. package/build/server/chunks/{handler-CuWqZDuC.js → handler-BHf-eLnl.js} +3 -3
  316. package/build/server/chunks/{handler-CuWqZDuC.js.map → handler-BHf-eLnl.js.map} +1 -1
  317. package/build/server/chunks/{index.js-NHTL74z2.js → index.js-D6Auup1Z.js} +6 -5
  318. package/build/server/chunks/index.js-D6Auup1Z.js.map +1 -0
  319. package/build/server/chunks/manifest.js-B3Q7n9I6.js +91 -0
  320. package/build/server/chunks/manifest.js-B3Q7n9I6.js.map +1 -0
  321. package/build/server/chunks/nodes/0.js-BMP_Yttt.js +9 -0
  322. package/build/server/chunks/nodes/{0.js-Bg4_hDjk.js.map → 0.js-BMP_Yttt.js.map} +1 -1
  323. package/build/server/chunks/nodes/1.js-2HQHUOse.js +9 -0
  324. package/build/server/chunks/nodes/{1.js-CDSk3TvW.js.map → 1.js-2HQHUOse.js.map} +1 -1
  325. package/build/server/chunks/nodes/2.js-7RtHpiHN.js +9 -0
  326. package/build/server/chunks/nodes/2.js-7RtHpiHN.js.map +1 -0
  327. package/dist/cli/completions.js +6 -0
  328. package/dist/cli/index.js +149 -30
  329. package/dist/cli/options.js +28 -2
  330. package/dist/cli/preflight.js +72 -0
  331. package/dist/cli/ui.js +7 -3
  332. package/dist/lib/cli/aws.js +12 -4
  333. package/dist/lib/log-buffer.js +314 -0
  334. package/dist/lib/log-format.js +185 -0
  335. package/dist/lib/types.js +0 -4
  336. package/package.json +7 -2
  337. package/build/client/_app/immutable/assets/0.CocWI5K4.css +0 -2
  338. package/build/client/_app/immutable/assets/0.CocWI5K4.css.br +0 -0
  339. package/build/client/_app/immutable/assets/0.CocWI5K4.css.gz +0 -0
  340. package/build/client/_app/immutable/chunks/05WsiB4A.js +0 -1
  341. package/build/client/_app/immutable/chunks/05WsiB4A.js.br +0 -0
  342. package/build/client/_app/immutable/chunks/05WsiB4A.js.gz +0 -0
  343. package/build/client/_app/immutable/chunks/DyIYFD7i.js.br +0 -0
  344. package/build/client/_app/immutable/chunks/DyIYFD7i.js.gz +0 -0
  345. package/build/client/_app/immutable/chunks/ZAD2y5xJ.js +0 -3
  346. package/build/client/_app/immutable/chunks/ZAD2y5xJ.js.br +0 -0
  347. package/build/client/_app/immutable/chunks/ZAD2y5xJ.js.gz +0 -0
  348. package/build/client/_app/immutable/entry/app.D0_YFW4X.js +0 -2
  349. package/build/client/_app/immutable/entry/app.D0_YFW4X.js.br +0 -0
  350. package/build/client/_app/immutable/entry/app.D0_YFW4X.js.gz +0 -0
  351. package/build/client/_app/immutable/entry/start.D1rrHrg2.js +0 -1
  352. package/build/client/_app/immutable/entry/start.D1rrHrg2.js.br +0 -0
  353. package/build/client/_app/immutable/entry/start.D1rrHrg2.js.gz +0 -0
  354. package/build/client/_app/immutable/nodes/0.DHBUWbOo.js +0 -1
  355. package/build/client/_app/immutable/nodes/0.DHBUWbOo.js.br +0 -0
  356. package/build/client/_app/immutable/nodes/0.DHBUWbOo.js.gz +0 -0
  357. package/build/client/_app/immutable/nodes/1.D1vhnOgO.js +0 -1
  358. package/build/client/_app/immutable/nodes/1.D1vhnOgO.js.br +0 -0
  359. package/build/client/_app/immutable/nodes/1.D1vhnOgO.js.gz +0 -0
  360. package/build/client/_app/immutable/nodes/2.BPdL6B7F.js +0 -5
  361. package/build/client/_app/immutable/nodes/2.BPdL6B7F.js.br +0 -0
  362. package/build/client/_app/immutable/nodes/2.BPdL6B7F.js.gz +0 -0
  363. package/build/server/chunks/chunks/env2.js-D7xrfc37.js.map +0 -1
  364. package/build/server/chunks/chunks/exports.js-CvFPPbaH.js.map +0 -1
  365. package/build/server/chunks/chunks/internal.js-B-DZpK-L.js.map +0 -1
  366. package/build/server/chunks/chunks/internal2.js-DZ8ZzzjN.js.map +0 -1
  367. package/build/server/chunks/chunks/server.js-ByNB8uti.js.map +0 -1
  368. package/build/server/chunks/chunks/state.js-V2NYGhy_.js.map +0 -1
  369. package/build/server/chunks/entries/endpoints/api/health/_server.ts.js-aiNZfkco.js.map +0 -1
  370. package/build/server/chunks/entries/endpoints/api/log-groups/_server.ts.js-D143-vRl.js.map +0 -1
  371. package/build/server/chunks/entries/endpoints/api/regions/_server.ts.js-CDNL7msU.js.map +0 -1
  372. package/build/server/chunks/entries/endpoints/api/stream/_server.ts.js-D_i-xW6_.js.map +0 -1
  373. package/build/server/chunks/entries/pages/_layout.svelte.js-Bx7lXkAi.js +0 -22
  374. package/build/server/chunks/entries/pages/_layout.svelte.js-Bx7lXkAi.js.map +0 -1
  375. package/build/server/chunks/entries/pages/_page.svelte.js-DzAAqWz2.js.map +0 -1
  376. package/build/server/chunks/index.js-NHTL74z2.js.map +0 -1
  377. package/build/server/chunks/manifest.js-8g4gjxwf.js +0 -70
  378. package/build/server/chunks/manifest.js-8g4gjxwf.js.map +0 -1
  379. package/build/server/chunks/nodes/0.js-Bg4_hDjk.js +0 -9
  380. package/build/server/chunks/nodes/1.js-CDSk3TvW.js +0 -9
  381. package/build/server/chunks/nodes/2.js-BVhBcWrN.js +0 -9
  382. package/build/server/chunks/nodes/2.js-BVhBcWrN.js.map +0 -1
@@ -1,11 +1,15 @@
1
- import '../../chunks/internal.js-B-DZpK-L.js';
2
- import { ah as escape_html, ai as attr_style, P as derived, ag as attr, aj as attr_class, ak as ensure_array_like, al as stringify, am as clsx } from '../../chunks/server.js-ByNB8uti.js';
1
+ import { ah as escape_html, ag as attr, ai as attr_style, ac as derived, aj as attr_class, ak as ensure_array_like, al as stringify, am as bind_props, an as clsx } from '../../chunks/server.js-CAEBuMIH.js';
2
+ import { L as LogStore, t as tokenizeJson, f as filterByLevel, b as filterEvents, e as effectiveLevel, a as detectLevel, c as formatLogMessage, g as eventKey, l as levelColorClass } from '../../chunks/log-buffer.js-CpI7gyom.js';
3
3
  import { R as REGION_CODES } from '../../chunks/regions.js-IQPMyP5C.js';
4
4
  import { P as PRESET_LABELS, a as presetLabel, H as HISTORIC_PRESETS, t as toDateTimeLocal, d as describeWindow } from '../../chunks/time-range.js-BLMT8aU3.js';
5
- import { p as page, r as replaceState } from '../../chunks/state.js-V2NYGhy_.js';
5
+ import { p as page, r as replaceState } from '../../chunks/state.js-Dves8H8h.js';
6
+ import { S as SERIES_LEVEL_COLOR, g as groupByLevel, i as isMeaningfulBrush, b as bucketEvents } from '../../chunks/series-buckets.js-CX_rWCeH.js';
6
7
  import '../../chunks/uneval.js-DaakSYFQ.js';
8
+ import '../../chunks/index-server.js-DNLOKvM0.js';
7
9
  import '../../chunks/shared.js-CgjUIvRK.js';
8
- import '../../chunks/exports.js-CvFPPbaH.js';
10
+ import '../../chunks/exports.js-ClPPfcHj.js';
11
+ import '../../chunks/internal2.js-DDcktblR.js';
12
+ import '../../chunks/legacy-client.js-DJmDw3Zx.js';
9
13
  import '../../chunks/utils.js-BIlHjgGW.js';
10
14
 
11
15
  //#region src/lib/format.ts
@@ -138,7 +142,8 @@ var STORAGE_KEYS = {
138
142
  sidebarWidth: "watch-stream:sidebar-width",
139
143
  prefixWidth: "watch-stream:prefix-width",
140
144
  wrap: "watch-stream:wrap",
141
- jsonView: "watch-stream:json-view"
145
+ jsonView: "watch-stream:json-view",
146
+ chartOpen: "watch-stream:chart-open"
142
147
  };
143
148
  /** Default width of the group-list pane, in rem (matches the original layout). */
144
149
  var SIDEBAR_WIDTH = {
@@ -162,6 +167,85 @@ function remToPx(rem, rootFontSize = 16) {
162
167
  return rem * rootFontSize;
163
168
  }
164
169
  //#endregion
170
+ //#region src/lib/components/EventScatterPanel.svelte
171
+ function EventScatterPanel($$renderer, $$props) {
172
+ $$renderer.component(($$renderer) => {
173
+ /**
174
+ * The chart panel above the log view: a header that is always cheap to render
175
+ * (title, totals, per-level legend) and the chart below it, loaded on demand.
176
+ *
177
+ * The charting library is imported dynamically and only once the panel is open,
178
+ * so layerchart never delays the first paint of the log view. The header stays
179
+ * available while the chart chunk is still loading, and the whole panel
180
+ * collapses to that header so a user who does not want the chart can reclaim
181
+ * the space - the choice is remembered.
182
+ */
183
+ /** Props the lazily loaded chart takes. */
184
+ /** The chart component, as its module exports it. */
185
+ /** Fetches the chart module; injectable so tests need no chunk fetch. */
186
+ /**
187
+ * The real loader: a dynamic import, which is what keeps layerchart out of the
188
+ * initial bundle. It only runs in the browser, and only once the panel is open.
189
+ */
190
+ const loadChartModule = () => import('../../chunks/EventScatter.js-DqCMPeHG.js');
191
+ /** Bucketed counts for the window. */
192
+ /** Inclusive start of the window, epoch ms. */
193
+ /** Inclusive end of the window, epoch ms. */
194
+ /** Bucket width in ms, shown in the summary. */
195
+ /** Log groups the points cover, for the summary. */
196
+ /** True while the counts are being fetched. */
197
+ /** Called with the brushed range when the drag ends, or `null` on clear. */
198
+ /** Chart height in px. */
199
+ /** Start open, which is also what the first visit uses. */
200
+ /** Chart module loader; defaults to the dynamic import. */
201
+ let { points = [], from = 0, to = 0, bucketMs = 0, groups = [], loading = false, onBrush, height = 200, open = true, loadChart = loadChartModule } = $$props;
202
+ let expanded = derived(() => open);
203
+ /** Totals and labels per level, in severity order. */
204
+ let levelGroups = derived(() => groupByLevel(points));
205
+ /** Total events drawn. */
206
+ let totalEvents = derived(() => levelGroups().reduce((sum, entry) => sum + entry.events, 0));
207
+ /** Buckets drawn. */
208
+ let bucketCount = derived(() => new Set(points.map((point) => point.t)).size);
209
+ /** True when there is something to draw. */
210
+ let hasPoints = derived(() => points.length > 0);
211
+ /** Clears the brush, for the host's Reset zoom control. */
212
+ function reset() {
213
+ onBrush?.(null);
214
+ }
215
+ $$renderer.push(`<section class="flex min-w-0 flex-col gap-1 rounded-lg border border-neutral-800 bg-neutral-950/60 p-2" data-testid="event-scatter" aria-label="Events over time"><div class="flex flex-wrap items-center gap-x-3 gap-y-1 px-1 text-[0.6875rem]"><button type="button"${attr("aria-expanded", expanded())}${attr("title", expanded() ? "Hide the chart" : "Show the chart")} data-testid="scatter-toggle" class="flex items-center gap-1 rounded-md border border-neutral-800 bg-neutral-900 px-2 py-0.5 font-semibold uppercase tracking-wider text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"><span aria-hidden="true">${escape_html(expanded() ? "▾" : "▸")}</span> Events over time</button> <span class="text-neutral-400" data-testid="scatter-summary">${escape_html(formatCount(totalEvents()))} events in ${escape_html(formatCount(bucketCount()))} buckets</span> `);
216
+ if (groups.length > 1) $$renderer.push(`<!--[0--><span class="text-neutral-500" data-testid="scatter-groups">across ${escape_html(groups.length)} groups</span>`);
217
+ else $$renderer.push("<!--[-1-->");
218
+ $$renderer.push(`<!--]--> <!--[-->`);
219
+ const each_array = ensure_array_like(levelGroups());
220
+ for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
221
+ let entry = each_array[$$index];
222
+ $$renderer.push(`<span class="flex items-center gap-1 text-neutral-400"${attr("data-testid", `scatter-legend-${stringify(entry.level)}`)}><span class="inline-block h-2 w-2 rounded-full"${attr_style(`background-color: ${stringify(SERIES_LEVEL_COLOR[entry.level])}`)}></span> ${escape_html(entry.label)} <span class="text-neutral-500">${escape_html(formatCount(entry.events))}</span></span>`);
223
+ }
224
+ $$renderer.push(`<!--]--> `);
225
+ if (loading && expanded()) $$renderer.push(`<!--[0--><span class="text-amber-300" data-testid="scatter-loading">counting…</span>`);
226
+ else $$renderer.push("<!--[-1-->");
227
+ $$renderer.push(`<!--]--> `);
228
+ if (expanded()) {
229
+ $$renderer.push("<!--[0-->");
230
+ $$renderer.push(`<!--[-1--><span class="ml-auto text-neutral-500" data-testid="scatter-hint">drag to zoom · click to clear</span>`);
231
+ $$renderer.push(`<!--]-->`);
232
+ } else $$renderer.push("<!--[-1-->");
233
+ $$renderer.push(`<!--]--></div> `);
234
+ if (expanded()) {
235
+ $$renderer.push("<!--[0-->");
236
+ if (hasPoints()) {
237
+ $$renderer.push("<!--[1-->");
238
+ $$renderer.push(`<!--[-1--><div class="w-full animate-pulse rounded bg-neutral-900/60"${attr_style(`height: ${stringify(height)}px`)}></div>`);
239
+ $$renderer.push(`<!--]-->`);
240
+ } else if (!loading) $$renderer.push(`<!--[2--><p class="px-1 py-6 text-xs text-neutral-500" data-testid="scatter-empty">No events in this window yet.</p>`);
241
+ else $$renderer.push("<!--[-1-->");
242
+ $$renderer.push(`<!--]-->`);
243
+ } else $$renderer.push("<!--[-1-->");
244
+ $$renderer.push(`<!--]--></section>`);
245
+ bind_props($$props, { reset });
246
+ });
247
+ }
248
+ //#endregion
165
249
  //#region src/lib/components/ColumnResizer.svelte
166
250
  function ColumnResizer($$renderer, $$props) {
167
251
  $$renderer.component(($$renderer) => {
@@ -189,11 +273,12 @@ function RangeControls($$renderer, $$props) {
189
273
  /** Active custom bounds as epoch ms, owned by the page. */
190
274
  /** True when no group is selected, so there is nothing to scope. */
191
275
  /** True while a historic window is being fetched. */
276
+ /** True when the source cannot follow live events, such as the local archive. */
192
277
  /** Called when the user switches mode or applies a range. */
193
- let { mode = "live", range = "15m", from = null, to = null, disabled = false, loading = false} = $$props;
278
+ let { mode = "live", range = "15m", from = null, to = null, disabled = false, loading = false, liveDisabled = false} = $$props;
194
279
  /** Label shown on the custom chip. */
195
280
  let customLabel = derived(() => mode === "historic" && range === "" && from !== null && to !== null ? `${toDateTimeLocal(from).replace("T", " ")} → ${toDateTimeLocal(to).replace("T", " ")}` : "Custom…");
196
- $$renderer.push(`<div class="flex flex-wrap items-center gap-2" data-testid="range-controls"><div class="flex items-center gap-1 rounded-lg border border-neutral-800 bg-neutral-900 p-0.5" role="group" aria-label="Tail mode"><button type="button"${attr("aria-pressed", mode === "live")} data-testid="mode-live"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${mode === "live" ? "border-sky-700 bg-sky-950/60 text-sky-300" : "border-transparent text-neutral-400 hover:text-neutral-200"}`)}>Live</button> <button type="button"${attr("aria-pressed", mode === "historic")} data-testid="mode-historic"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${mode === "historic" ? "border-amber-700 bg-amber-950/50 text-amber-300" : "border-transparent text-neutral-400 hover:text-neutral-200"}`)}>Historic</button></div> `);
281
+ $$renderer.push(`<div class="flex flex-wrap items-center gap-2" data-testid="range-controls"><div class="flex items-center gap-1 rounded-lg border border-neutral-800 bg-neutral-900 p-0.5" role="group" aria-label="Tail mode"><button type="button"${attr("aria-pressed", mode === "live")}${attr("disabled", liveDisabled, true)}${attr("title", liveDisabled ? "There is nothing to tail: the local archive holds historic windows" : void 0)} data-testid="mode-live"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${mode === "live" ? "border-sky-700 bg-sky-950/60 text-sky-300" : "border-transparent text-neutral-400 hover:text-neutral-200"} disabled:opacity-40`)}>Live</button> <button type="button"${attr("aria-pressed", mode === "historic")} data-testid="mode-historic"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${mode === "historic" ? "border-amber-700 bg-amber-950/50 text-amber-300" : "border-transparent text-neutral-400 hover:text-neutral-200"}`)}>Historic</button></div> `);
197
282
  if (mode === "historic") {
198
283
  $$renderer.push(`<!--[0--><div class="flex flex-wrap items-center gap-1" role="group" aria-label="Historic range"><!--[-->`);
199
284
  const each_array = ensure_array_like(HISTORIC_PRESETS);
@@ -216,8 +301,8 @@ function RangeControls($$renderer, $$props) {
216
301
  //#region src/lib/groups-client.ts
217
302
  /**
218
303
  * Browser-side fetch helpers for the watch-stream JSON API (`/api/health`, `/api/regions`,
219
- * `/api/log-groups`). Every helper throws {@link ApiError} on a non-2xx response so the UI can show
220
- * the API `error` message instead of an opaque failure.
304
+ * `/api/log-groups`, `/api/archive`). Every helper throws {@link ApiError} on a non-2xx response so
305
+ * the UI can show the API `error` message instead of an opaque failure.
221
306
  */
222
307
  /** Region list used by the picker before (or instead of) `/api/regions`. */
223
308
  var DEFAULT_REGIONS = REGION_CODES;
@@ -251,11 +336,30 @@ function apiErrorMessage(value, fallback = "Unexpected API error") {
251
336
  function buildLogGroupsUrl(query) {
252
337
  const search = new URLSearchParams();
253
338
  if (query.region) search.set("region", query.region);
339
+ if (query.source) search.set("source", query.source);
254
340
  if (query.prefix) search.set("prefix", query.prefix);
255
341
  if (typeof query.limit === "number" && Number.isFinite(query.limit)) search.set("limit", String(query.limit));
256
342
  const suffix = search.toString();
257
343
  return suffix === "" ? "/api/log-groups" : `/api/log-groups?${suffix}`;
258
344
  }
345
+ /** Builds the `/api/series` URL for a query. */
346
+ function buildSeriesUrl(query) {
347
+ const search = new URLSearchParams();
348
+ if (query.region) search.set("region", query.region);
349
+ search.set("source", "archive");
350
+ const groups = (query.groups ?? []).filter((name) => name.length > 0);
351
+ if (groups.length > 0) search.set("groups", groups.join(","));
352
+ if (typeof query.from === "number" && Number.isFinite(query.from)) search.set("from", String(Math.round(query.from)));
353
+ if (typeof query.to === "number" && Number.isFinite(query.to)) search.set("to", String(Math.round(query.to)));
354
+ const levels = (query.levels ?? []).filter((level) => level.length > 0);
355
+ if (levels.length > 0) search.set("level", levels.join(","));
356
+ if (typeof query.bucketMs === "number" && Number.isFinite(query.bucketMs) && query.bucketMs > 0) search.set("bucket", String(Math.round(query.bucketMs)));
357
+ return `/api/series?${search.toString()}`;
358
+ }
359
+ /** `GET /api/series` - bucketed event counts for the chart. */
360
+ function fetchSeries(query) {
361
+ return getJson(buildSeriesUrl(query), query);
362
+ }
259
363
  /** True for an abort, which is a normal way to cancel a request. */
260
364
  function isAbort(error) {
261
365
  return error instanceof Error && error.name === "AbortError";
@@ -287,7 +391,11 @@ async function getJson(url, options) {
287
391
  });
288
392
  return body;
289
393
  }
290
- /** `GET /api/log-groups` - log groups for a region. */
394
+ /**
395
+ * `GET /api/log-groups` - log groups for a region, from CloudWatch or the local archive.
396
+ *
397
+ * The archive list needs no credentials and never reaches AWS.
398
+ */
291
399
  function fetchLogGroups(query) {
292
400
  return getJson(buildLogGroupsUrl(query), query);
293
401
  }
@@ -295,6 +403,18 @@ function fetchLogGroups(query) {
295
403
  function noGroupsMessage(region) {
296
404
  return `No log groups found in ${region}. Start floci and run \`pnpm seed\` to create sample groups.`;
297
405
  }
406
+ /** Plain statement of what the archive source shows, used next to the toggle. */
407
+ var ARCHIVE_NOTE = "Locally archived events - read from the local DuckDB file, no CloudWatch credentials, historic windows only.";
408
+ /** Tooltip for the archive indicator: what it shows and which file backs it. */
409
+ function describeArchive(path) {
410
+ const trimmed = typeof path === "string" ? path.trim() : "";
411
+ const head = "Locally archived events instead of live CloudWatch data";
412
+ return trimmed === "" ? head : `${head} (${trimmed})`;
413
+ }
414
+ /** Message shown when the archive holds nothing for a region. */
415
+ function noArchivedGroupsMessage(region) {
416
+ return `Nothing archived for ${region === "" ? "this region" : region} yet - stream a group from CloudWatch once and it will appear here.`;
417
+ }
298
418
  /** Drops a trailing period so an appended sentence does not double up. */
299
419
  function trimSentence(text) {
300
420
  return text.trim().replace(/\.+$/, "");
@@ -316,25 +436,57 @@ function describeStreamError(region, group, message) {
316
436
  function LogGroupList($$renderer, $$props) {
317
437
  $$renderer.component(($$renderer) => {
318
438
  /** Groups returned for the active region. */
439
+ /** Where the list came from; the archive list shows local counts instead of sizes. */
319
440
  /** Active region, used for context in the empty state. */
320
- /** Group name currently being tailed. */
441
+ /** Names of the groups currently being tailed; empty when none are selected. */
321
442
  /** API error message, shown instead of the list. */
322
443
  /** Row cap; defaults to {@link MAX_GROUP_ROWS}. */
323
- /** Called with the group name when a row is clicked. */
444
+ /** Called with the group name when a row is clicked, replacing the selection. */
445
+ /** Called with the group name when its checkbox is toggled in or out of the selection. */
324
446
  /** Called when the refresh button is clicked. */
325
- let { groups = [], region = "", selected = null, loading = false, error = null, maxRows = 500} = $$props;
447
+ let { groups = [], source = "cloudwatch", region = "", selected = [], loading = false, error = null, maxRows = 500} = $$props;
326
448
  let search = "";
449
+ /** True when the list holds archived groups rather than live CloudWatch ones. */
450
+ let archived = derived(() => source === "archive");
451
+ /** Badge text for a group: how many events the archive holds for it. */
452
+ function archivedCount(group) {
453
+ return group.archivedEvents === void 0 ? null : `${formatCount(group.archivedEvents)} archived`;
454
+ }
455
+ /** `oldest -> newest` span of a group's archived events, or `null` when unknown. */
456
+ function archivedSpan(group) {
457
+ if (group.archivedOldest === void 0 || group.archivedNewest === void 0) return null;
458
+ return `${formatTimestamp(group.archivedOldest)} \u2192 ${formatTimestamp(group.archivedNewest)}`;
459
+ }
327
460
  /** Groups matching the search box, case-insensitively. */
328
461
  let filtered = derived(() => groups.filter((group) => group.name.toLowerCase().includes(search.trim().toLowerCase())));
329
462
  /** Rows actually rendered, capped at `maxRows`. */
330
463
  let visible = derived(() => filtered().slice(0, maxRows));
331
464
  let truncated = derived(() => filtered().length > visible().length);
332
465
  let query = derived(() => search.trim());
333
- /** Row styling, highlighting the group that is currently being tailed. */
466
+ /**
467
+ * Selection as a flat list of names. `selected` is typed as a list, but callers that have not
468
+ * migrated yet may still pass a single name (or `null`), so those are normalised here.
469
+ */
470
+ function selectionList(value) {
471
+ if (value === null || value === void 0) return [];
472
+ return Array.isArray(value) ? value : [value];
473
+ }
474
+ let activeSelection = derived(() => selectionList(selected));
475
+ /** True when `name` is part of the current (possibly multi-group) selection. */
476
+ function isSelected(name) {
477
+ return activeSelection().includes(name);
478
+ }
479
+ /** Row styling, highlighting every group that is currently being tailed. */
334
480
  function rowTone(name) {
335
- return name === selected ? "border-sky-800 bg-sky-950/50 text-sky-200" : "border-transparent bg-neutral-900/60 text-neutral-300 hover:border-neutral-800 hover:text-neutral-100";
481
+ return isSelected(name) ? "border-sky-800 bg-sky-950/50 text-sky-200" : "border-transparent bg-neutral-900/60 text-neutral-300 hover:border-neutral-800 hover:text-neutral-100";
336
482
  }
337
- $$renderer.push(`<section class="flex min-h-0 flex-1 flex-col gap-3"><div class="flex items-center justify-between gap-2"><div class="flex items-baseline gap-2"><h2 class="text-sm font-semibold text-neutral-200">Log groups</h2> <span class="text-xs text-neutral-500" data-testid="group-count">${escape_html(formatCount(filtered().length))} of ${escape_html(formatCount(groups.length))}</span></div> <button type="button"${attr("disabled", loading, true)} class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100 disabled:cursor-not-allowed disabled:opacity-50">${escape_html(loading ? "Refreshing…" : "Refresh")}</button></div> <div class="flex flex-col gap-1.5"><label class="sr-only" for="group-search">Search log groups</label> <input id="group-search" type="search" placeholder="Search log groups"${attr("value", search)} class="w-full rounded-md border border-neutral-800 bg-neutral-900 px-2.5 py-1.5 text-sm text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 hover:border-neutral-700 focus:border-sky-600"/></div> `);
483
+ $$renderer.push(`<section class="flex min-h-0 flex-1 flex-col gap-3"><div class="flex items-center justify-between gap-2"><div class="flex items-baseline gap-2"><h2 class="text-sm font-semibold text-neutral-200">${escape_html(archived() ? "Archived groups" : "Log groups")}</h2> <span class="text-xs text-neutral-500" data-testid="group-count">${escape_html(formatCount(filtered().length))} of ${escape_html(formatCount(groups.length))}</span> `);
484
+ if (activeSelection().length > 1) $$renderer.push(`<!--[0--><span class="text-xs font-medium text-sky-400" data-testid="selected-count">${escape_html(formatCount(activeSelection().length))} selected</span>`);
485
+ else $$renderer.push("<!--[-1-->");
486
+ $$renderer.push(`<!--]--> `);
487
+ if (archived()) $$renderer.push(`<!--[0--><span class="rounded-full border border-teal-900 bg-teal-950/50 px-1.5 py-0.5 text-[0.625rem] font-medium text-teal-300" data-testid="group-source-badge">local</span>`);
488
+ else $$renderer.push("<!--[-1-->");
489
+ $$renderer.push(`<!--]--></div> <button type="button"${attr("disabled", loading, true)} class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100 disabled:cursor-not-allowed disabled:opacity-50">${escape_html(loading ? "Refreshing…" : "Refresh")}</button></div> <div class="flex flex-col gap-1.5"><label class="sr-only" for="group-search">Search log groups</label> <input id="group-search" type="search" placeholder="Search log groups"${attr("value", search)} class="w-full rounded-md border border-neutral-800 bg-neutral-900 px-2.5 py-1.5 text-sm text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 hover:border-neutral-700 focus:border-sky-600"/></div> `);
338
490
  if (error !== null && error !== "") $$renderer.push(`<!--[0--><p role="alert" data-testid="group-error" class="rounded-md border border-red-900 bg-red-950/50 px-3 py-2 text-xs leading-5 text-red-300">${escape_html(error)}</p>`);
339
491
  else if (loading && groups.length === 0) {
340
492
  $$renderer.push(`<!--[1--><div class="flex flex-col gap-2" data-testid="group-loading" aria-busy="true"><!--[-->`);
@@ -344,15 +496,23 @@ function LogGroupList($$renderer, $$props) {
344
496
  $$renderer.push(`<div class="h-5 animate-pulse rounded bg-neutral-800/80"></div>`);
345
497
  }
346
498
  $$renderer.push(`<!--]--> <p class="text-xs text-neutral-500">Loading log groups…</p></div>`);
347
- } else if (groups.length === 0) $$renderer.push(`<!--[2--><p data-testid="group-empty" class="rounded-md border border-neutral-800 bg-neutral-900/60 px-3 py-2 text-xs leading-5 text-neutral-400">${escape_html(noGroupsMessage(region))}</p>`);
499
+ } else if (groups.length === 0) $$renderer.push(`<!--[2--><p data-testid="group-empty" class="rounded-md border border-neutral-800 bg-neutral-900/60 px-3 py-2 text-xs leading-5 text-neutral-400">${escape_html(archived() ? noArchivedGroupsMessage(region) : noGroupsMessage(region))}</p>`);
348
500
  else if (visible().length === 0) $$renderer.push(`<!--[3--><p data-testid="group-no-match" class="px-1 py-2 text-xs text-neutral-500">No log groups match “${escape_html(query())}”.</p>`);
349
501
  else {
350
502
  $$renderer.push(`<!--[-1--><ul class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto pr-1"><!--[-->`);
351
503
  const each_array_1 = ensure_array_like(visible());
352
504
  for (let $$index_1 = 0, $$length = each_array_1.length; $$index_1 < $$length; $$index_1++) {
353
505
  let group = each_array_1[$$index_1];
354
- $$renderer.push(`<li><button type="button" data-testid="group-row"${attr("aria-current", group.name === selected ? "true" : void 0)}${attr_class(`flex w-full items-center justify-between gap-2 rounded-md border px-2.5 py-1.5 text-left text-xs transition-colors ${stringify(rowTone(group.name))}`)}><span class="truncate font-mono">${escape_html(group.name)}</span> `);
355
- if (group.storedBytes !== void 0) $$renderer.push(`<!--[0--><span class="shrink-0 text-[0.6875rem] text-neutral-500">${escape_html(formatBytes(group.storedBytes))}</span>`);
506
+ $$renderer.push(`<li class="flex items-center gap-1.5"><input type="checkbox"${attr("data-testid", `group-check-${stringify(group.name)}`)}${attr("aria-label", `Include ${group.name} in the selection`)}${attr("checked", isSelected(group.name), true)} class="size-3.5 shrink-0 cursor-pointer accent-sky-500"/> <button type="button" data-testid="group-row"${attr("aria-current", isSelected(group.name) ? "true" : void 0)}${attr_class(`flex min-w-0 flex-1 items-center justify-between gap-2 rounded-md border px-2.5 py-1.5 text-left text-xs transition-colors ${stringify(rowTone(group.name))}`)}><span class="truncate font-mono">${escape_html(group.name)}</span> `);
507
+ if (archived()) {
508
+ $$renderer.push(`<!--[0--><span class="flex shrink-0 items-center gap-1.5">`);
509
+ if (archivedCount(group) !== null) $$renderer.push(`<!--[0--><span data-testid="group-archived-count"${attr("title", archivedSpan(group) ?? "Events held in the local DuckDB archive")} class="rounded-full border border-teal-900 bg-teal-950/50 px-1.5 py-0.5 text-[0.625rem] font-medium text-teal-300">${escape_html(archivedCount(group))}</span>`);
510
+ else $$renderer.push("<!--[-1-->");
511
+ $$renderer.push(`<!--]--> `);
512
+ if (archivedSpan(group) !== null) $$renderer.push(`<!--[0--><span class="hidden text-[0.6875rem] text-neutral-500 xl:inline" data-testid="group-archived-span">${escape_html(archivedSpan(group))}</span>`);
513
+ else $$renderer.push("<!--[-1-->");
514
+ $$renderer.push(`<!--]--></span>`);
515
+ } else if (group.storedBytes !== void 0) $$renderer.push(`<!--[1--><span class="shrink-0 text-[0.6875rem] text-neutral-500">${escape_html(formatBytes(group.storedBytes))}</span>`);
356
516
  else $$renderer.push("<!--[-1-->");
357
517
  $$renderer.push(`<!--]--></button></li>`);
358
518
  }
@@ -402,297 +562,6 @@ function StatusBadge($$renderer, $$props) {
402
562
  $$renderer.push(`<span data-testid="status-badge"${attr("data-status", status)}${attr_class(`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium ${stringify(tone())}`)}><span${attr_class(`size-1.5 rounded-full ${stringify(dot())}`)} aria-hidden="true"></span> ${escape_html(text())}</span>`);
403
563
  }
404
564
  //#endregion
405
- //#region src/lib/log-buffer.ts
406
- /** Maximum number of lines kept in the visible client buffer. */
407
- var DEFAULT_CAPACITY = 5e3;
408
- var ERROR_PATTERN = /\b(ERROR|FATAL|CRITICAL|CRIT|PANIC|EXCEPTION|TRACEBACK|SEVERE|FAILED|FAILURE)\b/;
409
- var WARN_PATTERN = /\b(WARN|WARNING)\b/;
410
- var DEBUG_PATTERN = /\b(DEBUG|TRACE|VERBOSE)\b/;
411
- /**
412
- * Guesses the severity of a log line from its text.
413
- * Plain structured logs such as `{"level":"WARN"}` are detected too, because the level word is
414
- * matched anywhere in the message. Order of precedence: error, warn, debug, info.
415
- */
416
- function detectLevel(message) {
417
- const upper = (message ?? "").toUpperCase();
418
- if (ERROR_PATTERN.test(upper)) return "error";
419
- if (WARN_PATTERN.test(upper)) return "warn";
420
- if (DEBUG_PATTERN.test(upper)) return "debug";
421
- return "info";
422
- }
423
- /** Tailwind text colour class for a severity. */
424
- function levelColorClass(level) {
425
- switch (level) {
426
- case "error": return "text-red-400";
427
- case "warn": return "text-amber-300";
428
- case "debug": return "text-neutral-500";
429
- default: return "text-neutral-200";
430
- }
431
- }
432
- /** Stable key for rendering a log line, since event ids can repeat or be missing. */
433
- function eventKey(event, index) {
434
- return `${event.id ?? "no-id"}:${event.timestamp}:${index}`;
435
- }
436
- /** Case-insensitive substring match over the message and the stream name. */
437
- function matchesFilter(event, query) {
438
- const needle = (query ?? "").trim().toLowerCase();
439
- if (needle === "") return true;
440
- return `${event.message ?? ""} ${event.streamName ?? ""}`.toLowerCase().includes(needle);
441
- }
442
- /** Returns the events matching `query`, in arrival order. */
443
- function filterEvents(events, query) {
444
- if ((query ?? "").trim() === "") return events.slice();
445
- return events.filter((event) => matchesFilter(event, query));
446
- }
447
- /** Creates an array of `length` undefined slots. */
448
- function emptySlots(length) {
449
- return Array.from({ length });
450
- }
451
- /**
452
- * Fixed-capacity ring buffer that overwrites the oldest item once it is full.
453
- */
454
- var LogRingBuffer = class {
455
- /** Maximum number of items kept. */
456
- capacity;
457
- #items;
458
- #start = 0;
459
- #size = 0;
460
- #dropped = 0;
461
- constructor(capacity = DEFAULT_CAPACITY) {
462
- if (!Number.isInteger(capacity) || capacity <= 0) throw new RangeError(`capacity must be a positive integer, received ${capacity}`);
463
- this.capacity = capacity;
464
- this.#items = emptySlots(capacity);
465
- }
466
- /** Number of items currently stored. */
467
- get size() {
468
- return this.#size;
469
- }
470
- /** Number of items discarded because the buffer was full. */
471
- get dropped() {
472
- return this.#dropped;
473
- }
474
- /** True when the buffer holds `capacity` items. */
475
- get full() {
476
- return this.#size === this.capacity;
477
- }
478
- /** Appends one item, dropping the oldest item when the buffer is full. */
479
- push(item) {
480
- if (this.#size === this.capacity) {
481
- this.#dropped += 1;
482
- this.#start = (this.#start + 1) % this.capacity;
483
- this.#size -= 1;
484
- }
485
- const index = (this.#start + this.#size) % this.capacity;
486
- this.#items[index] = item;
487
- this.#size += 1;
488
- }
489
- /** Appends several items in order. */
490
- pushMany(items) {
491
- for (const item of items) this.push(item);
492
- }
493
- /** Copies the stored items into an array ordered oldest to newest. */
494
- toArray() {
495
- const output = emptySlots(this.#size);
496
- for (let offset = 0; offset < this.#size; offset += 1) output[offset] = this.#items[(this.#start + offset) % this.capacity];
497
- return output;
498
- }
499
- /** Oldest stored item, or `undefined` when empty. */
500
- first() {
501
- return this.#size === 0 ? void 0 : this.#items[this.#start];
502
- }
503
- /** Newest stored item, or `undefined` when empty. */
504
- last() {
505
- return this.#size === 0 ? void 0 : this.#items[(this.#start + this.#size - 1) % this.capacity];
506
- }
507
- /** Removes every item and resets the dropped counter. */
508
- clear() {
509
- this.#items = emptySlots(this.capacity);
510
- this.#start = 0;
511
- this.#size = 0;
512
- this.#dropped = 0;
513
- }
514
- };
515
- /**
516
- * Log store used by the UI: a visible ring buffer plus a pending buffer that fills up while the
517
- * view is paused. `push()` never drops silent data, it either appends to the visible buffer or to
518
- * the pending buffer; `resume()` moves the pending events into the visible buffer in order.
519
- */
520
- var LogStore = class {
521
- buffer;
522
- #pending;
523
- #paused = false;
524
- constructor(capacity = DEFAULT_CAPACITY) {
525
- this.buffer = new LogRingBuffer(capacity);
526
- this.#pending = new LogRingBuffer(capacity);
527
- }
528
- /** True when incoming events are buffered instead of appended to the visible buffer. */
529
- get paused() {
530
- return this.#paused;
531
- }
532
- /** Number of visible lines. */
533
- get size() {
534
- return this.buffer.size;
535
- }
536
- /** Number of lines held back while paused. */
537
- get pendingCount() {
538
- return this.#pending.size;
539
- }
540
- /** Number of visible lines dropped because the buffer was full. */
541
- get droppedCount() {
542
- return this.buffer.dropped;
543
- }
544
- /** Appends events to the visible buffer, or holds them back while paused. */
545
- push(events) {
546
- if (this.#paused) {
547
- this.#pending.pushMany(events);
548
- return;
549
- }
550
- this.buffer.pushMany(events);
551
- }
552
- /** Pauses or resumes appending. Resuming flushes the pending events immediately. */
553
- setPaused(paused) {
554
- if (paused === this.#paused) return [];
555
- this.#paused = paused;
556
- return paused ? [] : this.flush();
557
- }
558
- /** Pauses appending to the visible buffer. */
559
- pause() {
560
- this.#paused = true;
561
- }
562
- /** Resumes appending and returns the events flushed from the pending buffer. */
563
- resume() {
564
- this.#paused = false;
565
- return this.flush();
566
- }
567
- /** Moves every pending event into the visible buffer, oldest first. */
568
- flush() {
569
- const flushed = this.#pending.toArray();
570
- this.#pending.clear();
571
- this.buffer.pushMany(flushed);
572
- return flushed;
573
- }
574
- /** Copies the visible lines, oldest first. */
575
- toArray() {
576
- return this.buffer.toArray();
577
- }
578
- /** Empties the visible buffer and discards pending events. */
579
- clear() {
580
- this.buffer.clear();
581
- this.#pending.clear();
582
- }
583
- };
584
- //#endregion
585
- //#region src/lib/log-format.ts
586
- /**
587
- * Parses a log message that is entirely JSON.
588
- *
589
- * Only objects and arrays count: a message that happens to be `42` or `"ok"` is
590
- * left alone, and anything that does not parse is not JSON. Leading and trailing
591
- * whitespace is ignored.
592
- */
593
- function detectJson(message) {
594
- if (typeof message !== "string") return null;
595
- const trimmed = message.trim();
596
- if (trimmed.length === 0) return null;
597
- const first = trimmed[0];
598
- if (first !== "{" && first !== "[") return null;
599
- try {
600
- const parsed = JSON.parse(trimmed);
601
- if (parsed === null || typeof parsed !== "object") return null;
602
- return parsed;
603
- } catch {
604
- return null;
605
- }
606
- }
607
- /**
608
- * Returns the text to render for a log message.
609
- *
610
- * `prettyJson` is normally true: JSON messages are re-serialised with two-space
611
- * indentation. With `prettyJson` false the raw message is returned unchanged,
612
- * which is what the viewer's JSON toggle switches to.
613
- */
614
- function formatLogMessage(message, options = {}) {
615
- const { prettyJson = true } = options;
616
- const value = detectJson(message);
617
- if (value === null || !prettyJson) return {
618
- text: message,
619
- isJson: value !== null,
620
- value
621
- };
622
- return {
623
- text: JSON.stringify(value, null, 2),
624
- isJson: true,
625
- value
626
- };
627
- }
628
- var NUMBER_PATTERN = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
629
- /**
630
- * Splits pretty-printed JSON into coloured tokens.
631
- *
632
- * The concatenation of every token's `text` is always identical to the input, so
633
- * rendering tokens can never lose or alter a character. This is a display-only
634
- * scanner: it assumes well-formed JSON produced by `JSON.stringify`.
635
- */
636
- function tokenizeJson(text) {
637
- const tokens = [];
638
- let index = 0;
639
- const push = (type, value) => {
640
- if (value.length > 0) tokens.push({
641
- type,
642
- text: value
643
- });
644
- };
645
- while (index < text.length) {
646
- const char = text[index];
647
- if (char === " " || char === "\n" || char === " " || char === "\r") {
648
- let end = index;
649
- while (end < text.length && " \n \r".includes(text[end])) end += 1;
650
- push("whitespace", text.slice(index, end));
651
- index = end;
652
- continue;
653
- }
654
- if (char === "\"") {
655
- let end = index + 1;
656
- while (end < text.length) {
657
- if (text[end] === "\\") {
658
- end += 2;
659
- continue;
660
- }
661
- if (text[end] === "\"") break;
662
- end += 1;
663
- }
664
- const raw = text.slice(index, Math.min(end + 1, text.length));
665
- let lookahead = index + raw.length;
666
- while (lookahead < text.length && " \n \r".includes(text[lookahead])) lookahead += 1;
667
- push(text[lookahead] === ":" ? "key" : "string", raw);
668
- index += raw.length;
669
- continue;
670
- }
671
- if (char === "{" || char === "}" || char === "[" || char === "]" || char === "," || char === ":") {
672
- push("punctuation", char);
673
- index += 1;
674
- continue;
675
- }
676
- if (text.startsWith("true", index) || text.startsWith("false", index)) {
677
- const literal = text.startsWith("true", index) ? "true" : "false";
678
- push("boolean", literal);
679
- index += literal.length;
680
- continue;
681
- }
682
- if (text.startsWith("null", index)) {
683
- push("null", "null");
684
- index += 4;
685
- continue;
686
- }
687
- let end = index;
688
- while (end < text.length && !"{}[],:\" \n \r".includes(text[end])) end += 1;
689
- const raw = text.slice(index, Math.max(end, index + 1));
690
- push(NUMBER_PATTERN.test(raw) ? "number" : "other", raw);
691
- index += raw.length;
692
- }
693
- return tokens;
694
- }
695
- //#endregion
696
565
  //#region src/lib/components/LogViewer.svelte
697
566
  function LogViewer($$renderer, $$props) {
698
567
  $$renderer.component(($$renderer) => {
@@ -710,7 +579,15 @@ function LogViewer($$renderer, $$props) {
710
579
  /** Active tail mode, used for the window chip. */
711
580
  /** Last `ready` payload, which describes the window the server is scanning. */
712
581
  /** Reason the server ended the stream, for example `window-complete`. */
713
- let { lines = [], status = "idle", error = null, region = "", group = null, filter = "", paused = false, autoScroll = true, pendingCount = 0, droppedCount = 0, receivedCount = 0, mode = "live", ready = null, endReason = null} = $$props;
582
+ /** Database file behind the local archive, shown in the archive tooltip. */
583
+ /** Log groups in the view; more than one adds a group column. */
584
+ /**
585
+ * Active level filter. Leave it undefined to let the viewer own the filter;
586
+ * pass a value (including `null`) to control it from outside, which is how
587
+ * the chart above stays in step with the chips.
588
+ */
589
+ /** Called when the level filter changes while it is controlled. */
590
+ let { lines = [], status = "idle", error = null, region = "", group = null, filter = "", paused = false, autoScroll = true, pendingCount = 0, droppedCount = 0, receivedCount = 0, mode = "live", ready = null, endReason = null, archivePath = null, groups = [], level = void 0, onLevelChange, onFilterChange, onPauseToggle, onClear, onAutoScrollToggle } = $$props;
714
591
  const TIMESTAMP_COLUMN_PX = remToPx(7.5);
715
592
  const ROW_PADDING_PX = remToPx(.75);
716
593
  const ROW_GAP_PX = remToPx(.5);
@@ -729,28 +606,88 @@ function LogViewer($$renderer, $$props) {
729
606
  let jsonView = true;
730
607
  let prefixPx = PREFIX_DEFAULT_PX;
731
608
  /** One rendered row, pre-formatted so the template stays declarative. */
732
- /** Lines matching the client-side filter. */
733
- let visible = derived(() => filterEvents(lines, filter));
609
+ /**
610
+ * Payload found inside the raw line, used when pretty-printing is off and
611
+ * the row is expanded. `null` when the line carries no JSON.
612
+ */
613
+ /** Log group the line came from, when the payload carries one. */
614
+ /** Detected level, or `null` when the line carried no signal. */
615
+ /** Level the viewer owns when the parent does not control it. */
616
+ let ownLevelFilter = null;
617
+ /** Active level filter: the parent's when given, otherwise the viewer's. */
618
+ let levelFilter = derived(() => level === void 0 ? ownLevelFilter : level);
619
+ /** The four levels offered as filter chips, in severity order. */
620
+ const LEVEL_CHIPS = [
621
+ {
622
+ level: "error",
623
+ label: "Error",
624
+ activeClass: "border-red-800 bg-red-950/50 text-red-300"
625
+ },
626
+ {
627
+ level: "warn",
628
+ label: "Warn",
629
+ activeClass: "border-amber-800 bg-amber-950/40 text-amber-300"
630
+ },
631
+ {
632
+ level: "info",
633
+ label: "Info",
634
+ activeClass: "border-neutral-700 bg-neutral-800 text-neutral-100"
635
+ },
636
+ {
637
+ level: "debug",
638
+ label: "Debug",
639
+ activeClass: "border-neutral-700 bg-neutral-800 text-neutral-400"
640
+ }
641
+ ];
642
+ /** Lines matching the text filter and the level filter. */
643
+ let visible = derived(() => filterByLevel(filterEvents(lines, filter), levelFilter()));
644
+ /** How many lines of each level are loaded, including `unknown`. */
645
+ let levelCounts = derived(() => {
646
+ const counts = {
647
+ error: 0,
648
+ warn: 0,
649
+ info: 0,
650
+ debug: 0,
651
+ unknown: 0
652
+ };
653
+ for (const event of lines) {
654
+ const rowLevel = effectiveLevel(event);
655
+ counts[rowLevel ?? "unknown"] = (counts[rowLevel ?? "unknown"] ?? 0) + 1;
656
+ }
657
+ return counts;
658
+ });
734
659
  /** Presentation rows for the visible lines. */
735
660
  let rows = derived(() => visible().map((event, index) => {
736
- const level = detectLevel(event.message);
661
+ const rowLevel = event.level !== void 0 ? event.level : detectLevel(event.message);
737
662
  const formatted = formatLogMessage(event.message, { prettyJson: jsonView });
663
+ const levelClass = levelColorClass(rowLevel);
738
664
  return {
739
665
  key: eventKey(event, index),
740
666
  time: formatTime(event.timestamp),
741
667
  timestamp: formatTimestamp(event.timestamp),
742
668
  streamName: event.streamName,
669
+ group: event.group ?? null,
743
670
  message: formatted.text,
744
671
  tokens: formatted.isJson ? tokenizeJson(formatted.text) : null,
745
- level,
746
- levelClass: levelColorClass(level)
672
+ expandable: null,
673
+ level: rowLevel,
674
+ levelClass
747
675
  };
748
676
  }));
749
677
  /** Keeps the newest line in view while auto-scroll is on (vertical only). */
750
678
  /** Error text with the group and region context. */
751
679
  let errorText = derived(() => error === null ? null : describeStreamError(region, group, error.message));
680
+ /** True when the ready frame says the events come from the local archive. */
681
+ let archived = derived(() => ready?.source === "archive");
682
+ /**
683
+ * Status the badge shows. The archive has no live tail: it replays a fixed window and ends by
684
+ * itself, so its connecting/live/reconnecting states would all be misleading.
685
+ */
686
+ let displayStatus = derived(() => archived() && (status === "connecting" || status === "live" || status === "reconnecting") ? "idle" : status);
687
+ /** Tooltip of the archive indicator, naming the database file it reads. */
688
+ let archiveTitle = derived(() => describeArchive(archivePath));
752
689
  /** Body text when there is nothing to render. */
753
- let emptyText = derived(() => group === null || group === "" ? "Select a log group to start tailing." : filter.trim() === "" ? `Waiting for events from ${group}\u2026` : "No lines match the filter.");
690
+ let emptyText = derived(() => group === null || group === "" ? "Select a log group to start tailing." : filter.trim() !== "" ? "No lines match the filter." : archived() ? `No archived events for ${group} in this window.` : `Waiting for events from ${group}\u2026`);
754
691
  /** Auto-scroll button colour. */
755
692
  let autoScrollTone = derived(() => autoScroll ? "text-sky-300" : "text-neutral-400");
756
693
  /** Chip describing the active window. */
@@ -769,6 +706,12 @@ function LogViewer($$renderer, $$props) {
769
706
  let handleLeft = derived(() => ROW_PADDING_PX + TIMESTAMP_COLUMN_PX + ROW_GAP_PX + prefixPx);
770
707
  /** Prefix column width as a CSS length. */
771
708
  let prefixStyle = derived(() => `max-width: ${pxToRem(prefixPx)}`);
709
+ /** Rows the user has opened, keyed by row key. */
710
+ let expandedRows = {};
711
+ /** True when a row is open. */
712
+ function isExpanded(key) {
713
+ return expandedRows[key] === true;
714
+ }
772
715
  /** Colour for a JSON token, keeping punctuation and whitespace quiet. */
773
716
  function tokenClass(type) {
774
717
  switch (type) {
@@ -786,8 +729,11 @@ function LogViewer($$renderer, $$props) {
786
729
  writePref(STORAGE_KEYS.prefixWidth, String(Math.round(prefixPx)));
787
730
  }
788
731
  $$renderer.push(`<section class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-neutral-800 bg-neutral-950"><div class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-neutral-800 bg-neutral-900/40 px-3 py-2">`);
789
- StatusBadge($$renderer, { status });
790
- $$renderer.push(`<!----> <span class="text-xs text-neutral-400" data-testid="visible-count">${escape_html(formatCount(visible().length))} shown</span> <span class="text-xs text-neutral-500" data-testid="received-count">${escape_html(formatCount(receivedCount))} received</span> `);
732
+ StatusBadge($$renderer, { status: displayStatus() });
733
+ $$renderer.push(`<!----> `);
734
+ if (archived()) $$renderer.push(`<!--[0--><span data-testid="archive-badge"${attr("title", archiveTitle())} class="rounded-full border border-teal-900 bg-teal-950/60 px-2 py-0.5 text-[0.6875rem] font-medium text-teal-300">local archive</span>`);
735
+ else $$renderer.push("<!--[-1-->");
736
+ $$renderer.push(`<!--]--> <span class="text-xs text-neutral-400" data-testid="visible-count">${escape_html(formatCount(visible().length))} shown</span> <span class="text-xs text-neutral-500" data-testid="received-count">${escape_html(formatCount(receivedCount))} received</span> `);
791
737
  if (pendingCount > 0) $$renderer.push(`<!--[0--><span class="text-xs text-amber-300" data-testid="pending-count">${escape_html(formatCount(pendingCount))} buffered</span>`);
792
738
  else $$renderer.push("<!--[-1-->");
793
739
  $$renderer.push(`<!--]--> `);
@@ -799,7 +745,16 @@ function LogViewer($$renderer, $$props) {
799
745
  $$renderer.push(`<!--]--> `);
800
746
  if (windowComplete()) $$renderer.push(`<!--[0--><span class="text-[0.6875rem] text-emerald-400/80" data-testid="window-complete">window complete</span>`);
801
747
  else $$renderer.push("<!--[-1-->");
802
- $$renderer.push(`<!--]--> <div class="ml-auto flex flex-wrap items-center gap-2"><input type="search" aria-label="Filter log lines" placeholder="Filter lines"${attr("value", filter)} class="w-40 rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 hover:border-neutral-700 focus:border-sky-600"/> <button type="button"${attr("aria-pressed", wrapLines)} title="Wrap long lines instead of scrolling sideways" data-testid="wrap-toggle"${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${wrapTone()}`)}>Wrap</button> <button type="button"${attr("aria-pressed", jsonView)} title="Pretty-print JSON log entries" data-testid="json-toggle"${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${jsonTone()}`)}>JSON</button> <button type="button"${attr("aria-pressed", paused)} class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100">${escape_html(paused ? "Resume" : "Pause")}</button> <button type="button" class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100">Clear</button> <button type="button"${attr("aria-pressed", autoScroll)}${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${autoScrollTone()}`)}>Auto-scroll</button></div></div> `);
748
+ $$renderer.push(`<!--]--> <div class="ml-auto flex flex-wrap items-center gap-2"><div class="flex items-center gap-1" role="group" aria-label="Filter by level"><button type="button"${attr("aria-pressed", levelFilter() === null)} title="Show every level, including lines with no level" data-testid="level-all"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${levelFilter() === null ? "border-sky-700 bg-sky-950/60 text-sky-200" : "border-neutral-800 bg-neutral-900 text-neutral-400 hover:border-neutral-700"}`)}>All</button> <!--[-->`);
749
+ const each_array = ensure_array_like(LEVEL_CHIPS);
750
+ for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
751
+ let chip = each_array[$$index];
752
+ $$renderer.push(`<button type="button"${attr("aria-pressed", levelFilter() === chip.level)}${attr("title", `Show only ${stringify(chip.label)} lines (${stringify(levelCounts()[chip.level] ?? 0)} loaded)`)}${attr("data-testid", `level-${stringify(chip.level)}`)}${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${stringify(levelFilter() === chip.level ? chip.activeClass : "border-neutral-800 bg-neutral-900 text-neutral-400 hover:border-neutral-700")}`)}>${escape_html(chip.label)} `);
753
+ if ((levelCounts()[chip.level] ?? 0) > 0) $$renderer.push(`<!--[0--><span class="ml-1 opacity-70">${escape_html(formatCount(levelCounts()[chip.level] ?? 0))}</span>`);
754
+ else $$renderer.push("<!--[-1-->");
755
+ $$renderer.push(`<!--]--></button>`);
756
+ }
757
+ $$renderer.push(`<!--]--></div> <input type="search" aria-label="Filter log lines" placeholder="Filter lines"${attr("value", filter)} class="w-40 rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 hover:border-neutral-700 focus:border-sky-600"/> <button type="button"${attr("aria-pressed", wrapLines)} title="Wrap long lines instead of scrolling sideways" data-testid="wrap-toggle"${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${wrapTone()}`)}>Wrap</button> <button type="button"${attr("aria-pressed", jsonView)} title="Pretty-print JSON log entries (off: click a line to open its JSON)" data-testid="json-toggle"${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${jsonTone()}`)}>JSON</button> <button type="button"${attr("aria-pressed", paused)} class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100">${escape_html(paused ? "Resume" : "Pause")}</button> <button type="button" class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100">Clear</button> <button type="button"${attr("aria-pressed", autoScroll)}${attr_class(`rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 text-xs font-medium transition-colors hover:border-neutral-700 ${autoScrollTone()}`)}>Auto-scroll</button></div></div> `);
803
758
  if (errorText() !== null) $$renderer.push(`<!--[0--><p role="alert" data-testid="stream-error" class="border-b border-red-900 bg-red-950/50 px-3 py-2 text-xs leading-5 text-red-300">${escape_html(errorText())}</p>`);
804
759
  else $$renderer.push("<!--[-1-->");
805
760
  $$renderer.push(`<!--]--> <div class="min-h-0 min-w-0 flex-1 overflow-auto bg-neutral-950" data-testid="log-scroller">`);
@@ -807,20 +762,38 @@ function LogViewer($$renderer, $$props) {
807
762
  else if (rows().length === 0) $$renderer.push(`<!--[1--><p class="px-3 py-6 text-sm text-neutral-500" data-testid="viewer-empty">${escape_html(emptyText())}</p>`);
808
763
  else {
809
764
  $$renderer.push(`<!--[-1--><div${attr_class(`relative min-h-full ${listClass()}`)} data-testid="log-canvas"><ol class="font-mono text-xs leading-5" data-testid="log-lines"><!--[-->`);
810
- const each_array = ensure_array_like(rows());
811
- for (let $$index_1 = 0, $$length = each_array.length; $$index_1 < $$length; $$index_1++) {
812
- let row = each_array[$$index_1];
813
- $$renderer.push(`<li${attr_class(`flex items-baseline gap-2 px-3 py-0.5 hover:bg-neutral-900/60 ${listClass()}`)} data-testid="log-line"${attr("data-level", row.level)}><span class="w-[7.5rem] shrink-0 text-neutral-500"${attr("title", row.timestamp)}>${escape_html(row.time)}</span> <span class="shrink-0 truncate text-sky-400/80"${attr_style(prefixStyle())}${attr("title", row.streamName ?? "")} data-testid="log-stream">${escape_html(row.streamName ?? "")}</span> <span${attr_class(`${messageClass()} ${stringify(row.levelClass)}`)} data-testid="log-message">`);
765
+ const each_array_1 = ensure_array_like(rows());
766
+ for (let $$index_3 = 0, $$length = each_array_1.length; $$index_3 < $$length; $$index_3++) {
767
+ let row = each_array_1[$$index_3];
768
+ const expandable = row.expandable !== null;
769
+ const open = expandable && isExpanded(row.key);
770
+ $$renderer.push(`<li${attr_class(`flex flex-wrap items-baseline gap-2 px-3 py-0.5 hover:bg-neutral-900/60 ${listClass()} ${expandable ? "cursor-pointer" : ""}`)} data-testid="log-line"${attr("data-level", row.level ?? "unknown")}${attr("data-expandable", expandable ? "true" : void 0)}${attr("data-expanded", open ? "true" : void 0)}${attr("role", expandable ? "button" : void 0)}${attr("tabindex", expandable ? 0 : void 0)}${attr("aria-expanded", expandable ? open : void 0)}${attr("title", expandable ? open ? "Hide the JSON in this line" : "Show this line as JSON" : void 0)}><span class="w-[7.5rem] shrink-0 text-neutral-500"${attr("title", row.timestamp)}>${escape_html(row.time)}</span> `);
771
+ if (groups.length > 1) $$renderer.push(`<!--[0--><span class="w-[9rem] shrink-0 truncate text-teal-400/80"${attr("title", row.group ?? "")} data-testid="log-group">${escape_html(row.group ?? "")}</span>`);
772
+ else $$renderer.push("<!--[-1-->");
773
+ $$renderer.push(`<!--]--> <span class="shrink-0 truncate text-sky-400/80"${attr_style(prefixStyle())}${attr("title", row.streamName ?? "")} data-testid="log-stream">${escape_html(row.streamName ?? "")}</span> `);
774
+ if (expandable) $$renderer.push(`<!--[0--><span class="shrink-0 text-neutral-600" aria-hidden="true">${escape_html(open ? "▾" : "▸")}</span>`);
775
+ else $$renderer.push("<!--[-1-->");
776
+ $$renderer.push(`<!--]--> <span${attr_class(`${messageClass()} ${stringify(row.levelClass)}`)} data-testid="log-message">`);
814
777
  if (row.tokens !== null) {
815
778
  $$renderer.push(`<!--[0--><!--[-->`);
816
- const each_array_1 = ensure_array_like(row.tokens);
817
- for (let tokenIndex = 0, $$length = each_array_1.length; tokenIndex < $$length; tokenIndex++) {
818
- let token = each_array_1[tokenIndex];
779
+ const each_array_2 = ensure_array_like(row.tokens);
780
+ for (let tokenIndex = 0, $$length = each_array_2.length; tokenIndex < $$length; tokenIndex++) {
781
+ let token = each_array_2[tokenIndex];
819
782
  $$renderer.push(`<span${attr_class(clsx(tokenClass(token.type)))}>${escape_html(token.text)}</span>`);
820
783
  }
821
784
  $$renderer.push(`<!--]-->`);
822
785
  } else $$renderer.push(`<!--[-1-->${escape_html(row.message)}`);
823
- $$renderer.push(`<!--]--></span></li>`);
786
+ $$renderer.push(`<!--]--></span> `);
787
+ if (open && row.expandable !== null) {
788
+ $$renderer.push(`<!--[0--><span class="w-full whitespace-pre text-xs text-neutral-300" data-testid="log-json-expanded"><!--[-->`);
789
+ const each_array_3 = ensure_array_like(tokenizeJson(row.expandable));
790
+ for (let tokenIndex = 0, $$length = each_array_3.length; tokenIndex < $$length; tokenIndex++) {
791
+ let token = each_array_3[tokenIndex];
792
+ $$renderer.push(`<span${attr_class(clsx(tokenClass(token.type)))}>${escape_html(token.text)}</span>`);
793
+ }
794
+ $$renderer.push(`<!--]--></span>`);
795
+ } else $$renderer.push("<!--[-1-->");
796
+ $$renderer.push(`<!--]--></li>`);
824
797
  }
825
798
  $$renderer.push(`<!--]--></ol> `);
826
799
  ColumnResizer($$renderer, {
@@ -870,6 +843,29 @@ function RegionSelect($$renderer, $$props) {
870
843
  $$renderer.push(`</div>`);
871
844
  });
872
845
  }
846
+ //#endregion
847
+ //#region src/lib/components/SourceControls.svelte
848
+ function SourceControls($$renderer, $$props) {
849
+ $$renderer.component(($$renderer) => {
850
+ /** Active source, owned by the page. */
851
+ /**
852
+ * True only when `GET /api/archive` reported `available: true`.
853
+ * The toggle is hidden otherwise, so the app keeps its CloudWatch-only look.
854
+ */
855
+ /** Database file behind the archive, shown in the tooltip. */
856
+ /** Called with the newly chosen source. */
857
+ let { source = "cloudwatch", archiveAvailable = false, archivePath = null} = $$props;
858
+ /** Tooltip of the archive button: what it reads and which file backs it. */
859
+ let archiveTitle = derived(() => describeArchive(archivePath));
860
+ if (archiveAvailable) {
861
+ $$renderer.push(`<!--[0--><div class="flex flex-col gap-1.5"><div class="flex items-center gap-1 rounded-lg border border-neutral-800 bg-neutral-900 p-0.5" role="group" aria-label="Log source"><button type="button"${attr("aria-pressed", source === "cloudwatch")} title="Stream from CloudWatch Logs with your AWS credentials" data-testid="source-cloudwatch"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${source === "cloudwatch" ? "border-sky-700 bg-sky-950/60 text-sky-300" : "border-transparent text-neutral-400 hover:text-neutral-200"}`)}>CloudWatch</button> <button type="button"${attr("aria-pressed", source === "archive")}${attr("title", archiveTitle())} data-testid="source-archive"${attr_class(`rounded-md border px-2 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${source === "archive" ? "border-teal-700 bg-teal-950/60 text-teal-300" : "border-transparent text-neutral-400 hover:text-neutral-200"}`)}>Local archive</button></div> `);
862
+ if (source === "archive") $$renderer.push(`<!--[0--><p class="text-[0.6875rem] leading-4 text-teal-300/80"${attr("title", archiveTitle())} data-testid="archive-note">${escape_html(ARCHIVE_NOTE)}</p>`);
863
+ else $$renderer.push("<!--[-1-->");
864
+ $$renderer.push(`<!--]--></div>`);
865
+ } else $$renderer.push("<!--[-1-->");
866
+ $$renderer.push(`<!--]-->`);
867
+ });
868
+ }
873
869
  function createBrowserEventSource(url) {
874
870
  return new EventSource(url);
875
871
  }
@@ -877,7 +873,10 @@ function createBrowserEventSource(url) {
877
873
  function buildStreamUrl(target, base = "") {
878
874
  const search = new URLSearchParams();
879
875
  search.set("region", target.region);
880
- search.set("group", target.group);
876
+ const groups = (target.groups ?? []).filter((name) => name.length > 0);
877
+ if (groups.length > 1) search.set("groups", groups.join(","));
878
+ else search.set("group", groups[0] ?? target.group);
879
+ if (target.source === "archive") search.set("source", "archive");
881
880
  if (target.filterPattern) search.set("filterPattern", target.filterPattern);
882
881
  if (target.mode === "historic") {
883
882
  search.set("mode", "historic");
@@ -899,7 +898,7 @@ function parsePayload(data) {
899
898
  }
900
899
  }
901
900
  /**
902
- * Live view over one CloudWatch log group.
901
+ * View over one log group: the live CloudWatch stream, or the local archive replay.
903
902
  *
904
903
  * Pause policy: pausing keeps the connection open and keeps receiving events; those events are
905
904
  * held in a pending buffer and flushed into the visible buffer on resume.
@@ -1116,7 +1115,12 @@ function _page($$renderer, $$props) {
1116
1115
  const stream = new LogStream();
1117
1116
  /** Region picker state. Seeded from the URL so a shared link restores the view. */
1118
1117
  let region = page.url.searchParams.get("region") ?? "";
1119
- /** Tail mode and historic window, seeded from the URL. */
1118
+ /**
1119
+ * Where the view reads from, seeded from the URL. `archive` is only offered while the local
1120
+ * DuckDB archive reports `available: true`, so the fallback below is applied during bootstrap.
1121
+ */
1122
+ let source = page.url.searchParams.get("source") === "archive" ? "archive" : "cloudwatch";
1123
+ /** Tail mode and historic window, seeded from the URL; bootstrap forces historic for the archive. */
1120
1124
  let mode = page.url.searchParams.get("mode") === "historic" ? "historic" : "live";
1121
1125
  let range = page.url.searchParams.get("range") ?? "15m";
1122
1126
  let windowFrom = parseUrlEpoch(page.url.searchParams.get("from"));
@@ -1129,8 +1133,25 @@ function _page($$renderer, $$props) {
1129
1133
  let endpoint = null;
1130
1134
  let filter = "";
1131
1135
  let autoScroll = true;
1132
- /** Group currently being tailed, or `null`. */
1133
- let selectedGroup = derived(() => stream.target?.group ?? null);
1136
+ /**
1137
+ * Log groups in the view, seeded from the URL. One entry is the normal case;
1138
+ * more than one tails them together and adds a group column.
1139
+ */
1140
+ let selectedGroups = parseUrlGroups(page.url.searchParams);
1141
+ /** Primary group: the one the viewer names the view after. */
1142
+ let selectedGroup = derived(() => selectedGroups[0] ?? null);
1143
+ /** Level filter shared by the log view and the chart. */
1144
+ let levelFilter = null;
1145
+ /** Bucketed counts behind the chart. */
1146
+ let seriesPoints = [];
1147
+ /** True while the archive counts are being fetched. */
1148
+ let seriesLoading = false;
1149
+ /** True only when the local archive reports that it can be read. */
1150
+ let archiveAvailable = derived(() => false);
1151
+ /** Database file behind the archive, or `null` when it is unknown. */
1152
+ let archivePath = derived(() => null);
1153
+ /** Tooltip of the archive badge in the header. */
1154
+ let archiveTitle = derived(() => describeArchive(archivePath()));
1134
1155
  /** Sidebar width in pixels; the layout default is 22rem. */
1135
1156
  let sidebarPx = remToPx(SIDEBAR_WIDTH.defaultRem);
1136
1157
  /** Largest sidebar width allowed: 44vw, but never below the minimum. */
@@ -1144,21 +1165,29 @@ function _page($$renderer, $$props) {
1144
1165
  localStorage.setItem(STORAGE_KEYS.sidebarWidth, String(Math.round(sidebarPx)));
1145
1166
  } catch {}
1146
1167
  }
1147
- /** Loads the log groups of a region; a newer region change wins the race. */
1168
+ /**
1169
+ * Loads the log groups of a region from the active source; a newer region or source change wins
1170
+ * the race. The CloudWatch URL is left exactly as it was, so only the archive names its source.
1171
+ */
1148
1172
  async function loadGroups(target) {
1149
1173
  groupsLoading = true;
1150
1174
  groupsError = null;
1175
+ const requestedSource = source;
1176
+ const query = requestedSource === "archive" ? {
1177
+ region: target,
1178
+ source: requestedSource
1179
+ } : { region: target };
1151
1180
  try {
1152
- const response = await fetchLogGroups({ region: target });
1153
- if (target !== region) return;
1181
+ const response = await fetchLogGroups(query);
1182
+ if (target !== region || requestedSource !== source) return;
1154
1183
  if (response.endpoint !== null) endpoint = response.endpoint;
1155
1184
  groups = response.groups;
1156
1185
  } catch (error) {
1157
- if (target !== region) return;
1186
+ if (target !== region || requestedSource !== source) return;
1158
1187
  groups = [];
1159
1188
  groupsError = describeGroupsError(target, error);
1160
1189
  } finally {
1161
- if (target === region) groupsLoading = false;
1190
+ if (target === region && requestedSource === source) groupsLoading = false;
1162
1191
  }
1163
1192
  }
1164
1193
  /** Parses an epoch-millisecond URL parameter, or `null`. */
@@ -1167,21 +1196,83 @@ function _page($$renderer, $$props) {
1167
1196
  const parsed = Number(value);
1168
1197
  return Number.isFinite(parsed) ? parsed : null;
1169
1198
  }
1199
+ /** The window the current mode asks for. The archive is always a historic window. */
1200
+ function windowParams() {
1201
+ if (mode === "live" && source !== "archive") return { mode: "live" };
1202
+ if (range === "" && windowFrom !== null && windowTo !== null) return {
1203
+ mode: "historic",
1204
+ from: windowFrom,
1205
+ to: windowTo
1206
+ };
1207
+ return {
1208
+ mode: "historic",
1209
+ range: range === "" ? "15m" : range
1210
+ };
1211
+ }
1212
+ /** Stream target for the active source, region, groups and window. */
1213
+ function streamTarget() {
1214
+ const window = windowParams();
1215
+ const [primary, ...rest] = selectedGroups;
1216
+ const target = {
1217
+ region,
1218
+ group: primary ?? "",
1219
+ groups: [...selectedGroups],
1220
+ ...window
1221
+ };
1222
+ if (source === "archive") target.source = "archive";
1223
+ return target;
1224
+ }
1225
+ /** Applies a mode or range change: restarts the stream and mirrors it into the URL. */
1226
+ function applyRange(payload) {
1227
+ if (source === "archive" && payload.mode === "live") return;
1228
+ mode = payload.mode;
1229
+ range = payload.range;
1230
+ windowFrom = payload.from;
1231
+ windowTo = payload.to;
1232
+ syncUrl(region, selectedGroups);
1233
+ if (selectedGroups.length === 0) return;
1234
+ rangeLoading = payload.mode === "historic";
1235
+ stream.start(streamTarget());
1236
+ loadSeries();
1237
+ }
1238
+ /**
1239
+ * Applies a brushed range from the chart as the new historic window, which
1240
+ * re-scopes the log view to exactly what was brushed.
1241
+ */
1242
+ function applyBrush(selection) {
1243
+ if (selection === null) return;
1244
+ const window = chartWindow();
1245
+ if (!isMeaningfulBrush(selection, window, chartBucketMs(window.from, window.to))) return;
1246
+ applyRange({
1247
+ mode: "historic",
1248
+ range: "",
1249
+ from: selection.from,
1250
+ to: selection.to
1251
+ });
1252
+ }
1170
1253
  /** Switches region: stops the stream, clears the list and reloads the groups. */
1171
1254
  function changeRegion(next) {
1172
1255
  if (next === "" || next === region) return;
1173
1256
  region = next;
1174
1257
  stream.stop();
1175
1258
  groups = [];
1176
- syncUrl(next);
1259
+ selectedGroups = [];
1260
+ seriesPoints = [];
1261
+ syncUrl(next, []);
1177
1262
  loadGroups(next);
1178
1263
  }
1179
1264
  /** Replaces the view parameters in the address bar. */
1180
- function syncUrl(nextRegion, nextGroup) {
1265
+ function syncUrl(nextRegion, nextGroups) {
1181
1266
  const url = new URL(page.url);
1182
1267
  if (nextRegion === "") url.searchParams.delete("region");
1183
1268
  else url.searchParams.set("region", nextRegion);
1269
+ if (source === "archive") url.searchParams.set("source", "archive");
1270
+ else url.searchParams.delete("source");
1184
1271
  url.searchParams.delete("group");
1272
+ url.searchParams.delete("groups");
1273
+ const names = nextGroups.filter((name) => name.length > 0);
1274
+ if (names.length === 1) url.searchParams.set("group", names[0]);
1275
+ else if (names.length > 1) url.searchParams.set("groups", names.join(","));
1185
1276
  if (mode === "live") {
1186
1277
  url.searchParams.delete("mode");
1187
1278
  url.searchParams.delete("range");
@@ -1199,12 +1290,106 @@ function _page($$renderer, $$props) {
1199
1290
  }
1200
1291
  if (url.href !== page.url.href) replaceState();
1201
1292
  }
1293
+ /** Clears the loading hint once the server reports its window. */
1294
+ /**
1295
+ * Keeps the chart in step with the view: a new window, selection, source, level
1296
+ * or a new batch of lines all re-load the counts.
1297
+ */
1298
+ /**
1299
+ * Reacts to `archive-unavailable` from the stream (the file was locked or removed after boot) by
1300
+ * re-checking the archive, so the toggle hides itself instead of offering a source that fails.
1301
+ */
1302
+ /** Parses the group selection from the URL, accepting both parameter shapes. */
1303
+ function parseUrlGroups(params) {
1304
+ const names = (params.get("groups") ?? "").split(",").map((name) => name.trim()).filter((name) => name.length > 0);
1305
+ if (names.length > 0) return [...new Set(names)];
1306
+ const single = params.get("group")?.trim() ?? "";
1307
+ return single.length > 0 ? [single] : [];
1308
+ }
1309
+ /** Bucket width for the chart of one window, mirroring the server's choice. */
1310
+ function chartBucketMs(from, to) {
1311
+ const span = Math.max(1, to - from);
1312
+ return [
1313
+ 1e3,
1314
+ 5e3,
1315
+ 1e4,
1316
+ 3e4,
1317
+ 6e4,
1318
+ 3e5,
1319
+ 9e5,
1320
+ 18e5
1321
+ ].find((step) => span / step <= 90) ?? 36e5;
1322
+ }
1323
+ /** Window the chart describes: the active historic window, or a rolling quarter hour. */
1324
+ function chartWindow() {
1325
+ if (mode === "historic") {
1326
+ const to = windowTo ?? stream.ready?.endTime ?? Date.now();
1327
+ const from = windowFrom ?? stream.ready?.startTime ?? to - 9e5;
1328
+ return {
1329
+ from: Math.min(from, to - 1e3),
1330
+ to
1331
+ };
1332
+ }
1333
+ const to = Date.now();
1334
+ return {
1335
+ from: to - 9e5,
1336
+ to
1337
+ };
1338
+ }
1339
+ /**
1340
+ * Loads the counts behind the chart.
1341
+ *
1342
+ * The archive can count a whole window in one statement, so it is asked for
1343
+ * them; a CloudWatch view has no aggregate endpoint, so the events already
1344
+ * streamed are bucketed here. Both end up as the same points.
1345
+ */
1346
+ async function loadSeries() {
1347
+ if (selectedGroups.length === 0 || region === "") {
1348
+ seriesPoints = [];
1349
+ return;
1350
+ }
1351
+ const { from, to } = chartWindow();
1352
+ if (source !== "archive") {
1353
+ seriesPoints = bucketEvents(stream.lines, {
1354
+ from,
1355
+ to,
1356
+ bucketMs: chartBucketMs(from, to),
1357
+ level: levelFilter,
1358
+ fallbackGroup: selectedGroup() ?? ""
1359
+ });
1360
+ return;
1361
+ }
1362
+ seriesLoading = true;
1363
+ try {
1364
+ seriesPoints = (await fetchSeries({
1365
+ region,
1366
+ groups: selectedGroups,
1367
+ from,
1368
+ to,
1369
+ levels: levelFilter === null ? [] : [levelFilter]
1370
+ })).points;
1371
+ } catch {
1372
+ seriesPoints = [];
1373
+ } finally {
1374
+ seriesLoading = false;
1375
+ }
1376
+ }
1202
1377
  $$renderer.push(`<div class="flex flex-wrap items-center gap-x-3 gap-y-2"><span class="text-[0.6875rem] font-semibold uppercase tracking-wider text-neutral-500">Watching</span> <span class="max-w-[22rem] truncate rounded-md border border-neutral-800 bg-neutral-900 px-2 py-1 font-mono text-xs text-neutral-200">${escape_html(selectedGroup() ?? "no group selected")}</span> <span class="text-xs text-neutral-500">in</span> <span class="font-mono text-xs text-neutral-300">${escape_html(region === "" ? "resolving region…" : region)}</span> `);
1203
- EndpointBadge($$renderer, {
1204
- endpoint,
1205
- credentials: null
1206
- });
1207
- $$renderer.push(`<!----> `);
1378
+ if (source === "archive") {
1379
+ $$renderer.push("<!--[0-->");
1380
+ EndpointBadge($$renderer, {
1381
+ endpoint: null,
1382
+ credentials: null
1383
+ });
1384
+ $$renderer.push(`<!----> <span data-testid="archive-source-badge"${attr("title", archiveTitle())} class="rounded-full border border-teal-900 bg-teal-950/60 px-2.5 py-1 text-xs font-medium text-teal-300">local archive</span>`);
1385
+ } else {
1386
+ $$renderer.push("<!--[-1-->");
1387
+ EndpointBadge($$renderer, {
1388
+ endpoint,
1389
+ credentials: null
1390
+ });
1391
+ }
1392
+ $$renderer.push(`<!--]--> `);
1208
1393
  $$renderer.push("<!--[-1-->");
1209
1394
  $$renderer.push(`<!--]--></div> `);
1210
1395
  $$renderer.push("<!--[-1-->");
@@ -1215,8 +1400,14 @@ function _page($$renderer, $$props) {
1215
1400
  from: windowFrom,
1216
1401
  to: windowTo,
1217
1402
  loading: rangeLoading,
1218
- disabled: selectedGroup() === null});
1403
+ disabled: selectedGroup() === null,
1404
+ liveDisabled: source === "archive"});
1219
1405
  $$renderer.push(`<!----> <div class="flex min-h-0 min-w-0 flex-1 flex-col gap-3 lg:flex-row"><div class="flex min-h-0 w-full flex-col gap-3 rounded-lg border border-neutral-800 bg-neutral-950/60 p-3 lg:shrink-0"${attr_style(sidebarStyle())} data-testid="sidebar">`);
1406
+ SourceControls($$renderer, {
1407
+ source,
1408
+ archiveAvailable: archiveAvailable(),
1409
+ archivePath: archivePath()});
1410
+ $$renderer.push(`<!----> `);
1220
1411
  RegionSelect($$renderer, {
1221
1412
  regions,
1222
1413
  value: region,
@@ -1225,8 +1416,9 @@ function _page($$renderer, $$props) {
1225
1416
  $$renderer.push(`<!----> `);
1226
1417
  LogGroupList($$renderer, {
1227
1418
  groups,
1419
+ source,
1228
1420
  region,
1229
- selected: selectedGroup(),
1421
+ selected: selectedGroups,
1230
1422
  loading: groupsLoading,
1231
1423
  error: groupsError});
1232
1424
  $$renderer.push(`<!----></div> `);
@@ -1241,6 +1433,19 @@ function _page($$renderer, $$props) {
1241
1433
  class: "hidden lg:block lg:w-2"
1242
1434
  });
1243
1435
  $$renderer.push(`<!----> <div class="flex min-h-0 min-w-0 flex-1 flex-col">`);
1436
+ EventScatterPanel($$renderer, {
1437
+ points: seriesPoints,
1438
+ from: chartWindow().from,
1439
+ to: chartWindow().to,
1440
+ bucketMs: chartBucketMs(chartWindow().from, chartWindow().to),
1441
+ groups: selectedGroups,
1442
+ loading: seriesLoading,
1443
+ onBrush: applyBrush
1444
+ });
1445
+ $$renderer.push(`<!----> `);
1446
+ if (windowFrom !== null && windowTo !== null && mode === "historic") $$renderer.push(`<!--[0--><div class="flex flex-wrap items-center gap-2 text-[0.6875rem] text-neutral-500"><span data-testid="brush-window">zoomed to ${escape_html(new Date(windowFrom).toLocaleTimeString())} – ${escape_html(new Date(windowTo).toLocaleTimeString())}</span> <button type="button" data-testid="brush-clear" class="rounded-md border border-neutral-800 bg-neutral-900 px-2 py-0.5 font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:text-neutral-100">Reset zoom</button></div>`);
1447
+ else $$renderer.push("<!--[-1-->");
1448
+ $$renderer.push(`<!--]--> `);
1244
1449
  LogViewer($$renderer, {
1245
1450
  lines: stream.lines,
1246
1451
  status: stream.status,
@@ -1248,17 +1453,26 @@ function _page($$renderer, $$props) {
1248
1453
  mode,
1249
1454
  ready: stream.ready,
1250
1455
  endReason: stream.endReason,
1456
+ archivePath: archivePath(),
1251
1457
  region,
1252
1458
  group: selectedGroup(),
1459
+ groups: selectedGroups,
1460
+ level: levelFilter,
1461
+ onLevelChange: (next) => levelFilter = next,
1253
1462
  filter,
1254
1463
  paused: stream.paused,
1255
1464
  autoScroll,
1256
1465
  pendingCount: stream.pendingCount,
1257
1466
  droppedCount: stream.droppedCount,
1258
- receivedCount: stream.receivedCount});
1467
+ receivedCount: stream.receivedCount,
1468
+ onFilterChange: (value) => filter = value,
1469
+ onPauseToggle: () => stream.togglePause(),
1470
+ onClear: () => stream.clear(),
1471
+ onAutoScrollToggle: () => autoScroll = !autoScroll
1472
+ });
1259
1473
  $$renderer.push(`<!----></div></div>`);
1260
1474
  });
1261
1475
  }
1262
1476
 
1263
1477
  export { _page as default };
1264
- //# sourceMappingURL=_page.svelte.js-DzAAqWz2.js.map
1478
+ //# sourceMappingURL=_page.svelte.js-B-kVwiNh.js.map