plum-e2e 1.1.1 → 1.2.1

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 (334) hide show
  1. package/README.md +142 -70
  2. package/backend/Dockerfile +4 -2
  3. package/backend/_scaffold/utils/browser.ts +1 -0
  4. package/backend/app.js +4 -2
  5. package/backend/config/scripts/generate-report.js +38 -30
  6. package/{.prettierignore → backend/entrypoint.sh} +7 -6
  7. package/backend/package-lock.json +453 -10
  8. package/backend/package.json +5 -2
  9. package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
  10. package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
  11. package/backend/prisma/migrations/migration_lock.toml +3 -0
  12. package/backend/prisma/schema.prisma +53 -0
  13. package/backend/routes/backup.routes.js +50 -0
  14. package/backend/routes/cron.routes.js +9 -60
  15. package/backend/routes/reports.routes.js +39 -6
  16. package/backend/routes/settings.routes.js +43 -0
  17. package/backend/server.js +52 -1
  18. package/backend/services/backupService.js +88 -0
  19. package/backend/services/cronService.js +68 -78
  20. package/backend/{routes/schedules.routes.js → services/prisma.js} +3 -13
  21. package/backend/services/reportService.js +48 -20
  22. package/backend/services/settingsService.js +36 -0
  23. package/bin/plum.js +216 -34
  24. package/docker-compose.yml +24 -0
  25. package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +190 -0
  26. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  27. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  28. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  29. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  30. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  31. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  32. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  33. package/frontend/.svelte-kit/adapter-node/chunks/Badge.js +30 -0
  34. package/frontend/.svelte-kit/adapter-node/chunks/Button.js +32 -0
  35. package/frontend/.svelte-kit/adapter-node/chunks/attributes.js +34 -0
  36. package/frontend/.svelte-kit/adapter-node/chunks/client.js +59 -0
  37. package/{backend/services/scheduleService.js → frontend/.svelte-kit/adapter-node/chunks/equality.js} +13 -16
  38. package/frontend/.svelte-kit/adapter-node/chunks/escaping.js +36 -0
  39. package/frontend/.svelte-kit/adapter-node/chunks/exports.js +247 -0
  40. package/frontend/.svelte-kit/adapter-node/chunks/index.js +1135 -0
  41. package/frontend/.svelte-kit/adapter-node/chunks/internal.js +695 -0
  42. package/frontend/.svelte-kit/adapter-node/chunks/reports.js +57 -0
  43. package/frontend/.svelte-kit/adapter-node/entries/fallbacks/error.svelte.js +44 -0
  44. package/frontend/.svelte-kit/adapter-node/entries/pages/_layout.svelte.js +181 -0
  45. package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +164 -0
  46. package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_page.svelte.js +141 -0
  47. package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_slug_/_page.svelte.js +42 -0
  48. package/frontend/.svelte-kit/adapter-node/entries/pages/scheduled-tests/_page.svelte.js +179 -0
  49. package/frontend/.svelte-kit/adapter-node/index.js +3144 -0
  50. package/frontend/.svelte-kit/adapter-node/internal.js +30 -0
  51. package/frontend/.svelte-kit/adapter-node/manifest-full.js +77 -0
  52. package/frontend/.svelte-kit/adapter-node/manifest.js +81 -0
  53. package/frontend/.svelte-kit/adapter-node/nodes/0.js +25 -0
  54. package/frontend/.svelte-kit/adapter-node/nodes/1.js +25 -0
  55. package/frontend/.svelte-kit/adapter-node/nodes/2.js +25 -0
  56. package/frontend/.svelte-kit/adapter-node/nodes/3.js +25 -0
  57. package/frontend/.svelte-kit/adapter-node/nodes/4.js +25 -0
  58. package/frontend/.svelte-kit/adapter-node/nodes/5.js +25 -0
  59. package/frontend/.svelte-kit/ambient.d.ts +240 -0
  60. package/frontend/.svelte-kit/generated/client/app.js +51 -0
  61. package/frontend/.svelte-kit/generated/client/matchers.js +18 -0
  62. package/frontend/.svelte-kit/generated/client/nodes/0.js +18 -0
  63. package/frontend/.svelte-kit/generated/client/nodes/1.js +18 -0
  64. package/frontend/.svelte-kit/generated/client/nodes/2.js +18 -0
  65. package/frontend/.svelte-kit/generated/client/nodes/3.js +18 -0
  66. package/frontend/.svelte-kit/generated/client/nodes/4.js +18 -0
  67. package/frontend/.svelte-kit/generated/client/nodes/5.js +18 -0
  68. package/frontend/.svelte-kit/generated/client-optimized/app.js +51 -0
  69. package/frontend/.svelte-kit/generated/client-optimized/matchers.js +18 -0
  70. package/frontend/.svelte-kit/generated/client-optimized/nodes/0.js +18 -0
  71. package/frontend/.svelte-kit/generated/client-optimized/nodes/1.js +18 -0
  72. package/frontend/.svelte-kit/generated/client-optimized/nodes/2.js +18 -0
  73. package/frontend/.svelte-kit/generated/client-optimized/nodes/3.js +18 -0
  74. package/frontend/.svelte-kit/generated/client-optimized/nodes/4.js +18 -0
  75. package/frontend/.svelte-kit/generated/client-optimized/nodes/5.js +18 -0
  76. package/frontend/.svelte-kit/generated/root.js +20 -0
  77. package/frontend/.svelte-kit/generated/root.svelte +83 -0
  78. package/frontend/.svelte-kit/generated/server/internal.js +65 -0
  79. package/frontend/.svelte-kit/non-ambient.d.ts +42 -0
  80. package/frontend/.svelte-kit/output/client/.vite/manifest.json +289 -0
  81. package/frontend/.svelte-kit/output/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
  82. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
  83. package/frontend/.svelte-kit/output/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
  84. package/frontend/.svelte-kit/output/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
  85. package/frontend/.svelte-kit/output/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
  86. package/frontend/.svelte-kit/output/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  87. package/frontend/.svelte-kit/output/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  88. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  89. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  90. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  91. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  92. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  93. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B0bn91RF.js +18 -0
  94. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
  95. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
  96. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
  97. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
  98. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
  99. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
  100. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
  101. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
  102. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
  103. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
  104. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DFJLQFic.js +18 -0
  105. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
  106. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
  107. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/oc0we5Us.js +18 -0
  108. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
  109. package/frontend/.svelte-kit/output/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
  110. package/frontend/.svelte-kit/output/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
  111. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
  112. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
  113. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
  114. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
  115. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
  116. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
  117. package/frontend/.svelte-kit/output/client/_app/version.json +1 -0
  118. package/frontend/.svelte-kit/output/client/favicon.png +0 -0
  119. package/frontend/.svelte-kit/output/server/.vite/manifest.json +190 -0
  120. package/frontend/.svelte-kit/output/server/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  121. package/frontend/.svelte-kit/output/server/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  122. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  123. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  124. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  125. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  126. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  127. package/frontend/.svelte-kit/output/server/chunks/Badge.js +30 -0
  128. package/frontend/.svelte-kit/output/server/chunks/Button.js +32 -0
  129. package/frontend/.svelte-kit/output/server/chunks/attributes.js +34 -0
  130. package/frontend/.svelte-kit/output/server/chunks/client.js +59 -0
  131. package/frontend/.svelte-kit/output/server/chunks/equality.js +31 -0
  132. package/frontend/.svelte-kit/output/server/chunks/escaping.js +36 -0
  133. package/frontend/.svelte-kit/output/server/chunks/exports.js +247 -0
  134. package/frontend/.svelte-kit/output/server/chunks/index.js +1135 -0
  135. package/frontend/.svelte-kit/output/server/chunks/internal.js +695 -0
  136. package/frontend/.svelte-kit/output/server/chunks/reports.js +57 -0
  137. package/frontend/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +44 -0
  138. package/frontend/.svelte-kit/output/server/entries/pages/_layout.svelte.js +181 -0
  139. package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +164 -0
  140. package/frontend/.svelte-kit/output/server/entries/pages/reports/_page.svelte.js +141 -0
  141. package/frontend/.svelte-kit/output/server/entries/pages/reports/_slug_/_page.svelte.js +42 -0
  142. package/frontend/.svelte-kit/output/server/entries/pages/scheduled-tests/_page.svelte.js +179 -0
  143. package/frontend/.svelte-kit/output/server/index.js +3144 -0
  144. package/frontend/.svelte-kit/output/server/internal.js +30 -0
  145. package/frontend/.svelte-kit/output/server/manifest-full.js +77 -0
  146. package/frontend/.svelte-kit/output/server/manifest.js +77 -0
  147. package/frontend/.svelte-kit/output/server/nodes/0.js +25 -0
  148. package/frontend/.svelte-kit/output/server/nodes/1.js +25 -0
  149. package/frontend/.svelte-kit/output/server/nodes/2.js +25 -0
  150. package/frontend/.svelte-kit/output/server/nodes/3.js +25 -0
  151. package/frontend/.svelte-kit/output/server/nodes/4.js +25 -0
  152. package/frontend/.svelte-kit/output/server/nodes/5.js +25 -0
  153. package/frontend/.svelte-kit/tsconfig.json +49 -0
  154. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
  155. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.br +0 -0
  156. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.gz +0 -0
  157. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
  158. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.br +0 -0
  159. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.gz +0 -0
  160. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
  161. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.br +0 -0
  162. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.gz +0 -0
  163. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
  164. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.br +0 -0
  165. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.gz +0 -0
  166. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
  167. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.br +0 -0
  168. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.gz +0 -0
  169. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  170. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.br +0 -0
  171. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.gz +0 -0
  172. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  173. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.br +0 -0
  174. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.gz +0 -0
  175. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  176. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.br +0 -0
  177. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.gz +0 -0
  178. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  179. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.br +0 -0
  180. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.gz +0 -0
  181. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  182. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.br +0 -0
  183. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.gz +0 -0
  184. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  185. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.br +0 -0
  186. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.gz +0 -0
  187. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  188. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.br +0 -0
  189. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.gz +0 -0
  190. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js +18 -0
  191. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.br +0 -0
  192. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.gz +0 -0
  193. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
  194. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.br +0 -0
  195. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.gz +0 -0
  196. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
  197. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.br +0 -0
  198. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.gz +0 -0
  199. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
  200. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.br +0 -0
  201. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.gz +0 -0
  202. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
  203. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.br +0 -0
  204. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.gz +0 -0
  205. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
  206. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.br +0 -0
  207. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.gz +0 -0
  208. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
  209. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.br +18 -0
  210. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.gz +0 -0
  211. package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
  212. package/{resources/comments-format.text → frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.br} +0 -0
  213. package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.gz +0 -0
  214. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
  215. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.br +0 -0
  216. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.gz +0 -0
  217. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
  218. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.br +0 -0
  219. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.gz +0 -0
  220. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
  221. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.br +0 -0
  222. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.gz +0 -0
  223. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js +18 -0
  224. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.br +0 -0
  225. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.gz +0 -0
  226. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
  227. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.br +0 -0
  228. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.gz +0 -0
  229. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
  230. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.br +0 -0
  231. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.gz +0 -0
  232. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js +18 -0
  233. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.br +0 -0
  234. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.gz +0 -0
  235. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
  236. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.br +0 -0
  237. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.gz +0 -0
  238. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
  239. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.br +0 -0
  240. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.gz +0 -0
  241. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
  242. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.br +18 -0
  243. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.gz +0 -0
  244. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
  245. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.br +0 -0
  246. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.gz +0 -0
  247. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
  248. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.br +0 -0
  249. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.gz +0 -0
  250. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
  251. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.br +0 -0
  252. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.gz +0 -0
  253. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
  254. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.br +0 -0
  255. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.gz +0 -0
  256. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
  257. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.br +0 -0
  258. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.gz +0 -0
  259. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
  260. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.br +0 -0
  261. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.gz +0 -0
  262. package/frontend/build/client/_app/version.json +1 -0
  263. package/frontend/build/client/_app/version.json.br +0 -0
  264. package/frontend/build/client/_app/version.json.gz +0 -0
  265. package/frontend/build/client/favicon.png +0 -0
  266. package/frontend/build/env.js +111 -0
  267. package/frontend/build/handler.js +1511 -0
  268. package/frontend/build/index.js +362 -0
  269. package/frontend/build/server/chunks/0-BFfqmlsS.js +26 -0
  270. package/frontend/build/server/chunks/0-BFfqmlsS.js.map +17 -0
  271. package/frontend/build/server/chunks/1-hso3H4Ax.js +26 -0
  272. package/frontend/build/server/chunks/1-hso3H4Ax.js.map +17 -0
  273. package/frontend/build/server/chunks/2-BSZWXCWp.js +26 -0
  274. package/frontend/build/server/chunks/2-BSZWXCWp.js.map +17 -0
  275. package/frontend/build/server/chunks/3-BYumY9dv.js +26 -0
  276. package/frontend/build/server/chunks/3-BYumY9dv.js.map +17 -0
  277. package/frontend/build/server/chunks/4-2UUrCVtM.js +26 -0
  278. package/frontend/build/server/chunks/4-2UUrCVtM.js.map +17 -0
  279. package/frontend/build/server/chunks/5-C2_y_fwT.js +26 -0
  280. package/frontend/build/server/chunks/5-C2_y_fwT.js.map +17 -0
  281. package/frontend/build/server/chunks/Badge-CC4U7C0c.js +30 -0
  282. package/frontend/build/server/chunks/Badge-CC4U7C0c.js.map +17 -0
  283. package/frontend/build/server/chunks/Button-BLkPicwS.js +33 -0
  284. package/frontend/build/server/chunks/Button-BLkPicwS.js.map +17 -0
  285. package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js +183 -0
  286. package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js.map +17 -0
  287. package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js +45 -0
  288. package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js.map +17 -0
  289. package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js +142 -0
  290. package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js.map +17 -0
  291. package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js +180 -0
  292. package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js.map +17 -0
  293. package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js +165 -0
  294. package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js.map +17 -0
  295. package/frontend/build/server/chunks/attributes-BeaNKpgU.js +34 -0
  296. package/frontend/build/server/chunks/attributes-BeaNKpgU.js.map +17 -0
  297. package/frontend/build/server/chunks/client-C9IaEwNo.js +52 -0
  298. package/frontend/build/server/chunks/client-C9IaEwNo.js.map +17 -0
  299. package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js +45 -0
  300. package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js.map +17 -0
  301. package/frontend/build/server/chunks/escaping-CqgfEcN3.js +36 -0
  302. package/frontend/build/server/chunks/escaping-CqgfEcN3.js.map +17 -0
  303. package/frontend/build/server/chunks/exports-OxlCJ8yE.js +192 -0
  304. package/frontend/build/server/chunks/exports-OxlCJ8yE.js.map +17 -0
  305. package/frontend/build/server/chunks/index-CjHlq1kt.js +1076 -0
  306. package/frontend/build/server/chunks/index-CjHlq1kt.js.map +17 -0
  307. package/frontend/build/server/chunks/reports-D3ZeYSmi.js +56 -0
  308. package/frontend/build/server/chunks/reports-D3ZeYSmi.js.map +17 -0
  309. package/frontend/build/server/index.js +5015 -0
  310. package/frontend/build/server/index.js.map +17 -0
  311. package/frontend/build/server/manifest.js +84 -0
  312. package/frontend/build/server/manifest.js.map +17 -0
  313. package/frontend/build/shims.js +49 -0
  314. package/frontend/package-lock.json +2 -2
  315. package/frontend/package.json +1 -1
  316. package/frontend/src/lib/api/reports.js +38 -27
  317. package/frontend/src/lib/api/schedules.js +9 -25
  318. package/frontend/src/lib/api/settings.js +48 -0
  319. package/frontend/src/lib/components/layout/Nav.svelte +2 -1
  320. package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
  321. package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
  322. package/frontend/src/lib/stores/runner.js +9 -0
  323. package/frontend/src/routes/+page.svelte +10 -3
  324. package/frontend/src/routes/reports/+page.svelte +342 -51
  325. package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
  326. package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
  327. package/frontend/src/routes/settings/+page.svelte +410 -0
  328. package/package.json +6 -2
  329. package/.claude/settings.local.json +0 -27
  330. package/.husky/pre-commit +0 -2
  331. package/.prettierrc +0 -15
  332. package/.vscode/settings.json +0 -10
  333. package/backend/config/scripts/create-settings.js +0 -53
  334. package/license-config.json +0 -37
@@ -0,0 +1,84 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ const manifest = (() => {
19
+ function __memo(fn) {
20
+ let value;
21
+ return () => value ??= (value = fn());
22
+ }
23
+
24
+ return {
25
+ appDir: "_app",
26
+ appPath: "_app",
27
+ assets: new Set(["favicon.png"]),
28
+ mimeTypes: {".png":"image/png"},
29
+ _: {
30
+ client: {start:"_app/immutable/entry/start.GiBry-lw.js",app:"_app/immutable/entry/app.C2MbX06K.js",imports:["_app/immutable/entry/start.GiBry-lw.js","_app/immutable/chunks/Cit9MuSg.js","_app/immutable/chunks/B0bn91RF.js","_app/immutable/entry/app.C2MbX06K.js","_app/immutable/chunks/B0bn91RF.js","_app/immutable/chunks/DFJLQFic.js","_app/immutable/chunks/CwBTqcbj.js","_app/immutable/chunks/Be4iUAB0.js","_app/immutable/chunks/CHV2KnPr.js"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},
31
+ nodes: [
32
+ __memo(() => import('./chunks/0-BFfqmlsS.js')),
33
+ __memo(() => import('./chunks/1-hso3H4Ax.js')),
34
+ __memo(() => import('./chunks/2-BSZWXCWp.js')),
35
+ __memo(() => import('./chunks/3-BYumY9dv.js')),
36
+ __memo(() => import('./chunks/4-2UUrCVtM.js')),
37
+ __memo(() => import('./chunks/5-C2_y_fwT.js'))
38
+ ],
39
+ routes: [
40
+ {
41
+ id: "/",
42
+ pattern: /^\/$/,
43
+ params: [],
44
+ page: { layouts: [0,], errors: [1,], leaf: 2 },
45
+ endpoint: null
46
+ },
47
+ {
48
+ id: "/reports",
49
+ pattern: /^\/reports\/?$/,
50
+ params: [],
51
+ page: { layouts: [0,], errors: [1,], leaf: 3 },
52
+ endpoint: null
53
+ },
54
+ {
55
+ id: "/reports/[slug]",
56
+ pattern: /^\/reports\/([^/]+?)\/?$/,
57
+ params: [{"name":"slug","optional":false,"rest":false,"chained":false}],
58
+ page: { layouts: [0,], errors: [1,], leaf: 4 },
59
+ endpoint: null
60
+ },
61
+ {
62
+ id: "/scheduled-tests",
63
+ pattern: /^\/scheduled-tests\/?$/,
64
+ params: [],
65
+ page: { layouts: [0,], errors: [1,], leaf: 5 },
66
+ endpoint: null
67
+ }
68
+ ],
69
+ prerendered_routes: new Set([]),
70
+ matchers: async () => {
71
+
72
+ return { };
73
+ },
74
+ server_assets: {}
75
+ }
76
+ }
77
+ })();
78
+
79
+ const prerendered = new Set([]);
80
+
81
+ const base = "";
82
+
83
+ export { base, manifest, prerendered };
84
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,17 @@
1
+ /*
2
+ This file is part of Plum.
3
+
4
+ Plum is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ Plum is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+ {"version":3,"file":"manifest.js","sources":["../../.svelte-kit/adapter-node/manifest.js"],"sourcesContent":["export const manifest = (() => {\nfunction __memo(fn) {\n\tlet value;\n\treturn () => value ??= (value = fn());\n}\n\nreturn {\n\tappDir: \"_app\",\n\tappPath: \"_app\",\n\tassets: new Set([\"favicon.png\"]),\n\tmimeTypes: {\".png\":\"image/png\"},\n\t_: {\n\t\tclient: {start:\"_app/immutable/entry/start.GiBry-lw.js\",app:\"_app/immutable/entry/app.C2MbX06K.js\",imports:[\"_app/immutable/entry/start.GiBry-lw.js\",\"_app/immutable/chunks/Cit9MuSg.js\",\"_app/immutable/chunks/B0bn91RF.js\",\"_app/immutable/entry/app.C2MbX06K.js\",\"_app/immutable/chunks/B0bn91RF.js\",\"_app/immutable/chunks/DFJLQFic.js\",\"_app/immutable/chunks/CwBTqcbj.js\",\"_app/immutable/chunks/Be4iUAB0.js\",\"_app/immutable/chunks/CHV2KnPr.js\"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},\n\t\tnodes: [\n\t\t\t__memo(() => import('./nodes/0.js')),\n\t\t\t__memo(() => import('./nodes/1.js')),\n\t\t\t__memo(() => import('./nodes/2.js')),\n\t\t\t__memo(() => import('./nodes/3.js')),\n\t\t\t__memo(() => import('./nodes/4.js')),\n\t\t\t__memo(() => import('./nodes/5.js'))\n\t\t],\n\t\troutes: [\n\t\t\t{\n\t\t\t\tid: \"/\",\n\t\t\t\tpattern: /^\\/$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 2 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/reports\",\n\t\t\t\tpattern: /^\\/reports\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 3 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/reports/[slug]\",\n\t\t\t\tpattern: /^\\/reports\\/([^/]+?)\\/?$/,\n\t\t\t\tparams: [{\"name\":\"slug\",\"optional\":false,\"rest\":false,\"chained\":false}],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 4 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/scheduled-tests\",\n\t\t\t\tpattern: /^\\/scheduled-tests\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 5 },\n\t\t\t\tendpoint: null\n\t\t\t}\n\t\t],\n\t\tprerendered_routes: new Set([]),\n\t\tmatchers: async () => {\n\t\t\t\n\t\t\treturn { };\n\t\t},\n\t\tserver_assets: {}\n\t}\n}\n})();\n\nexport const prerendered = new Set([]);\n\nexport const base = \"\";"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ import buffer from 'node:buffer';
19
+ import { webcrypto } from 'node:crypto';
20
+
21
+ // `buffer.File` was added in Node 18.13.0 while the `File` global was added in Node 20.0.0
22
+ const File = /** @type {import('node:buffer') & { File?: File}} */ (buffer).File;
23
+
24
+ /** @type {Record<string, any>} */
25
+ const globals = {
26
+ crypto: webcrypto,
27
+ File
28
+ };
29
+
30
+ // exported for dev/preview and node environments
31
+ /**
32
+ * Make various web APIs available as globals:
33
+ * - `crypto`
34
+ * - `File`
35
+ */
36
+ function installPolyfills() {
37
+ for (const name in globals) {
38
+ if (name in globalThis) continue;
39
+
40
+ Object.defineProperty(globalThis, name, {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: globals[name]
45
+ });
46
+ }
47
+ }
48
+
49
+ installPolyfills();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "plum-frontend",
3
- "version": "0.0.1",
3
+ "version": "1.2.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "plum-frontend",
9
- "version": "0.0.1",
9
+ "version": "1.2.1",
10
10
  "dependencies": {
11
11
  "socket.io-client": "^4.8.1"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plum-frontend",
3
3
  "private": true,
4
- "version": "0.0.1",
4
+ "version": "1.2.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
@@ -15,29 +15,12 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- /*
19
- This file is part of Plum.
20
-
21
- Plum is free software: you can redistribute it and/or modify
22
- it under the terms of the GNU General Public License as published by
23
- the Free Software Foundation, either version 3 of the License, or
24
- (at your option) any later version.
25
-
26
- Plum is distributed in the hope that it will be useful,
27
- but WITHOUT ANY WARRANTY; without even the implied warranty of
28
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
- GNU General Public License for more details.
30
-
31
- You should have received a copy of the GNU General Public License
32
- along with Plum. If not, see https://www.gnu.org/licenses/.
33
- */
34
-
35
18
  const BASE = 'http://localhost:3001';
36
19
 
37
20
  export async function fetchReports() {
38
21
  const res = await fetch(`${BASE}/reports`);
39
22
  const { reports } = await res.json();
40
- return reports.map(parseReport).filter(Boolean);
23
+ return reports.map((r) => ({ ...r, date: new Date(r.createdAt).toLocaleString() }));
41
24
  }
42
25
 
43
26
  export async function fetchLatestReport() {
@@ -50,19 +33,47 @@ export function reportUrl(fileName) {
50
33
  return `/reports/${encodeURIComponent(fileName)}`;
51
34
  }
52
35
 
36
+ // Parses metadata out of a report filename.
37
+ // Format: {STATUS}_cucumber_report_{trigger}_{tags}_runners_{n}_{timestamp}.json
38
+ export function parseReport(fileName) {
39
+ const m = fileName.match(
40
+ /^(PASS|FAIL)_cucumber_report_(.+?)_(\([^)]+\))_runners_(\d+)_(\d{4}_\d{2}_\d{2}T[\d_]+Z)\.json$/
41
+ );
42
+ if (!m) return null;
43
+ const [, status, triggerType, tags, runners, tsRaw] = m;
44
+ const isoStr = tsRaw.replace(
45
+ /^(\d{4})_(\d{2})_(\d{2})T(\d{2})_(\d{2})_(\d{2})_(\d+)Z$/,
46
+ '$1-$2-$3T$4:$5:$6.$7Z'
47
+ );
48
+ return {
49
+ status,
50
+ triggerType,
51
+ tags,
52
+ runners: parseInt(runners, 10),
53
+ date: new Date(isoStr).toLocaleString()
54
+ };
55
+ }
56
+
53
57
  export async function fetchReportDetail(fileName) {
54
58
  const res = await fetch(`${BASE}/reports/${encodeURIComponent(fileName)}/detail`);
55
59
  if (!res.ok) throw new Error('Report not found');
56
60
  return res.json();
57
61
  }
58
62
 
59
- export function parseReport(fileName) {
60
- const match = fileName.match(
61
- /(PASS|FAIL)_cucumber_report_(.+?)_\(([^)]+)\)(?:_runners_(\d+))?_(\d{4})_(\d{2})_(\d{2})T(\d{2})_(\d{2})_(\d{2})_\d{3}Z\.json/
62
- );
63
- if (!match) return null;
64
- const [, status, triggerType, tags, runners = '1', year, month, day, hour, minute, second] =
65
- match;
66
- const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
67
- return { fileName, status, triggerType, tags, runners: Number(runners), date };
63
+ export async function deleteReport(fileName) {
64
+ const res = await fetch(`${BASE}/reports/${encodeURIComponent(fileName)}`, {
65
+ method: 'DELETE'
66
+ });
67
+ if (!res.ok) throw new Error('Failed to delete report');
68
+ return res.json();
69
+ }
70
+
71
+ export async function deleteReports(fileNames) {
72
+ const res = await fetch(`${BASE}/reports/bulk`, {
73
+ method: 'DELETE',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ fileNames })
76
+ });
77
+ if (!res.ok) throw new Error('Failed to delete reports');
78
+ return res.json();
68
79
  }
@@ -15,45 +15,29 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- /*
19
- This file is part of Plum.
20
-
21
- Plum is free software: you can redistribute it and/or modify
22
- it under the terms of the GNU General Public License as published by
23
- the Free Software Foundation, either version 3 of the License, or
24
- (at your option) any later version.
25
-
26
- Plum is distributed in the hope that it will be useful,
27
- but WITHOUT ANY WARRANTY; without even the implied warranty of
28
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
- GNU General Public License for more details.
30
-
31
- You should have received a copy of the GNU General Public License
32
- along with Plum. If not, see https://www.gnu.org/licenses/.
33
- */
34
-
35
18
  const BASE = 'http://localhost:3001';
36
19
 
37
- export async function fetchSchedules() {
38
- const res = await fetch(`${BASE}/schedules`);
39
- const { schedules } = await res.json();
40
- return schedules ?? [];
41
- }
42
-
43
20
  export async function fetchCronJobs() {
44
21
  const res = await fetch(`${BASE}/cron-jobs`);
45
22
  const { cronJobs } = await res.json();
46
23
  return cronJobs ?? [];
47
24
  }
48
25
 
49
- export async function saveCronJob({ taskName, cronExpression, tags, isEditing, editTaskName }) {
26
+ export async function saveCronJob({
27
+ taskName,
28
+ cronExpression,
29
+ tags,
30
+ workers,
31
+ isEditing,
32
+ editTaskName
33
+ }) {
50
34
  const formattedTags = tags.replace(/\sOR\s/gi, (m) => m.toLowerCase());
51
35
  const url = isEditing ? `${BASE}/cron-jobs/${editTaskName}` : `${BASE}/cron-jobs`;
52
36
  const method = isEditing ? 'PUT' : 'POST';
53
37
  const res = await fetch(url, {
54
38
  method,
55
39
  headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
40
+ body: JSON.stringify({ cronExpression, taskName, tags: formattedTags, workers })
57
41
  });
58
42
  return res.json();
59
43
  }
@@ -0,0 +1,48 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ const BASE = 'http://localhost:3001';
19
+
20
+ export async function fetchProject() {
21
+ const res = await fetch(`${BASE}/settings/project`);
22
+ if (!res.ok) return { name: '', logoUrl: '' };
23
+ return res.json();
24
+ }
25
+
26
+ export async function saveProject({ name, logoUrl }) {
27
+ const res = await fetch(`${BASE}/settings/project`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ name, logoUrl })
31
+ });
32
+ return res.json();
33
+ }
34
+
35
+ export async function exportBackup() {
36
+ const res = await fetch(`${BASE}/backup/export`);
37
+ if (!res.ok) throw new Error('Export failed');
38
+ return res.json();
39
+ }
40
+
41
+ export async function importBackup(data) {
42
+ const res = await fetch(`${BASE}/backup/import`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(data)
46
+ });
47
+ return res.json();
48
+ }
@@ -25,7 +25,8 @@
25
25
  const links = [
26
26
  { href: '/', label: 'Run Tests' },
27
27
  { href: '/reports', label: 'Reports' },
28
- { href: '/scheduled-tests', label: 'Scheduled' }
28
+ { href: '/scheduled-tests', label: 'Scheduled' },
29
+ { href: '/settings', label: 'Settings' }
29
30
  ];
30
31
 
31
32
  function toggleTheme() {
@@ -19,7 +19,16 @@
19
19
  import { onMount } from 'svelte';
20
20
  import { slide, fly } from 'svelte/transition';
21
21
  import { io } from 'socket.io-client';
22
- import { socket, runnerState, runnerConfig, panelExpanded, triggerRun } from '$lib/stores/runner';
22
+ import {
23
+ socket,
24
+ runnerState,
25
+ runnerConfig,
26
+ panelExpanded,
27
+ triggerRun,
28
+ testsVersion,
29
+ reportsVersion,
30
+ activeCronJobs
31
+ } from '$lib/stores/runner';
23
32
  import { fetchLatestReport, reportUrl } from '$lib/api/reports';
24
33
  import Terminal from '$lib/components/ui/Terminal.svelte';
25
34
  import Button from '$lib/components/ui/Button.svelte';
@@ -47,30 +56,56 @@
47
56
  }));
48
57
  });
49
58
 
59
+ s.on('tests-changed', () => {
60
+ testsVersion.update((v) => v + 1);
61
+ });
62
+
63
+ s.on('report-ready', () => {
64
+ reportsVersion.update((v) => v + 1);
65
+ });
66
+
67
+ s.on('cron-start', ({ taskName }) => {
68
+ activeCronJobs.update((j) => ({ ...j, [taskName]: true }));
69
+ });
70
+
71
+ s.on('cron-done', ({ taskName }) => {
72
+ activeCronJobs.update((j) => {
73
+ const next = { ...j };
74
+ delete next[taskName];
75
+ return next;
76
+ });
77
+ reportsVersion.update((v) => v + 1);
78
+ });
79
+
50
80
  return () => s.disconnect();
51
81
  });
52
82
 
53
83
  $: state = $runnerState;
54
84
  $: cfg = $runnerConfig;
55
- $: workerCount = Math.min(cfg.workers, 4);
56
- $: dots = Array.from({ length: workerCount });
85
+ $: dots = Array.from({ length: cfg.workers });
86
+ $: cronJobs = Object.keys($activeCronJobs);
87
+ $: anyCronRunning = cronJobs.length > 0;
88
+ $: anyRunning = state.running || anyCronRunning;
57
89
 
58
90
  $: statusColor =
59
91
  state.status === 'pass'
60
92
  ? 'var(--pass)'
61
93
  : state.status === 'fail'
62
94
  ? 'var(--fail)'
63
- : state.status === 'running'
95
+ : state.running
64
96
  ? 'var(--accent)'
65
- : 'var(--border)';
66
-
67
- $: statusLabel =
68
- state.status === 'running'
69
- ? 'running'
70
- : state.status === 'pass'
71
- ? 'passed'
72
- : state.status === 'fail'
73
- ? 'failed'
97
+ : anyCronRunning
98
+ ? 'var(--pass)'
99
+ : 'var(--border)';
100
+
101
+ $: statusLabel = state.running
102
+ ? 'running'
103
+ : state.status === 'pass'
104
+ ? 'passed'
105
+ : state.status === 'fail'
106
+ ? 'failed'
107
+ : anyCronRunning
108
+ ? 'scheduled'
74
109
  : 'ready';
75
110
 
76
111
  function handleKeydown(e) {
@@ -142,7 +177,7 @@
142
177
  class:active={cfg.workers === n}
143
178
  on:click={() => runnerConfig.update((c) => ({ ...c, workers: n }))}
144
179
  >
145
- {n === 1 ? 'Off' : n}
180
+ {n}
146
181
  </button>
147
182
  {/each}
148
183
  </div>
@@ -152,15 +187,38 @@
152
187
  </Button>
153
188
  </div>
154
189
 
155
- <Terminal output={state.output} />
190
+ <div class="main-row">
191
+ <!-- Terminal column -->
192
+ <div class="terminal-col">
193
+ <Terminal output={state.output} />
194
+ {#if state.testCompleted && state.latestReport}
195
+ <div class="report-row" transition:fly={{ y: 6, duration: 200 }}>
196
+ <a href={reportUrl(state.latestReport)} target="_blank" rel="noopener noreferrer">
197
+ <Button variant="outline" size="sm">View Report →</Button>
198
+ </a>
199
+ </div>
200
+ {/if}
201
+ </div>
156
202
 
157
- {#if state.testCompleted && state.latestReport}
158
- <div class="report-row" transition:fly={{ y: 6, duration: 200 }}>
159
- <a href={reportUrl(state.latestReport)} target="_blank" rel="noopener noreferrer">
160
- <Button variant="outline" size="sm">View Report →</Button>
161
- </a>
203
+ <!-- Sidebar: scheduled jobs -->
204
+ <div class="sidebar">
205
+ <div class="sidebar-section">
206
+ <span class="sidebar-label">Scheduled</span>
207
+ {#if cronJobs.length === 0}
208
+ <span class="sidebar-empty">none running</span>
209
+ {:else}
210
+ <ul class="cron-list">
211
+ {#each cronJobs as name}
212
+ <li class="cron-item" transition:fly={{ x: -6, duration: 180 }}>
213
+ <span class="cron-dot"></span>
214
+ <span class="cron-name">{name}</span>
215
+ </li>
216
+ {/each}
217
+ </ul>
218
+ {/if}
219
+ </div>
162
220
  </div>
163
- {/if}
221
+ </div>
164
222
  </div>
165
223
  {/if}
166
224
  </div>
@@ -372,7 +430,88 @@
372
430
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
373
431
  }
374
432
 
433
+ /* ── Main row: terminal + sidebar ── */
434
+ .main-row {
435
+ display: flex;
436
+ gap: 1rem;
437
+ align-items: flex-start;
438
+ }
439
+
440
+ .terminal-col {
441
+ flex: 1;
442
+ min-width: 0;
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 0.5rem;
446
+ /* Constrain terminal height via the Terminal component's wrapper */
447
+ --terminal-max-height: 140px;
448
+ }
449
+
375
450
  .report-row {
376
451
  display: flex;
377
452
  }
453
+
454
+ /* ── Sidebar ── */
455
+ .sidebar {
456
+ width: 260px;
457
+ flex-shrink: 0;
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 1rem;
461
+ padding-left: 1.25rem;
462
+ border-left: 1px solid var(--border);
463
+ }
464
+
465
+ .sidebar-section {
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: 0.5rem;
469
+ }
470
+
471
+ .sidebar-label {
472
+ font-size: 0.65rem;
473
+ font-weight: 600;
474
+ letter-spacing: 0.09em;
475
+ text-transform: uppercase;
476
+ color: var(--text-muted);
477
+ }
478
+
479
+ .sidebar-empty {
480
+ font-size: 0.75rem;
481
+ color: var(--text-muted);
482
+ font-style: italic;
483
+ }
484
+
485
+ .cron-list {
486
+ list-style: none;
487
+ padding: 0;
488
+ margin: 0;
489
+ display: flex;
490
+ flex-direction: column;
491
+ gap: 0.375rem;
492
+ }
493
+
494
+ .cron-item {
495
+ display: flex;
496
+ align-items: center;
497
+ gap: 0.375rem;
498
+ }
499
+
500
+ .cron-dot {
501
+ width: 7px;
502
+ height: 7px;
503
+ border-radius: 50%;
504
+ background: var(--pass);
505
+ flex-shrink: 0;
506
+ animation: statusPulse 1.6s ease-in-out infinite;
507
+ }
508
+
509
+ .cron-name {
510
+ font-size: 0.75rem;
511
+ font-family: 'JetBrains Mono', monospace;
512
+ color: var(--text);
513
+ white-space: nowrap;
514
+ overflow: hidden;
515
+ text-overflow: ellipsis;
516
+ }
378
517
  </style>
@@ -77,8 +77,8 @@
77
77
  background: var(--terminal-bg);
78
78
  color: var(--terminal-text);
79
79
  padding: 1rem;
80
- min-height: 220px;
81
- max-height: 280px;
80
+ min-height: 120px;
81
+ max-height: 160px;
82
82
  overflow-y: auto;
83
83
  white-space: pre-wrap;
84
84
  word-break: break-word;
@@ -35,6 +35,15 @@ export const runnerConfig = writable({
35
35
 
36
36
  export const panelExpanded = writable(true);
37
37
 
38
+ // Increments whenever the backend detects a change in tests/features/
39
+ export const testsVersion = writable(0);
40
+
41
+ // Increments whenever a new report file is detected
42
+ export const reportsVersion = writable(0);
43
+
44
+ // Map of taskName → true for every cron job currently executing
45
+ export const activeCronJobs = writable({});
46
+
38
47
  export function triggerRun(id) {
39
48
  const s = get(socket);
40
49
  if (!s) return;