plum-e2e 2.8.6 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/backend/_scaffold/utils/browser.ts +184 -30
  2. package/backend/_scaffold/utils/hooks.ts +37 -8
  3. package/backend/app.js +0 -5
  4. package/backend/config/scripts/generate-report.js +2 -2
  5. package/backend/constants/socketEvents.js +7 -4
  6. package/backend/lib/reportFilename.js +1 -2
  7. package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
  8. package/backend/lib/serverBootstrap.js +14 -0
  9. package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
  10. package/backend/mcp/server.js +3 -47
  11. package/backend/package-lock.json +199 -1
  12. package/backend/package.json +3 -1
  13. package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
  14. package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
  15. package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
  16. package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
  17. package/backend/prisma/schema.prisma +97 -70
  18. package/backend/routes/backup.routes.js +47 -1
  19. package/backend/routes/reports.routes.js +23 -0
  20. package/backend/server.js +1 -1
  21. package/backend/services/backupCronService.js +1 -1
  22. package/backend/services/backupService.js +134 -44
  23. package/backend/services/cronService.js +23 -15
  24. package/backend/services/nodeExecutionService.js +41 -15
  25. package/backend/services/nodeStreamRegistry.js +24 -0
  26. package/backend/services/reportService.js +167 -83
  27. package/backend/services/runnerService.js +19 -7
  28. package/backend/services/settingsService.js +6 -3
  29. package/backend/services/triggerService.js +20 -13
  30. package/backend/websockets/nodeSocketHandler.js +40 -0
  31. package/backend/websockets/socketHandler.js +9 -7
  32. package/frontend/.svelte-kit/generated/server/internal.js +1 -1
  33. package/frontend/package-lock.json +121 -32
  34. package/frontend/package.json +1 -0
  35. package/frontend/src/lib/api/reports.js +13 -4
  36. package/frontend/src/lib/api/settings.js +16 -1
  37. package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
  38. package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
  39. package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
  40. package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
  41. package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
  42. package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
  43. package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
  44. package/frontend/src/lib/constants.js +0 -1
  45. package/frontend/src/lib/copy/reports.js +18 -8
  46. package/frontend/src/lib/copy/settings.js +25 -4
  47. package/frontend/src/lib/socketEvents.js +7 -4
  48. package/frontend/src/lib/stores/runner.js +26 -3
  49. package/frontend/src/lib/styles/tokens.css +7 -0
  50. package/frontend/src/lib/utils/format.js +108 -2
  51. package/frontend/src/lib/utils/inspectElement.js +34 -0
  52. package/frontend/src/routes/reports/+page.svelte +79 -1
  53. package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
  54. package/frontend/src/routes/reports/live/+page.svelte +236 -260
  55. package/frontend/src/routes/settings/+page.svelte +246 -8
  56. package/package.json +1 -1
  57. package/backend/playwright.config.js +0 -85
@@ -13,7 +13,7 @@ const reportService = require('../services/reportService');
13
13
  const settingsService = require('../services/settingsService');
14
14
  const notificationService = require('../services/notificationService');
15
15
  const activeRunsService = require('../services/activeRunsService');
16
- const { startSsPoller } = require('../lib/screenshotPoller');
16
+ const { startRRwebPoller } = require('../lib/rrwebPoller');
17
17
  const { runWithRetries } = require('../lib/retryRunner');
18
18
  const { TRIGGER_TYPE, BUILT_IN_RUNNER_ID, TRIGGER_REMOTE } = require('../constants/triggers');
19
19
  const { DEFAULT_BROWSER } = require('../constants/defaults');
@@ -243,8 +243,8 @@ function runBuiltInAttempt({
243
243
  const proc = spawn('npm', ['run', 'test'], { env, shell: true });
244
244
  activeProcs.add(proc);
245
245
 
246
- const ssPoller = startSsPoller(ssDir, ({ stepName, data }) => {
247
- socket.emit(SOCKET_EVENTS.STEP_SCREENSHOT, { stepName, data });
246
+ const ssPoller = startRRwebPoller(ssDir, (batch) => {
247
+ socket.emit(SOCKET_EVENTS.RUNNER_LANE_RRWEB_BATCH, { id: BUILT_IN_RUNNER_ID, ...batch });
248
248
  });
249
249
 
250
250
  proc.stdout.on('data', (d) => onLog(d.toString()));
@@ -362,6 +362,7 @@ async function runBuiltIn(
362
362
  rawCucumberJson: rawJson,
363
363
  tags: tag,
364
364
  triggerType: TRIGGER_TYPE.MANUAL,
365
+ workerCount: workers,
365
366
  browser,
366
367
  testRunId: testRunId ?? null,
367
368
  logs: logBuffer || null,
@@ -468,6 +469,7 @@ async function runDistributed(
468
469
  .saveCombinedReport({
469
470
  reports: collectedReports,
470
471
  runners: laneInfos,
472
+ workers,
471
473
  overallCode,
472
474
  tag,
473
475
  triggerType: TRIGGER_TYPE.MANUAL,
@@ -543,8 +545,8 @@ async function runDistributed(
543
545
  const proc = spawn('npm', ['run', 'test'], { env, shell: true });
544
546
  activeProcs.add(proc);
545
547
 
546
- const ssPoller = startSsPoller(ssDir, ({ stepName, data }) => {
547
- socket.emit(SOCKET_EVENTS.RUNNER_LANE_SCREENSHOT, { id: laneId, stepName, data });
548
+ const ssPoller = startRRwebPoller(ssDir, (batch) => {
549
+ socket.emit(SOCKET_EVENTS.RUNNER_LANE_RRWEB_BATCH, { id: laneId, ...batch });
548
550
  });
549
551
 
550
552
  proc.stdout.on('data', (d) => onLog(d.toString()));
@@ -588,8 +590,8 @@ async function runDistributed(
588
590
  makeSyntheticFailReport(lane.name, chunkIds, 'could not fetch report from runner');
589
591
  resolve({ code, rawJson: JSON.parse(raw) });
590
592
  },
591
- ({ stepName, data }) => {
592
- socket.emit(SOCKET_EVENTS.RUNNER_LANE_SCREENSHOT, { id: laneId, stepName, data });
593
+ (batch) => {
594
+ socket.emit(SOCKET_EVENTS.RUNNER_LANE_RRWEB_BATCH, { id: laneId, ...batch });
593
595
  }
594
596
  );
595
597
  });
@@ -26,7 +26,7 @@ export const options = {
26
26
  app: ({ head, body, assets, nonce, env }) => "<!--\nThis file is part of Plum.\nLicensed under the MIT License. See LICENSE file in the project root for details.\n-->\n<!doctype html>\n<html lang=\"en\" data-theme=\"light\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.ico\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n\t\t<link\n\t\t\thref=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap\"\n\t\t\trel=\"stylesheet\"\n\t\t/>\n\t\t<!-- Prevent theme flash before Svelte hydrates -->\n\t\t<script>\n\t\t\ttry {\n\t\t\t\tconst t = localStorage.getItem('plum-theme');\n\t\t\t\tif (t) document.documentElement.setAttribute('data-theme', t);\n\t\t\t} catch (e) {}\n\t\t</script>\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
27
27
  error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
28
28
  },
29
- version_hash: "1cz1h1z"
29
+ version_hash: "19o4i41"
30
30
  };
31
31
 
32
32
  export async function get_hooks() {
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "plum-frontend",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "plum-frontend",
9
- "version": "1.3.3",
9
+ "version": "1.4.0",
10
+ "license": "MIT",
10
11
  "dependencies": {
12
+ "rrweb-player": "^2.1.1",
11
13
  "socket.io-client": "^4.8.1"
12
14
  },
13
15
  "devDependencies": {
@@ -986,6 +988,38 @@
986
988
  "win32"
987
989
  ]
988
990
  },
991
+ "node_modules/@rrweb/packer": {
992
+ "version": "2.1.1",
993
+ "resolved": "https://registry.npmjs.org/@rrweb/packer/-/packer-2.1.1.tgz",
994
+ "integrity": "sha512-xPGT7/Cfn1yHD2MOwQhX/8yTvNculurUxPT6ZKXVW8Wh/LI7Jn19L+e8PYKFhj/M89uwNeoTr6hiUQxgb8SMzQ==",
995
+ "license": "MIT",
996
+ "dependencies": {
997
+ "@rrweb/types": "^2.1.1",
998
+ "fflate": "^0.4.4"
999
+ }
1000
+ },
1001
+ "node_modules/@rrweb/replay": {
1002
+ "version": "2.1.1",
1003
+ "resolved": "https://registry.npmjs.org/@rrweb/replay/-/replay-2.1.1.tgz",
1004
+ "integrity": "sha512-jq64eyJvrh/ioBS1MpDrs/Lj+7QY2TW4xGxp4g4LJKdltSkqMjMbUmhTDcpzaEDZt66S0gasGcgqCulsksSXnw==",
1005
+ "license": "MIT",
1006
+ "dependencies": {
1007
+ "@rrweb/types": "^2.1.1",
1008
+ "rrweb": "^2.1.1"
1009
+ }
1010
+ },
1011
+ "node_modules/@rrweb/types": {
1012
+ "version": "2.1.1",
1013
+ "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.1.1.tgz",
1014
+ "integrity": "sha512-g0I3nCNL1S7slDXwhunxOuOoswkY8WVZJQrPFiwixOc6xoD514d1JLTuyAzCKIox8g/PaLKtV5g31Uk42inQSQ==",
1015
+ "license": "MIT"
1016
+ },
1017
+ "node_modules/@rrweb/utils": {
1018
+ "version": "2.1.1",
1019
+ "resolved": "https://registry.npmjs.org/@rrweb/utils/-/utils-2.1.1.tgz",
1020
+ "integrity": "sha512-x2SgJAD3YJ9eVcLZ5l6OqWMtczdA3VfS5XqPeqpZdQgV9oh6Id5GK2cDN4bimnkqryOcIJdz8cTvV0yNvqQ+nA==",
1021
+ "license": "MIT"
1022
+ },
989
1023
  "node_modules/@socket.io/component-emitter": {
990
1024
  "version": "3.1.2",
991
1025
  "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -1087,12 +1121,24 @@
1087
1121
  "vite": "^6.0.0"
1088
1122
  }
1089
1123
  },
1124
+ "node_modules/@tsconfig/svelte": {
1125
+ "version": "1.0.13",
1126
+ "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-1.0.13.tgz",
1127
+ "integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==",
1128
+ "license": "MIT"
1129
+ },
1090
1130
  "node_modules/@types/cookie": {
1091
1131
  "version": "0.6.0",
1092
1132
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
1093
1133
  "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
1094
1134
  "dev": true
1095
1135
  },
1136
+ "node_modules/@types/css-font-loading-module": {
1137
+ "version": "0.0.7",
1138
+ "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
1139
+ "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
1140
+ "license": "MIT"
1141
+ },
1096
1142
  "node_modules/@types/estree": {
1097
1143
  "version": "1.0.9",
1098
1144
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1107,6 +1153,12 @@
1107
1153
  "dev": true,
1108
1154
  "license": "MIT"
1109
1155
  },
1156
+ "node_modules/@xstate/fsm": {
1157
+ "version": "1.6.5",
1158
+ "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz",
1159
+ "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
1160
+ "license": "MIT"
1161
+ },
1110
1162
  "node_modules/acorn": {
1111
1163
  "version": "8.14.0",
1112
1164
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -1146,6 +1198,15 @@
1146
1198
  "node": ">= 0.4"
1147
1199
  }
1148
1200
  },
1201
+ "node_modules/base64-arraybuffer": {
1202
+ "version": "1.0.2",
1203
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
1204
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
1205
+ "license": "MIT",
1206
+ "engines": {
1207
+ "node": ">= 0.6.0"
1208
+ }
1209
+ },
1149
1210
  "node_modules/clsx": {
1150
1211
  "version": "2.1.1",
1151
1212
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1301,6 +1362,12 @@
1301
1362
  "dev": true,
1302
1363
  "license": "MIT"
1303
1364
  },
1365
+ "node_modules/fflate": {
1366
+ "version": "0.4.9",
1367
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.9.tgz",
1368
+ "integrity": "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==",
1369
+ "license": "MIT"
1370
+ },
1304
1371
  "node_modules/fsevents": {
1305
1372
  "version": "2.3.3",
1306
1373
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1377,17 +1444,6 @@
1377
1444
  "@types/estree": "^1.0.6"
1378
1445
  }
1379
1446
  },
1380
- "node_modules/jiti": {
1381
- "version": "1.21.7",
1382
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1383
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1384
- "dev": true,
1385
- "optional": true,
1386
- "peer": true,
1387
- "bin": {
1388
- "jiti": "bin/jiti.js"
1389
- }
1390
- },
1391
1447
  "node_modules/kleur": {
1392
1448
  "version": "4.1.5",
1393
1449
  "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -1412,6 +1468,12 @@
1412
1468
  "@jridgewell/sourcemap-codec": "^1.5.0"
1413
1469
  }
1414
1470
  },
1471
+ "node_modules/mitt": {
1472
+ "version": "3.0.1",
1473
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
1474
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
1475
+ "license": "MIT"
1476
+ },
1415
1477
  "node_modules/mri": {
1416
1478
  "version": "1.2.0",
1417
1479
  "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -1439,7 +1501,6 @@
1439
1501
  "version": "3.3.8",
1440
1502
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
1441
1503
  "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
1442
- "dev": true,
1443
1504
  "funding": [
1444
1505
  {
1445
1506
  "type": "github",
@@ -1462,14 +1523,12 @@
1462
1523
  "node_modules/picocolors": {
1463
1524
  "version": "1.1.1",
1464
1525
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1465
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1466
- "dev": true
1526
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
1467
1527
  },
1468
1528
  "node_modules/postcss": {
1469
1529
  "version": "8.5.3",
1470
1530
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
1471
1531
  "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
1472
- "dev": true,
1473
1532
  "funding": [
1474
1533
  {
1475
1534
  "type": "opencollective",
@@ -1558,6 +1617,51 @@
1558
1617
  "fsevents": "~2.3.2"
1559
1618
  }
1560
1619
  },
1620
+ "node_modules/rrdom": {
1621
+ "version": "2.1.1",
1622
+ "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.1.1.tgz",
1623
+ "integrity": "sha512-VBkTF3bGNqcZjqnbo/gKi9GVQPIxiG0dofw7CQdAZQFQ2juJdt2YKIf3oS9QudL8HTpGh9bz5TUN+3amBn1Geg==",
1624
+ "license": "MIT",
1625
+ "dependencies": {
1626
+ "rrweb-snapshot": "^2.1.1"
1627
+ }
1628
+ },
1629
+ "node_modules/rrweb": {
1630
+ "version": "2.1.1",
1631
+ "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.1.1.tgz",
1632
+ "integrity": "sha512-ToxhJg3SsrAhw+/DPhI/2iiwZQIrGK5BGkZ0kHn4qUExcvQYRaolkciD1FWX2+r6vf1IxFyhsoRY18h3D+XoCg==",
1633
+ "license": "MIT",
1634
+ "dependencies": {
1635
+ "@rrweb/types": "^2.1.1",
1636
+ "@rrweb/utils": "^2.1.1",
1637
+ "@types/css-font-loading-module": "0.0.7",
1638
+ "@xstate/fsm": "^1.4.0",
1639
+ "base64-arraybuffer": "^1.0.1",
1640
+ "mitt": "^3.0.0",
1641
+ "rrdom": "^2.1.1",
1642
+ "rrweb-snapshot": "^2.1.1"
1643
+ }
1644
+ },
1645
+ "node_modules/rrweb-player": {
1646
+ "version": "2.1.1",
1647
+ "resolved": "https://registry.npmjs.org/rrweb-player/-/rrweb-player-2.1.1.tgz",
1648
+ "integrity": "sha512-oiAWx/m9Pz0DD5jlJdrF8QlotOZZBK0jINNSYAixxhGa2dI1TrzVVmF4eWFSd6AR8f2zjazKXI/PXCqj3PttXQ==",
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "@rrweb/packer": "^2.1.1",
1652
+ "@rrweb/replay": "^2.1.1",
1653
+ "@tsconfig/svelte": "^1.0.0"
1654
+ }
1655
+ },
1656
+ "node_modules/rrweb-snapshot": {
1657
+ "version": "2.1.1",
1658
+ "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.1.1.tgz",
1659
+ "integrity": "sha512-al4Kx7Am7YlIn/5Yb2EMr7cdmjGiK+cLhdilJEXqkFXgpnys8t/yMJumXy2Ormgirpg3MBnFha0GTgny+iIL2w==",
1660
+ "license": "MIT",
1661
+ "dependencies": {
1662
+ "postcss": "^8.4.38"
1663
+ }
1664
+ },
1561
1665
  "node_modules/sade": {
1562
1666
  "version": "1.8.1",
1563
1667
  "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -1652,7 +1756,6 @@
1652
1756
  "version": "1.2.1",
1653
1757
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1654
1758
  "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1655
- "dev": true,
1656
1759
  "engines": {
1657
1760
  "node": ">=0.10.0"
1658
1761
  }
@@ -1820,20 +1923,6 @@
1820
1923
  "node": ">=0.4.0"
1821
1924
  }
1822
1925
  },
1823
- "node_modules/yaml": {
1824
- "version": "2.7.0",
1825
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
1826
- "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
1827
- "dev": true,
1828
- "optional": true,
1829
- "peer": true,
1830
- "bin": {
1831
- "yaml": "bin.mjs"
1832
- },
1833
- "engines": {
1834
- "node": ">= 14"
1835
- }
1836
- },
1837
1926
  "node_modules/zimmerframe": {
1838
1927
  "version": "1.1.2",
1839
1928
  "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
@@ -19,6 +19,7 @@
19
19
  "vite": "^6.0.0"
20
20
  },
21
21
  "dependencies": {
22
+ "rrweb-player": "^2.1.1",
22
23
  "socket.io-client": "^4.8.1"
23
24
  }
24
25
  }
@@ -32,16 +32,25 @@ export function reportUrl(id) {
32
32
  return `/reports/${id}`;
33
33
  }
34
34
 
35
- export function screenshotUrl(filename) {
36
- return `${API_BASE}/screenshots/${filename}`;
37
- }
38
-
39
35
  export async function fetchReportDetail(id) {
40
36
  const res = await fetch(`${API_BASE}/reports/${id}`);
41
37
  if (!res.ok) throw new Error('Report not found');
42
38
  return res.json();
43
39
  }
44
40
 
41
+ export async function fetchRecordings(reportId) {
42
+ const res = await fetch(`${API_BASE}/reports/${reportId}/recordings`);
43
+ if (!res.ok) throw new Error('Failed to fetch recordings');
44
+ return res.json();
45
+ }
46
+
47
+ export async function fetchRecordingEvents(reportId, recordingId) {
48
+ const res = await fetch(`${API_BASE}/reports/${reportId}/recordings/${recordingId}/events`);
49
+ if (!res.ok) throw new Error('Failed to fetch recording events');
50
+ const { events } = await res.json();
51
+ return events;
52
+ }
53
+
45
54
  export async function deleteReport(id) {
46
55
  const res = await fetch(`${API_BASE}/reports/${id}`, { method: 'DELETE' });
47
56
  if (!res.ok) throw new Error('Failed to delete report');
@@ -53,7 +53,8 @@ export async function fetchBackupConfig() {
53
53
  backupS3SecretKeySet: false,
54
54
  backupS3Prefix: '',
55
55
  backupLastRunAt: null,
56
- backupLastStatus: ''
56
+ backupLastStatus: '',
57
+ backupIncludeReports: false
57
58
  };
58
59
  return res.json();
59
60
  }
@@ -81,6 +82,20 @@ export async function runBackupNow() {
81
82
  return res.json();
82
83
  }
83
84
 
85
+ export async function fetchS3Backups() {
86
+ const res = await fetch(`${API_BASE}/backup/s3-backups`, { headers: authHeaders() });
87
+ return res.json();
88
+ }
89
+
90
+ export async function restoreFromS3(key) {
91
+ const res = await fetch(`${API_BASE}/backup/s3-restore`, {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
94
+ body: JSON.stringify({ key })
95
+ });
96
+ return res.json();
97
+ }
98
+
84
99
  export async function fetchIntegrations() {
85
100
  const res = await fetch(`${API_BASE}/settings/integrations`, { headers: authHeaders() });
86
101
  if (!res.ok) return { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
@@ -17,7 +17,9 @@
17
17
  triggerRun,
18
18
  reportsVersion,
19
19
  runsVersion,
20
- backgroundRuns
20
+ backgroundRuns,
21
+ appendRRwebBatch,
22
+ mergeRRwebBatch
21
23
  } from '$lib/stores/runner';
22
24
  import { fetchLatestReportId, reportUrl } from '$lib/api/reports';
23
25
  import { fetchRunners } from '$lib/api/runners';
@@ -121,7 +123,7 @@
121
123
  runTitle: label,
122
124
  startedBy: meta?.startedBy
123
125
  },
124
- latestScreenshot: null
126
+ rrwebByLane: {}
125
127
  };
126
128
  }
127
129
 
@@ -236,7 +238,7 @@
236
238
  s.on(SOCKET_EVENTS.RUNNER_LANES_INIT, (lanes) => {
237
239
  runnerState.update((r) => ({
238
240
  ...r,
239
- lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '', latestScreenshot: null }))
241
+ lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '' }))
240
242
  }));
241
243
  });
242
244
 
@@ -254,18 +256,7 @@
254
256
  }));
255
257
  });
256
258
 
257
- s.on(SOCKET_EVENTS.STEP_SCREENSHOT, ({ stepName, data }) => {
258
- runnerState.update((r) => ({ ...r, latestScreenshot: { stepName, data } }));
259
- });
260
-
261
- s.on(SOCKET_EVENTS.RUNNER_LANE_SCREENSHOT, ({ id, stepName, data }) => {
262
- runnerState.update((r) => ({
263
- ...r,
264
- lanes: r.lanes.map((l) =>
265
- l.id === id ? { ...l, latestScreenshot: { stepName, data } } : l
266
- )
267
- }));
268
- });
259
+ s.on(SOCKET_EVENTS.RUNNER_LANE_RRWEB_BATCH, (batch) => appendRRwebBatch(batch));
269
260
 
270
261
  s.on(SOCKET_EVENTS.REPORT_READY, () => reportsVersion.update((v) => v + 1));
271
262
 
@@ -288,7 +279,7 @@
288
279
  s.on(SOCKET_EVENTS.BG_RUN_LANES_INIT, ({ runId, lanes }) => {
289
280
  updateBgRun(runId, (run) => ({
290
281
  ...run,
291
- lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '', latestScreenshot: null }))
282
+ lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '' }))
292
283
  }));
293
284
  });
294
285
 
@@ -306,16 +297,10 @@
306
297
  }));
307
298
  });
308
299
 
309
- s.on(SOCKET_EVENTS.BG_RUN_SCREENSHOT, ({ runId, stepName, data }) => {
310
- updateBgRun(runId, (run) => ({ ...run, latestScreenshot: { stepName, data } }));
311
- });
312
-
313
- s.on(SOCKET_EVENTS.BG_RUN_LANE_SCREENSHOT, ({ runId, laneId, stepName, data }) => {
300
+ s.on(SOCKET_EVENTS.BG_RUN_LANE_RRWEB_BATCH, ({ runId, ...batch }) => {
314
301
  updateBgRun(runId, (run) => ({
315
302
  ...run,
316
- lanes: run.lanes.map((l) =>
317
- l.id === laneId ? { ...l, latestScreenshot: { stepName, data } } : l
318
- )
303
+ rrwebByLane: mergeRRwebBatch(run.rrwebByLane, batch)
319
304
  }));
320
305
  });
321
306
 
@@ -0,0 +1,141 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ * Licensed under the MIT License. See LICENSE file in the project root for details.
4
+ -->
5
+
6
+ <script>
7
+ import CodeViewer from '$lib/components/ui/CodeViewer.svelte';
8
+ import {
9
+ INSPECTOR_HEADING,
10
+ NO_ELEMENT_SELECTED,
11
+ ELEMENT_ATTRIBUTES_LABEL,
12
+ ELEMENT_SIZE_LABEL
13
+ } from '$lib/copy/reports';
14
+
15
+ export let selectedElement = null;
16
+ </script>
17
+
18
+ <aside class="inspector-panel">
19
+ <div class="inspector-header">
20
+ <svg
21
+ width="12"
22
+ height="12"
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ stroke-width="2"
27
+ stroke-linecap="round"
28
+ stroke-linejoin="round"
29
+ >
30
+ <path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" />
31
+ </svg>
32
+ {INSPECTOR_HEADING}
33
+ </div>
34
+ {#if selectedElement}
35
+ <CodeViewer code={selectedElement.markup} />
36
+ <div class="inspector-section">
37
+ <span class="inspector-section-label">{ELEMENT_SIZE_LABEL}</span>
38
+ <span class="inspector-size">{selectedElement.box.width} × {selectedElement.box.height}</span>
39
+ </div>
40
+ {#if selectedElement.attributes.length > 0}
41
+ <div class="inspector-section">
42
+ <span class="inspector-section-label">{ELEMENT_ATTRIBUTES_LABEL}</span>
43
+ </div>
44
+ <dl class="inspector-attrs">
45
+ {#each selectedElement.attributes as attr}
46
+ <div class="inspector-attr-row">
47
+ <dt>{attr.name}</dt>
48
+ <dd>{attr.value}</dd>
49
+ </div>
50
+ {/each}
51
+ </dl>
52
+ {/if}
53
+ {:else}
54
+ <div class="inspector-empty">{NO_ELEMENT_SELECTED}</div>
55
+ {/if}
56
+ </aside>
57
+
58
+ <style>
59
+ .inspector-panel {
60
+ flex-shrink: 0;
61
+ width: 280px;
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.65rem;
65
+ padding: 0.75rem 0.9rem 0.9rem;
66
+ background: var(--bg-elevated);
67
+ overflow-y: auto;
68
+ }
69
+ /* A long attribute list scrolls the panel, doesn't squeeze the code viewer. */
70
+ .inspector-panel > * {
71
+ flex-shrink: 0;
72
+ }
73
+ .inspector-panel :global(.code-viewer) {
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .inspector-header {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.4rem;
81
+ padding-bottom: 0.5rem;
82
+ margin-bottom: 0.1rem;
83
+ border-bottom: 1px solid var(--border);
84
+ font-family: 'JetBrains Mono', monospace;
85
+ font-size: 0.68rem;
86
+ font-weight: 600;
87
+ text-transform: uppercase;
88
+ letter-spacing: 0.06em;
89
+ color: var(--text-muted);
90
+ }
91
+
92
+ .inspector-empty {
93
+ color: var(--text-muted);
94
+ font-size: 0.8rem;
95
+ padding: 1rem 0.1rem;
96
+ line-height: 1.5;
97
+ }
98
+
99
+ .inspector-section {
100
+ display: flex;
101
+ align-items: baseline;
102
+ gap: 0.5rem;
103
+ }
104
+ .inspector-section-label {
105
+ font-size: 0.68rem;
106
+ font-weight: 600;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.05em;
109
+ color: var(--text-muted);
110
+ }
111
+ .inspector-size {
112
+ font-family: 'JetBrains Mono', monospace;
113
+ font-size: 0.75rem;
114
+ color: var(--text);
115
+ }
116
+
117
+ .inspector-attrs {
118
+ margin: 0;
119
+ display: flex;
120
+ flex-direction: column;
121
+ }
122
+ .inspector-attr-row {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 0.1rem;
126
+ padding: 0.35rem 0;
127
+ border-top: 1px solid var(--border);
128
+ }
129
+ .inspector-attr-row dt {
130
+ font-family: 'JetBrains Mono', monospace;
131
+ font-size: 0.7rem;
132
+ color: var(--accent);
133
+ }
134
+ .inspector-attr-row dd {
135
+ margin: 0;
136
+ font-family: 'JetBrains Mono', monospace;
137
+ font-size: 0.72rem;
138
+ color: var(--text-muted);
139
+ word-break: break-all;
140
+ }
141
+ </style>