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
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "plum-backend",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
- "init": "node services/envService.js && node config/scripts/create-settings.js",
6
+ "init": "node services/envService.js",
7
7
  "create-step": "node config/scripts/create-step.mjs",
8
8
  "create-env": "node services/envService.js",
9
9
  "test": "node config/scripts/run-tests.js"
@@ -15,14 +15,17 @@
15
15
  "@playwright/test": "^1.50.1",
16
16
  "@types/node": "^22.17.0",
17
17
  "cross-env": "^7.0.3",
18
+ "prisma": "^6.19.3",
18
19
  "ts-node": "^10.9.2",
19
20
  "typescript": "^5.9.2"
20
21
  },
21
22
  "dependencies": {
22
23
  "@clack/prompts": "^1.5.1",
23
24
  "@cucumber/cucumber": "^11.2.0",
25
+ "@prisma/client": "^6.19.3",
24
26
  "chai": "^4.3.6",
25
27
  "chai-soft-assert": "^0.0.5",
28
+ "chokidar": "^5.0.0",
26
29
  "cors": "^2.8.5",
27
30
  "dotenv": "^16.4.7",
28
31
  "express": "^4.21.2",
@@ -0,0 +1,35 @@
1
+ -- CreateTable
2
+ CREATE TABLE "CronJob" (
3
+ "id" SERIAL NOT NULL,
4
+ "taskName" TEXT NOT NULL,
5
+ "cronExpression" TEXT NOT NULL,
6
+ "tags" TEXT NOT NULL,
7
+ "workers" INTEGER NOT NULL DEFAULT 1,
8
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+ "updatedAt" TIMESTAMP(3) NOT NULL,
10
+
11
+ CONSTRAINT "CronJob_pkey" PRIMARY KEY ("id")
12
+ );
13
+
14
+ -- CreateTable
15
+ CREATE TABLE "Report" (
16
+ "id" SERIAL NOT NULL,
17
+ "fileName" TEXT NOT NULL,
18
+ "status" TEXT NOT NULL,
19
+ "tags" TEXT NOT NULL,
20
+ "triggerType" TEXT NOT NULL DEFAULT 'manual-trigger',
21
+ "runners" INTEGER NOT NULL DEFAULT 1,
22
+ "cronJobId" INTEGER,
23
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+
25
+ CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
26
+ );
27
+
28
+ -- CreateIndex
29
+ CREATE UNIQUE INDEX "CronJob_taskName_key" ON "CronJob"("taskName");
30
+
31
+ -- CreateIndex
32
+ CREATE UNIQUE INDEX "Report_fileName_key" ON "Report"("fileName");
33
+
34
+ -- AddForeignKey
35
+ ALTER TABLE "Report" ADD CONSTRAINT "Report_cronJobId_fkey" FOREIGN KEY ("cronJobId") REFERENCES "CronJob"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,8 @@
1
+ -- CreateTable
2
+ CREATE TABLE "Project" (
3
+ "id" SERIAL NOT NULL,
4
+ "name" TEXT NOT NULL DEFAULT '',
5
+ "logoUrl" TEXT NOT NULL DEFAULT '',
6
+
7
+ CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
8
+ );
@@ -0,0 +1,3 @@
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (i.e. Git)
3
+ provider = "postgresql"
@@ -0,0 +1,53 @@
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
+ generator client {
18
+ provider = "prisma-client-js"
19
+ }
20
+
21
+ datasource db {
22
+ provider = "postgresql"
23
+ url = env("DATABASE_URL")
24
+ }
25
+
26
+ model CronJob {
27
+ id Int @id @default(autoincrement())
28
+ taskName String @unique
29
+ cronExpression String
30
+ tags String
31
+ workers Int @default(1)
32
+ createdAt DateTime @default(now())
33
+ updatedAt DateTime @updatedAt
34
+ reports Report[]
35
+ }
36
+
37
+ model Report {
38
+ id Int @id @default(autoincrement())
39
+ fileName String @unique
40
+ status String
41
+ tags String
42
+ triggerType String @default("manual-trigger")
43
+ runners Int @default(1)
44
+ cronJobId Int?
45
+ cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
46
+ createdAt DateTime @default(now())
47
+ }
48
+
49
+ model Project {
50
+ id Int @id @default(autoincrement())
51
+ name String @default("")
52
+ logoUrl String @default("")
53
+ }
@@ -0,0 +1,50 @@
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 express = require('express');
19
+ const router = express.Router();
20
+ const backupService = require('../services/backupService');
21
+ const cronService = require('../services/cronService');
22
+
23
+ router.get('/export', async (req, res) => {
24
+ try {
25
+ const data = await backupService.exportAll();
26
+ const fileName = `plum-backup-${new Date().toISOString().slice(0, 10)}.json`;
27
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
28
+ res.setHeader('Content-Type', 'application/json');
29
+ res.json(data);
30
+ } catch (error) {
31
+ console.error('Export failed:', error);
32
+ res.status(500).json({ error: 'Export failed' });
33
+ }
34
+ });
35
+
36
+ router.post('/import', async (req, res) => {
37
+ try {
38
+ const { cronJobs, reports, project } = req.body;
39
+ if (!Array.isArray(cronJobs) && !Array.isArray(reports) && !project) {
40
+ return res.status(400).json({ error: 'Invalid backup format' });
41
+ }
42
+ await backupService.importAll({ cronJobs, reports, project }, cronService);
43
+ res.json({ message: 'Import successful' });
44
+ } catch (error) {
45
+ console.error('Import failed:', error);
46
+ res.status(500).json({ error: 'Import failed' });
47
+ }
48
+ });
49
+
50
+ module.exports = router;
@@ -19,14 +19,9 @@ const express = require('express');
19
19
  const router = express.Router();
20
20
  const cronService = require('../services/cronService');
21
21
 
22
- /* -----------------------------------------------------
23
- * Get Cron Jobs
24
- * Description:
25
- * - Get all cron jobs from config/cron-jobs.json
26
- * ------------------------------------------------------ */
27
- router.get('/', (req, res) => {
22
+ router.get('/', async (req, res) => {
28
23
  try {
29
- const cronJobs = cronService.getAllCronJobs();
24
+ const cronJobs = await cronService.getAllCronJobs();
30
25
  res.json({ cronJobs });
31
26
  } catch (error) {
32
27
  console.error('Error fetching cron jobs:', error);
@@ -34,28 +29,13 @@ router.get('/', (req, res) => {
34
29
  }
35
30
  });
36
31
 
37
- /* -----------------------------------------------------
38
- * Create Cron Job
39
- * Description:
40
- * Add a new cron job to config/cron-jobs.json
41
- * Params:
42
- * - cronExpression:
43
- * e.g. "* * * * *"
44
- * https://www.baeldung.com/cron-expressions
45
- * - taskName:
46
- * the unique identifier
47
- * - tags:
48
- * cucumber tag you want to run when cron job
49
- * is triggered.
50
- * ------------------------------------------------------ */
51
- router.post('/', (req, res) => {
32
+ router.post('/', async (req, res) => {
52
33
  try {
53
34
  const { cronExpression, taskName, tags } = req.body;
54
35
  if (!cronExpression || !taskName || !tags) {
55
36
  return res.status(400).json({ error: 'Missing required fields' });
56
37
  }
57
-
58
- cronService.addCronJob(req.body);
38
+ await cronService.addCronJob(req.body);
59
39
  res.json({
60
40
  message: `Cron job ${taskName} added with tags: ${tags}`,
61
41
  taskName,
@@ -67,56 +47,25 @@ router.post('/', (req, res) => {
67
47
  }
68
48
  });
69
49
 
70
- /* -----------------------------------------------------
71
- * Edit Cron Job
72
- * Description:
73
- * Edit an existing cron job from
74
- * config/cron-jobs.json
75
- * Params:
76
- * - taskName:
77
- * the unique identifier
78
- * - cronExpression:
79
- * e.g. "* * * * *"
80
- * https://www.baeldung.com/cron-expressions
81
- * - tags:
82
- * cucumber tag you want to run when cron job
83
- * is triggered.
84
- * ------------------------------------------------------ */
85
- router.put('/:taskName', (req, res) => {
50
+ router.put('/:taskName', async (req, res) => {
86
51
  try {
87
52
  const { taskName } = req.params;
88
53
  const { cronExpression, tags } = req.body;
89
-
90
54
  if (!cronExpression || !tags) {
91
55
  return res.status(400).json({ error: 'Missing required fields' });
92
56
  }
93
-
94
- const updatedCronJob = cronService.updateCronJob(taskName, req.body); // Assuming this is a function to update the cron job
95
- res.json({
96
- message: `Cron job ${taskName} updated`,
97
- taskName,
98
- cronExpression,
99
- tags: updatedCronJob.tags
100
- });
57
+ await cronService.updateCronJob(taskName, req.body);
58
+ res.json({ message: `Cron job ${taskName} updated`, taskName, cronExpression, tags });
101
59
  } catch (error) {
102
60
  console.error('Error updating cron job:', error);
103
61
  res.status(500).json({ error: 'Failed to update cron job' });
104
62
  }
105
63
  });
106
64
 
107
- /* -----------------------------------------------------
108
- * Delete Cron Job
109
- * Description:
110
- * Delete cron job from config/cron-jobs.json
111
- * by taskName
112
- * Params:
113
- * - taskName:
114
- * the unique identifier
115
- * ------------------------------------------------------ */
116
- router.delete('/:taskName', (req, res) => {
65
+ router.delete('/:taskName', async (req, res) => {
117
66
  try {
118
67
  const { taskName } = req.params;
119
- cronService.removeCronJob(taskName);
68
+ await cronService.removeCronJob(taskName);
120
69
  res.json({ message: `Cron job ${taskName} deleted` });
121
70
  } catch (error) {
122
71
  console.error('Error deleting cron job:', error);
@@ -19,14 +19,24 @@ const express = require('express');
19
19
  const router = express.Router();
20
20
  const reportService = require('../services/reportService');
21
21
 
22
- router.get('/', (req, res) => {
23
- const reports = reportService.getAllReports();
24
- res.json({ reports });
22
+ router.get('/', async (req, res) => {
23
+ try {
24
+ const reports = await reportService.getAllReports();
25
+ res.json({ reports });
26
+ } catch (error) {
27
+ console.error('Error fetching reports:', error);
28
+ res.status(500).json({ error: 'Failed to fetch reports' });
29
+ }
25
30
  });
26
31
 
27
- router.get('/latest', (req, res) => {
28
- const latestReport = reportService.getLatestReport();
29
- res.json({ latestReport });
32
+ router.get('/latest', async (req, res) => {
33
+ try {
34
+ const latestReport = await reportService.getLatestReport();
35
+ res.json({ latestReport });
36
+ } catch (error) {
37
+ console.error('Error fetching latest report:', error);
38
+ res.status(500).json({ error: 'Failed to fetch latest report' });
39
+ }
30
40
  });
31
41
 
32
42
  router.get('/:fileName/detail', (req, res) => {
@@ -35,4 +45,27 @@ router.get('/:fileName/detail', (req, res) => {
35
45
  res.json(detail);
36
46
  });
37
47
 
48
+ router.delete('/bulk', async (req, res) => {
49
+ const { fileNames } = req.body;
50
+ if (!Array.isArray(fileNames) || fileNames.length === 0)
51
+ return res.status(400).json({ error: 'fileNames array required' });
52
+ try {
53
+ await reportService.deleteReports(fileNames);
54
+ res.json({ deleted: fileNames.length });
55
+ } catch (error) {
56
+ console.error('Error deleting reports:', error);
57
+ res.status(500).json({ error: 'Failed to delete reports' });
58
+ }
59
+ });
60
+
61
+ router.delete('/:fileName', async (req, res) => {
62
+ try {
63
+ await reportService.deleteReport(req.params.fileName);
64
+ res.json({ deleted: req.params.fileName });
65
+ } catch (error) {
66
+ console.error('Error deleting report:', error);
67
+ res.status(500).json({ error: 'Failed to delete report' });
68
+ }
69
+ });
70
+
38
71
  module.exports = router;
@@ -0,0 +1,43 @@
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 express = require('express');
19
+ const router = express.Router();
20
+ const settingsService = require('../services/settingsService');
21
+
22
+ router.get('/project', async (req, res) => {
23
+ try {
24
+ const project = await settingsService.getProject();
25
+ res.json(project);
26
+ } catch (error) {
27
+ console.error('Error fetching project settings:', error);
28
+ res.status(500).json({ error: 'Failed to fetch project settings' });
29
+ }
30
+ });
31
+
32
+ router.post('/project', async (req, res) => {
33
+ try {
34
+ const { name, logoUrl } = req.body;
35
+ const project = await settingsService.updateProject({ name, logoUrl });
36
+ res.json(project);
37
+ } catch (error) {
38
+ console.error('Error updating project settings:', error);
39
+ res.status(500).json({ error: 'Failed to update project settings' });
40
+ }
41
+ });
42
+
43
+ module.exports = router;
package/backend/server.js CHANGED
@@ -19,6 +19,7 @@ const http = require('http');
19
19
  const { Server } = require('socket.io');
20
20
  const app = require('./app');
21
21
  const socketHandler = require('./websockets/socketHandler.js');
22
+ const cronService = require('./services/cronService');
22
23
  const server = http.createServer(app);
23
24
  const io = new Server(server, { cors: { origin: '*' } });
24
25
  const path = require('path');
@@ -35,5 +36,55 @@ if (!fs.existsSync(testsDir)) {
35
36
  console.log('📂 Loading tests from:', testsDir);
36
37
 
37
38
  socketHandler(io);
39
+ cronService.setSocketIO(io);
38
40
 
39
- server.listen(3001, () => console.log('Backend running on port 3001'));
41
+ async function start() {
42
+ await cronService.init();
43
+ server.listen(3001, async () => {
44
+ console.log('Backend running on port 3001');
45
+
46
+ // chokidar v5+ is ESM-only — use dynamic import to stay compatible with CJS
47
+ let chokidar;
48
+ try {
49
+ chokidar = (await import('chokidar')).default;
50
+ } catch {
51
+ console.warn('⚠️ chokidar unavailable — file watching disabled');
52
+ return;
53
+ }
54
+
55
+ const watchOpts = { usePolling: true, interval: 800, ignoreInitial: true };
56
+
57
+ // Watch tests/features/ — notify UI when feature files are added/changed/removed
58
+ const featuresDir = path.join(testsDir, 'features');
59
+ if (fs.existsSync(featuresDir)) {
60
+ let debounce = null;
61
+ chokidar.watch(featuresDir, watchOpts).on('all', (event, filePath) => {
62
+ clearTimeout(debounce);
63
+ debounce = setTimeout(() => {
64
+ console.log(
65
+ `📝 Tests changed (${event}: ${path.basename(filePath)}) — notifying clients`
66
+ );
67
+ io.emit('tests-changed');
68
+ }, 300);
69
+ });
70
+ console.log('👀 Watching for test file changes...');
71
+ }
72
+
73
+ // Watch reports/ — notify UI when a new report file lands
74
+ const reportsDir = path.resolve(process.cwd(), 'reports');
75
+ fs.mkdirSync(reportsDir, { recursive: true });
76
+ chokidar.watch(reportsDir, { ...watchOpts, interval: 1200 }).on('add', (filePath) => {
77
+ const name = path.basename(filePath);
78
+ if ((name.startsWith('PASS_') || name.startsWith('FAIL_')) && name.endsWith('.json')) {
79
+ console.log(`📊 New report: ${name} — notifying clients`);
80
+ io.emit('report-ready');
81
+ }
82
+ });
83
+ console.log('👀 Watching for new reports...');
84
+ });
85
+ }
86
+
87
+ start().catch((err) => {
88
+ console.error('Failed to start server:', err);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,88 @@
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 prisma = require('./prisma');
19
+
20
+ const exportAll = async () => {
21
+ const [cronJobs, reports, project] = await Promise.all([
22
+ prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } }),
23
+ prisma.report.findMany({
24
+ orderBy: { createdAt: 'asc' },
25
+ include: { cronJob: { select: { taskName: true } } }
26
+ }),
27
+ prisma.project.findUnique({ where: { id: 1 } })
28
+ ]);
29
+
30
+ // Annotate each report with the cronJob's taskName for portable restoration
31
+ const portableReports = reports.map(({ cronJob, cronJobId, ...r }) => ({
32
+ ...r,
33
+ cronJobTaskName: cronJob?.taskName ?? null
34
+ }));
35
+
36
+ return {
37
+ version: '1',
38
+ exportedAt: new Date().toISOString(),
39
+ cronJobs: cronJobs.map(({ id, createdAt, updatedAt, reports: _, ...r }) => r),
40
+ reports: portableReports.map(({ id, createdAt, ...r }) => r),
41
+ project: project ? { name: project.name, logoUrl: project.logoUrl } : null
42
+ };
43
+ };
44
+
45
+ const importAll = async ({ cronJobs = [], reports = [], project = null }, cronService) => {
46
+ await prisma.$transaction(async (tx) => {
47
+ // Upsert cron jobs and build taskName → id map
48
+ const taskNameToId = {};
49
+ for (const job of cronJobs) {
50
+ const upserted = await tx.cronJob.upsert({
51
+ where: { taskName: job.taskName },
52
+ create: {
53
+ taskName: job.taskName,
54
+ cronExpression: job.cronExpression,
55
+ tags: job.tags,
56
+ workers: job.workers ?? 1
57
+ },
58
+ update: { cronExpression: job.cronExpression, tags: job.tags, workers: job.workers ?? 1 }
59
+ });
60
+ taskNameToId[job.taskName] = upserted.id;
61
+ }
62
+
63
+ // Upsert reports, resolving cronJobId from the taskName map
64
+ for (const report of reports) {
65
+ const { cronJobTaskName, ...data } = report;
66
+ const cronJobId = cronJobTaskName ? (taskNameToId[cronJobTaskName] ?? null) : null;
67
+ await tx.report.upsert({
68
+ where: { fileName: data.fileName },
69
+ create: { ...data, cronJobId },
70
+ update: { ...data, cronJobId }
71
+ });
72
+ }
73
+
74
+ // Restore project settings
75
+ if (project) {
76
+ await tx.project.upsert({
77
+ where: { id: 1 },
78
+ create: { id: 1, name: project.name ?? '', logoUrl: project.logoUrl ?? '' },
79
+ update: { name: project.name ?? '', logoUrl: project.logoUrl ?? '' }
80
+ });
81
+ }
82
+ });
83
+
84
+ // Re-schedule cron jobs after import
85
+ if (cronService) await cronService.reload();
86
+ };
87
+
88
+ module.exports = { exportAll, importAll };