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.
- package/backend/_scaffold/utils/browser.ts +184 -30
- package/backend/_scaffold/utils/hooks.ts +37 -8
- package/backend/app.js +0 -5
- package/backend/config/scripts/generate-report.js +2 -2
- package/backend/constants/socketEvents.js +7 -4
- 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/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
|
@@ -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>
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { createEventDispatcher } from 'svelte';
|
|
8
|
+
|
|
9
|
+
// Replaces rrweb-player's own timeline for multi-tab recordings — rrweb's
|
|
10
|
+
// timeline belongs to whichever player is currently mounted and resets on
|
|
11
|
+
// every segment hand-off, so it can't track absolute position across tabs.
|
|
12
|
+
export let from = 0;
|
|
13
|
+
export let to = 0;
|
|
14
|
+
export let position = 0;
|
|
15
|
+
export let stepTimestamps = [];
|
|
16
|
+
|
|
17
|
+
const dispatch = createEventDispatcher();
|
|
18
|
+
|
|
19
|
+
function percentOf(ts) {
|
|
20
|
+
return to > from ? Math.min(100, Math.max(0, ((ts - from) / (to - from)) * 100)) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fmtClock(ms) {
|
|
24
|
+
if (ms <= 0) return '00:00';
|
|
25
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
26
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
27
|
+
const seconds = totalSeconds % 60;
|
|
28
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleClick(e) {
|
|
32
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
33
|
+
const pct = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
|
|
34
|
+
dispatch('seek', from + pct * (to - from));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$: percent = percentOf(position);
|
|
38
|
+
// Mirrors rrweb-player's own per-step tick marks (its `customEvents` rendering).
|
|
39
|
+
$: tickPercents = stepTimestamps.map(percentOf);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="combined-timeline">
|
|
43
|
+
<span class="combined-timeline-time">{fmtClock(position - from)}</span>
|
|
44
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
45
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
46
|
+
<div class="combined-timeline-track" on:click={handleClick}>
|
|
47
|
+
<div class="combined-timeline-fill" style="width: {percent}%"></div>
|
|
48
|
+
{#each tickPercents as pct}
|
|
49
|
+
<div class="combined-timeline-tick" style="left: {pct}%"></div>
|
|
50
|
+
{/each}
|
|
51
|
+
<div class="combined-timeline-handle" style="left: {percent}%"></div>
|
|
52
|
+
</div>
|
|
53
|
+
<span class="combined-timeline-time">{fmtClock(to - from)}</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
/* Matches rrweb-player's own .rr-timeline/.rr-progress/.rr-progress__step/
|
|
58
|
+
.rr-progress__handler (dist/style.css) so this reads as the same control. */
|
|
59
|
+
.combined-timeline {
|
|
60
|
+
position: absolute;
|
|
61
|
+
left: 0;
|
|
62
|
+
right: 0;
|
|
63
|
+
bottom: 38px;
|
|
64
|
+
width: 80%;
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.combined-timeline-time {
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
display: inline-block;
|
|
73
|
+
width: 100px;
|
|
74
|
+
text-align: center;
|
|
75
|
+
color: #11103e;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.combined-timeline-track {
|
|
79
|
+
position: relative;
|
|
80
|
+
flex: 1;
|
|
81
|
+
height: 12px;
|
|
82
|
+
background: #eee;
|
|
83
|
+
border-radius: 3px;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
border-top: solid 4px #fff;
|
|
87
|
+
border-bottom: solid 4px #fff;
|
|
88
|
+
}
|
|
89
|
+
.combined-timeline-fill {
|
|
90
|
+
position: absolute;
|
|
91
|
+
left: 0;
|
|
92
|
+
top: 0;
|
|
93
|
+
height: 100%;
|
|
94
|
+
background: #e0e1fe;
|
|
95
|
+
}
|
|
96
|
+
.combined-timeline-tick {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 2px;
|
|
99
|
+
width: 10px;
|
|
100
|
+
height: 5px;
|
|
101
|
+
transform: translate(-50%, -50%);
|
|
102
|
+
background: rgb(73, 80, 246);
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
.combined-timeline-handle {
|
|
106
|
+
position: absolute;
|
|
107
|
+
top: 2px;
|
|
108
|
+
width: 20px;
|
|
109
|
+
height: 20px;
|
|
110
|
+
border-radius: 10px;
|
|
111
|
+
transform: translate(-50%, -50%);
|
|
112
|
+
background: rgb(73, 80, 246);
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
</style>
|