plum-e2e 2.8.6 → 2.9.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.
- package/README.md +20 -19
- package/backend/_scaffold/utils/browser.ts +7 -63
- package/backend/_scaffold/utils/hooks.ts +5 -20
- package/backend/app.js +0 -5
- package/backend/config/scripts/generate-report.js +2 -2
- package/backend/config/scripts/run-tests.js +5 -1
- package/backend/constants/socketEvents.js +7 -4
- package/backend/lib/plumTestRuntime.js +262 -0
- package/backend/lib/reportFilename.js +1 -2
- package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
- package/backend/lib/serverBootstrap.js +14 -0
- package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
- package/backend/mcp/server.js +3 -47
- package/backend/package-lock.json +199 -1
- package/backend/package.json +3 -1
- package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
- package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
- package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
- package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
- package/backend/prisma/schema.prisma +97 -70
- package/backend/routes/backup.routes.js +47 -1
- package/backend/routes/reports.routes.js +23 -0
- package/backend/server.js +1 -1
- package/backend/services/backupCronService.js +1 -1
- package/backend/services/backupService.js +134 -44
- package/backend/services/cronService.js +23 -15
- package/backend/services/nodeExecutionService.js +41 -15
- package/backend/services/nodeStreamRegistry.js +24 -0
- package/backend/services/reportService.js +167 -83
- package/backend/services/runnerService.js +19 -7
- package/backend/services/settingsService.js +6 -3
- package/backend/services/triggerService.js +20 -13
- package/backend/websockets/nodeSocketHandler.js +40 -0
- package/backend/websockets/socketHandler.js +9 -7
- package/bin/plum.js +58 -1
- package/frontend/.svelte-kit/ambient.d.ts +28 -28
- package/frontend/.svelte-kit/generated/server/internal.js +1 -1
- package/frontend/package-lock.json +121 -32
- package/frontend/package.json +1 -0
- package/frontend/src/lib/api/reports.js +13 -4
- package/frontend/src/lib/api/settings.js +16 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
- package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
- package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
- package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
- package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
- package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
- package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
- package/frontend/src/lib/constants.js +0 -1
- package/frontend/src/lib/copy/reports.js +18 -8
- package/frontend/src/lib/copy/settings.js +25 -4
- package/frontend/src/lib/socketEvents.js +7 -4
- package/frontend/src/lib/stores/runner.js +26 -3
- package/frontend/src/lib/styles/tokens.css +7 -0
- package/frontend/src/lib/utils/format.js +108 -2
- package/frontend/src/lib/utils/inspectElement.js +34 -0
- package/frontend/src/routes/reports/+page.svelte +79 -1
- package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
- package/frontend/src/routes/reports/live/+page.svelte +236 -260
- package/frontend/src/routes/settings/+page.svelte +246 -8
- package/package.json +1 -1
- package/backend/playwright.config.js +0 -85
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-frontend",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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",
|
package/frontend/package.json
CHANGED
|
@@ -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
|
-
|
|
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: ''
|
|
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.
|
|
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: ''
|
|
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.
|
|
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
|
-
|
|
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>
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { onMount, onDestroy } from 'svelte';
|
|
8
|
+
import Player from 'rrweb-player';
|
|
9
|
+
import 'rrweb-player/dist/style.css';
|
|
10
|
+
|
|
11
|
+
// A growing array — new events are appended by the caller as they stream
|
|
12
|
+
// in over the socket. liveMode lets rrweb-player start from zero events
|
|
13
|
+
// (it otherwise requires a complete Meta+FullSnapshot pair up front).
|
|
14
|
+
export let events = [];
|
|
15
|
+
|
|
16
|
+
const META_EVENT_TYPE = 4;
|
|
17
|
+
|
|
18
|
+
let stage;
|
|
19
|
+
let container;
|
|
20
|
+
let player = null;
|
|
21
|
+
let fedCount = 0;
|
|
22
|
+
let nativeWidth = 0;
|
|
23
|
+
let nativeHeight = 0;
|
|
24
|
+
let resizeObserver;
|
|
25
|
+
|
|
26
|
+
// rrweb-player renders at the recording's native resolution — scale-and-crop
|
|
27
|
+
// it to fill the stage (object-fit: cover) instead of letterboxing. Re-run on
|
|
28
|
+
// every stage resize, since the panel's own layout (tab strips appearing,
|
|
29
|
+
// window resize) can still shift its size after the player is built.
|
|
30
|
+
function updateScale() {
|
|
31
|
+
if (!nativeWidth || !nativeHeight) return;
|
|
32
|
+
const rect = stage.getBoundingClientRect();
|
|
33
|
+
const scale = Math.max(rect.width / nativeWidth, rect.height / nativeHeight);
|
|
34
|
+
container.style.transform = `scale(${scale})`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// getMetaData() (called internally by rrweb-player's own controls on
|
|
38
|
+
// construction) reads events[0].timestamp unconditionally, so the initial
|
|
39
|
+
// batch must be passed in props rather than fed empty and via addEvent.
|
|
40
|
+
function buildPlayer() {
|
|
41
|
+
const rect = stage.getBoundingClientRect();
|
|
42
|
+
const meta = events[0]?.type === META_EVENT_TYPE ? events[0].data : null;
|
|
43
|
+
nativeWidth = meta?.width > 0 ? meta.width : rect.width;
|
|
44
|
+
nativeHeight = meta?.height > 0 ? meta.height : rect.height;
|
|
45
|
+
|
|
46
|
+
player = new Player({
|
|
47
|
+
target: container,
|
|
48
|
+
props: {
|
|
49
|
+
events,
|
|
50
|
+
liveMode: true,
|
|
51
|
+
autoPlay: false,
|
|
52
|
+
showController: false,
|
|
53
|
+
width: nativeWidth,
|
|
54
|
+
height: nativeHeight
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
fedCount = events.length;
|
|
58
|
+
player.getReplayer()?.startLive();
|
|
59
|
+
updateScale();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Feed only what's newly arrived since the last run of this block —
|
|
63
|
+
// addEvent() is the incremental API, not a full-array replace.
|
|
64
|
+
$: if (player && events.length > fedCount) {
|
|
65
|
+
for (let i = fedCount; i < events.length; i++) player.addEvent(events[i]);
|
|
66
|
+
fedCount = events.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMount(() => {
|
|
70
|
+
buildPlayer();
|
|
71
|
+
resizeObserver = new ResizeObserver(updateScale);
|
|
72
|
+
resizeObserver.observe(stage);
|
|
73
|
+
});
|
|
74
|
+
onDestroy(() => {
|
|
75
|
+
resizeObserver?.disconnect();
|
|
76
|
+
try {
|
|
77
|
+
player?.getReplayer()?.destroy();
|
|
78
|
+
} catch {
|
|
79
|
+
// already torn down
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<div class="live-stage" bind:this={stage}>
|
|
85
|
+
<div class="live-mount" bind:this={container}></div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.live-stage {
|
|
90
|
+
position: relative;
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
background: var(--bg-subtle);
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.live-mount {
|
|
102
|
+
display: flex;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Sandboxed iframe (no allow-scripts/forms/top-navigation) — safe to always
|
|
106
|
+
allow pointer interaction so a taller live page can still be scrolled. */
|
|
107
|
+
.live-mount :global(iframe) {
|
|
108
|
+
pointer-events: auto !important;
|
|
109
|
+
}
|
|
110
|
+
</style>
|