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
@@ -19,7 +19,7 @@
19
19
  import { onDestroy, onMount } from 'svelte';
20
20
  import { slide } from 'svelte/transition';
21
21
  import { fetchSuites } from '$lib/api/tests';
22
- import { runnerConfig, triggerRun } from '$lib/stores/runner';
22
+ import { runnerConfig, triggerRun, testsVersion } from '$lib/stores/runner';
23
23
 
24
24
  let suites = [];
25
25
  let search = '';
@@ -27,13 +27,18 @@
27
27
  let copiedIds = new Set();
28
28
  const copyTimers = new Map();
29
29
 
30
- onMount(async () => {
30
+ async function loadSuites() {
31
31
  try {
32
32
  suites = await fetchSuites();
33
33
  } catch (e) {
34
34
  console.error('Failed to fetch suites', e);
35
35
  }
36
- });
36
+ }
37
+
38
+ onMount(loadSuites);
39
+
40
+ // Re-fetch when the backend notifies us that test files changed
41
+ $: if ($testsVersion) loadSuites();
37
42
 
38
43
  function suiteIds(suite) {
39
44
  return Array.isArray(suite.suiteId) ? suite.suiteId : [suite.suiteId];
@@ -105,6 +110,8 @@
105
110
  $: visibleTests = filtered.reduce((n, s) => n + s.tests.length, 0);
106
111
  </script>
107
112
 
113
+ <svelte:head><title>Run Tests — Plum</title></svelte:head>
114
+
108
115
  <div class="page-header">
109
116
  <div class="header-top">
110
117
  <div>
@@ -17,21 +17,29 @@
17
17
 
18
18
  <script>
19
19
  import { onMount, tick } from 'svelte';
20
- import { fetchReports } from '$lib/api/reports';
20
+ import { fetchReports, deleteReport, deleteReports } from '$lib/api/reports';
21
+ import { reportsVersion } from '$lib/stores/runner';
21
22
  import Badge from '$lib/components/ui/Badge.svelte';
22
23
  import Pagination from '$lib/components/ui/Pagination.svelte';
24
+ import Modal from '$lib/components/ui/Modal.svelte';
23
25
 
24
26
  let reports = [];
25
27
  let currentPage = 1;
26
28
  const PER_PAGE = 15;
27
29
  let animateBar = false;
28
30
 
31
+ let selected = new Set();
32
+ let deleteModal = { open: false, targets: [] };
33
+ let deleting = false;
34
+
29
35
  $: totalPages = Math.ceil(reports.length / PER_PAGE);
30
36
  $: paginated = reports.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE);
31
37
  $: passCount = reports.filter((r) => r.status === 'PASS').length;
32
38
  $: failCount = reports.length - passCount;
33
39
  $: passRate = reports.length ? Math.round((passCount / reports.length) * 100) : 0;
34
40
  $: trend = reports.slice(0, 12).reverse();
41
+ $: allOnPageSelected = paginated.length > 0 && paginated.every((r) => selected.has(r.fileName));
42
+ $: someSelected = selected.size > 0;
35
43
 
36
44
  function triggerLabel(type) {
37
45
  if (type === 'manual-trigger') return 'Manual';
@@ -45,17 +53,95 @@
45
53
  return 'schedule';
46
54
  }
47
55
 
48
- onMount(async () => {
56
+ async function loadReports() {
49
57
  try {
50
58
  reports = await fetchReports();
59
+ selected = new Set();
51
60
  await tick();
52
61
  animateBar = true;
53
62
  } catch (e) {
54
63
  console.error('Failed to fetch reports', e);
55
64
  }
56
- });
65
+ }
66
+
67
+ onMount(loadReports);
68
+ $: if ($reportsVersion) loadReports();
69
+
70
+ function toggleSelect(fileName, e) {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ const next = new Set(selected);
74
+ if (next.has(fileName)) next.delete(fileName);
75
+ else next.add(fileName);
76
+ selected = next;
77
+ }
78
+
79
+ function toggleAll(e) {
80
+ e.stopPropagation();
81
+ if (allOnPageSelected) {
82
+ const next = new Set(selected);
83
+ paginated.forEach((r) => next.delete(r.fileName));
84
+ selected = next;
85
+ } else {
86
+ const next = new Set(selected);
87
+ paginated.forEach((r) => next.add(r.fileName));
88
+ selected = next;
89
+ }
90
+ }
91
+
92
+ function openDeleteModal(targets) {
93
+ deleteModal = { open: true, targets };
94
+ }
95
+
96
+ function openSingleDelete(fileName, e) {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ openDeleteModal([fileName]);
100
+ }
101
+
102
+ async function confirmDelete() {
103
+ deleting = true;
104
+ try {
105
+ if (deleteModal.targets.length === 1) {
106
+ await deleteReport(deleteModal.targets[0]);
107
+ } else {
108
+ await deleteReports(deleteModal.targets);
109
+ }
110
+ deleteModal = { open: false, targets: [] };
111
+ await loadReports();
112
+ } catch (e) {
113
+ console.error('Delete failed', e);
114
+ } finally {
115
+ deleting = false;
116
+ }
117
+ }
57
118
  </script>
58
119
 
120
+ <svelte:head><title>Reports — Plum</title></svelte:head>
121
+
122
+ <Modal
123
+ bind:open={deleteModal.open}
124
+ title={deleteModal.targets.length === 1
125
+ ? 'Delete report?'
126
+ : `Delete ${deleteModal.targets.length} reports?`}
127
+ >
128
+ <p class="modal-body">
129
+ {#if deleteModal.targets.length === 1}
130
+ This will permanently remove the report and its data file.
131
+ {:else}
132
+ This will permanently remove {deleteModal.targets.length} reports and their data files.
133
+ {/if}
134
+ </p>
135
+ <div class="modal-actions">
136
+ <button class="btn-cancel" on:click={() => (deleteModal.open = false)} disabled={deleting}>
137
+ Cancel
138
+ </button>
139
+ <button class="btn-danger" on:click={confirmDelete} disabled={deleting}>
140
+ {deleting ? 'Deleting…' : 'Delete'}
141
+ </button>
142
+ </div>
143
+ </Modal>
144
+
59
145
  <div class="page-header">
60
146
  <div class="header-top">
61
147
  <div>
@@ -65,19 +151,27 @@
65
151
  </p>
66
152
  </div>
67
153
 
68
- {#if reports.length > 0}
69
- <div class="rate-display">
70
- <span
71
- class="rate-number"
72
- class:pass={passRate >= 80}
73
- class:warn={passRate < 80 && passRate >= 50}
74
- class:fail={passRate < 50}
75
- >
76
- {passRate}%
77
- </span>
78
- <span class="rate-label">passing</span>
79
- </div>
80
- {/if}
154
+ <div class="header-actions">
155
+ {#if someSelected}
156
+ <button class="btn-delete-selected" on:click={() => openDeleteModal([...selected])}>
157
+ Delete ({selected.size})
158
+ </button>
159
+ {/if}
160
+
161
+ {#if reports.length > 0}
162
+ <div class="rate-display">
163
+ <span
164
+ class="rate-number"
165
+ class:pass={passRate >= 80}
166
+ class:warn={passRate < 80 && passRate >= 50}
167
+ class:fail={passRate < 50}
168
+ >
169
+ {passRate}%
170
+ </span>
171
+ <span class="rate-label">passing</span>
172
+ </div>
173
+ {/if}
174
+ </div>
81
175
  </div>
82
176
 
83
177
  {#if reports.length > 0}
@@ -112,39 +206,84 @@
112
206
  {#if reports.length === 0}
113
207
  <p class="empty">No reports yet. Run a test to generate one.</p>
114
208
  {:else}
209
+ <div class="list-header">
210
+ <label class="select-all-wrap" title="Select all on this page">
211
+ <input
212
+ type="checkbox"
213
+ class="checkbox"
214
+ checked={allOnPageSelected}
215
+ indeterminate={someSelected && !allOnPageSelected}
216
+ on:change={toggleAll}
217
+ />
218
+ </label>
219
+ </div>
220
+
115
221
  <div class="report-list">
116
222
  {#each paginated as report, i}
117
- <a
118
- class="report-item"
119
- class:is-pass={report.status === 'PASS'}
120
- class:is-fail={report.status !== 'PASS'}
121
- href="/reports/{encodeURIComponent(report.fileName)}"
223
+ <div
224
+ class="report-row"
225
+ class:is-selected={selected.has(report.fileName)}
122
226
  style="animation-delay: {i * 45}ms"
123
227
  >
124
- <div class="item-left">
125
- <span
126
- class="status-mark"
127
- class:pass={report.status === 'PASS'}
128
- class:fail={report.status !== 'PASS'}
129
- >
130
- {report.status === 'PASS' ? '✓' : '✗'}
131
- </span>
132
- <div class="item-meta">
133
- <span class="item-tags">{report.tags}</span>
134
- <div class="item-badges">
135
- <Badge variant={triggerVariant(report.triggerType)}>
136
- {triggerLabel(report.triggerType)}
137
- </Badge>
138
- <Badge variant="neutral">
139
- {report.runners} runner{report.runners !== 1 ? 's' : ''}
140
- </Badge>
228
+ <label class="row-check-wrap" title="Select">
229
+ <input
230
+ type="checkbox"
231
+ class="checkbox"
232
+ checked={selected.has(report.fileName)}
233
+ on:change={(e) => toggleSelect(report.fileName, e)}
234
+ />
235
+ </label>
236
+
237
+ <a
238
+ class="report-item"
239
+ class:is-pass={report.status === 'PASS'}
240
+ class:is-fail={report.status !== 'PASS'}
241
+ href="/reports/{encodeURIComponent(report.fileName)}"
242
+ >
243
+ <div class="item-left">
244
+ <span
245
+ class="status-mark"
246
+ class:pass={report.status === 'PASS'}
247
+ class:fail={report.status !== 'PASS'}
248
+ >
249
+ {report.status === 'PASS' ? '✓' : '✗'}
250
+ </span>
251
+ <div class="item-meta">
252
+ <span class="item-tags">{report.tags}</span>
253
+ <div class="item-badges">
254
+ <Badge variant={triggerVariant(report.triggerType)}>
255
+ {triggerLabel(report.triggerType)}
256
+ </Badge>
257
+ <Badge variant="neutral">
258
+ {report.runners} runner{report.runners !== 1 ? 's' : ''}
259
+ </Badge>
260
+ </div>
141
261
  </div>
142
262
  </div>
143
- </div>
144
- <div class="item-right">
145
- <span class="item-date">{report.date}</span>
263
+ <div class="item-right">
264
+ <span class="item-date">{report.date}</span>
265
+ <svg
266
+ class="item-arrow"
267
+ width="14"
268
+ height="14"
269
+ viewBox="0 0 24 24"
270
+ fill="none"
271
+ stroke="currentColor"
272
+ stroke-width="2"
273
+ stroke-linecap="round"
274
+ >
275
+ <line x1="5" y1="12" x2="19" y2="12" />
276
+ <polyline points="12 5 19 12 12 19" />
277
+ </svg>
278
+ </div>
279
+ </a>
280
+
281
+ <button
282
+ class="row-delete-btn"
283
+ title="Delete report"
284
+ on:click={(e) => openSingleDelete(report.fileName, e)}
285
+ >
146
286
  <svg
147
- class="item-arrow"
148
287
  width="14"
149
288
  height="14"
150
289
  viewBox="0 0 24 24"
@@ -152,12 +291,15 @@
152
291
  stroke="currentColor"
153
292
  stroke-width="2"
154
293
  stroke-linecap="round"
294
+ stroke-linejoin="round"
155
295
  >
156
- <line x1="5" y1="12" x2="19" y2="12" />
157
- <polyline points="12 5 19 12 12 19" />
296
+ <polyline points="3 6 5 6 21 6" />
297
+ <path d="M19 6l-1 14H6L5 6" />
298
+ <path d="M10 11v6M14 11v6" />
299
+ <path d="M9 6V4h6v2" />
158
300
  </svg>
159
- </div>
160
- </a>
301
+ </button>
302
+ </div>
161
303
  {/each}
162
304
  </div>
163
305
 
@@ -197,6 +339,33 @@
197
339
  font-size: 0.875rem;
198
340
  }
199
341
 
342
+ .header-actions {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 1rem;
346
+ }
347
+
348
+ .btn-delete-selected {
349
+ height: 32px;
350
+ padding: 0 0.875rem;
351
+ font-size: 0.8125rem;
352
+ font-family: inherit;
353
+ font-weight: 500;
354
+ color: var(--fail);
355
+ background: var(--fail-soft, rgba(239, 68, 68, 0.08));
356
+ border: 1px solid var(--fail);
357
+ border-radius: var(--radius-sm);
358
+ cursor: pointer;
359
+ transition:
360
+ background var(--duration-fast),
361
+ opacity var(--duration-fast);
362
+ }
363
+
364
+ .btn-delete-selected:hover {
365
+ background: var(--fail);
366
+ color: #fff;
367
+ }
368
+
200
369
  /* ── Pass rate ── */
201
370
  .rate-display {
202
371
  display: flex;
@@ -253,13 +422,11 @@
253
422
  display: flex;
254
423
  gap: 1rem;
255
424
  }
256
-
257
425
  .legend-pass {
258
426
  font-size: 0.75rem;
259
427
  color: var(--pass);
260
428
  font-weight: 500;
261
429
  }
262
-
263
430
  .legend-fail {
264
431
  font-size: 0.75rem;
265
432
  color: var(--fail);
@@ -299,7 +466,6 @@
299
466
  .trend-dot:hover {
300
467
  transform: scale(1.4);
301
468
  }
302
-
303
469
  .trend-dot.pass {
304
470
  background: var(--pass);
305
471
  }
@@ -313,14 +479,51 @@
313
479
  opacity: 0.6;
314
480
  }
315
481
 
316
- /* ── Report list ── */
482
+ /* ── Select all row ── */
483
+ .list-header {
484
+ display: flex;
485
+ align-items: center;
486
+ padding: 0 0.5rem 0.375rem;
487
+ }
488
+
489
+ .select-all-wrap {
490
+ display: flex;
491
+ align-items: center;
492
+ cursor: pointer;
493
+ padding: 0.25rem;
494
+ }
495
+
496
+ /* ── Report rows ── */
317
497
  .report-list {
318
498
  display: flex;
319
499
  flex-direction: column;
320
500
  gap: 0.5rem;
321
501
  }
322
502
 
503
+ .report-row {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: 0.5rem;
507
+ animation: fadeUp 0.32s var(--ease-out) both;
508
+ }
509
+
510
+ .row-check-wrap {
511
+ flex-shrink: 0;
512
+ display: flex;
513
+ align-items: center;
514
+ cursor: pointer;
515
+ padding: 0.25rem;
516
+ }
517
+
518
+ .checkbox {
519
+ width: 15px;
520
+ height: 15px;
521
+ accent-color: var(--accent, #7c3aed);
522
+ cursor: pointer;
523
+ }
524
+
323
525
  .report-item {
526
+ flex: 1;
324
527
  display: flex;
325
528
  align-items: center;
326
529
  justify-content: space-between;
@@ -336,7 +539,7 @@
336
539
  background var(--duration-fast),
337
540
  transform var(--duration-fast) var(--ease-out),
338
541
  box-shadow var(--duration-fast);
339
- animation: fadeUp 0.32s var(--ease-out) both;
542
+ min-width: 0;
340
543
  }
341
544
 
342
545
  .report-item:hover {
@@ -352,6 +555,11 @@
352
555
  border-left-color: var(--fail);
353
556
  }
354
557
 
558
+ .report-row.is-selected .report-item {
559
+ background: var(--bg-subtle);
560
+ border-color: var(--accent, #7c3aed);
561
+ }
562
+
355
563
  .item-left {
356
564
  display: flex;
357
565
  align-items: center;
@@ -417,6 +625,89 @@
417
625
  color: var(--text);
418
626
  }
419
627
 
628
+ /* ── Per-row delete button ── */
629
+ .row-delete-btn {
630
+ flex-shrink: 0;
631
+ display: flex;
632
+ align-items: center;
633
+ justify-content: center;
634
+ width: 30px;
635
+ height: 30px;
636
+ border: none;
637
+ border-radius: var(--radius-sm);
638
+ background: transparent;
639
+ color: var(--text-muted);
640
+ cursor: pointer;
641
+ opacity: 0;
642
+ transition:
643
+ opacity var(--duration-fast),
644
+ color var(--duration-fast),
645
+ background var(--duration-fast);
646
+ }
647
+
648
+ .report-row:hover .row-delete-btn {
649
+ opacity: 1;
650
+ }
651
+
652
+ .row-delete-btn:hover {
653
+ color: var(--fail);
654
+ background: var(--fail-soft, rgba(239, 68, 68, 0.08));
655
+ }
656
+
657
+ /* ── Modal ── */
658
+ .modal-actions {
659
+ display: flex;
660
+ justify-content: flex-end;
661
+ gap: 0.5rem;
662
+ }
663
+
664
+ .modal-body {
665
+ font-size: 0.9rem;
666
+ color: var(--text-muted);
667
+ line-height: 1.6;
668
+ margin: 0;
669
+ }
670
+
671
+ .btn-cancel {
672
+ height: 34px;
673
+ padding: 0 1rem;
674
+ font-size: 0.8125rem;
675
+ font-family: inherit;
676
+ background: var(--bg-elevated);
677
+ border: 1px solid var(--border);
678
+ border-radius: var(--radius-sm);
679
+ cursor: pointer;
680
+ color: var(--text);
681
+ transition: background var(--duration-fast);
682
+ }
683
+
684
+ .btn-cancel:hover {
685
+ background: var(--bg-subtle);
686
+ }
687
+
688
+ .btn-danger {
689
+ height: 34px;
690
+ padding: 0 1rem;
691
+ font-size: 0.8125rem;
692
+ font-family: inherit;
693
+ font-weight: 500;
694
+ background: var(--fail);
695
+ border: 1px solid var(--fail);
696
+ border-radius: var(--radius-sm);
697
+ cursor: pointer;
698
+ color: #fff;
699
+ transition: opacity var(--duration-fast);
700
+ }
701
+
702
+ .btn-danger:hover:not(:disabled) {
703
+ opacity: 0.85;
704
+ }
705
+ .btn-danger:disabled {
706
+ opacity: 0.5;
707
+ cursor: not-allowed;
708
+ }
709
+
710
+ /* ── Misc ── */
420
711
  .pagination-wrap {
421
712
  margin-top: 1.25rem;
422
713
  }
@@ -123,6 +123,8 @@
123
123
  })) ?? [];
124
124
  </script>
125
125
 
126
+ <svelte:head><title>Report — Plum</title></svelte:head>
127
+
126
128
  <div class="back-row">
127
129
  <a href="/reports" class="back-link">
128
130
  <svg