plum-e2e 1.1.1 → 1.2.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 (42) hide show
  1. package/.claude/settings.local.json +27 -25
  2. package/.husky/pre-commit +2 -2
  3. package/README.md +142 -70
  4. package/backend/Dockerfile +4 -2
  5. package/backend/app.js +4 -2
  6. package/backend/config/scripts/generate-report.js +38 -30
  7. package/backend/entrypoint.sh +22 -0
  8. package/backend/package-lock.json +453 -10
  9. package/backend/package.json +5 -2
  10. package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
  11. package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
  12. package/backend/prisma/migrations/migration_lock.toml +3 -0
  13. package/backend/prisma/schema.prisma +53 -0
  14. package/backend/routes/backup.routes.js +50 -0
  15. package/backend/routes/cron.routes.js +9 -60
  16. package/backend/routes/reports.routes.js +39 -6
  17. package/backend/routes/settings.routes.js +43 -0
  18. package/backend/server.js +52 -1
  19. package/backend/services/backupService.js +88 -0
  20. package/backend/services/cronService.js +68 -78
  21. package/backend/services/{scheduleService.js → prisma.js} +3 -15
  22. package/backend/services/reportService.js +48 -20
  23. package/backend/{routes/schedules.routes.js → services/settingsService.js} +17 -13
  24. package/bin/plum.js +213 -32
  25. package/docker-compose.yml +24 -0
  26. package/frontend/package-lock.json +2 -2
  27. package/frontend/package.json +1 -1
  28. package/frontend/src/lib/api/reports.js +38 -27
  29. package/frontend/src/lib/api/schedules.js +9 -25
  30. package/frontend/src/lib/api/settings.js +48 -0
  31. package/frontend/src/lib/components/layout/Nav.svelte +2 -1
  32. package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
  33. package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
  34. package/frontend/src/lib/stores/runner.js +9 -0
  35. package/frontend/src/routes/+page.svelte +10 -3
  36. package/frontend/src/routes/reports/+page.svelte +342 -51
  37. package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
  38. package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
  39. package/frontend/src/routes/settings/+page.svelte +410 -0
  40. package/license-config.json +2 -2
  41. package/package.json +6 -2
  42. package/backend/config/scripts/create-settings.js +0 -53
@@ -0,0 +1,410 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <script>
19
+ import { onMount } from 'svelte';
20
+ import { fly } from 'svelte/transition';
21
+ import { fetchProject, saveProject, exportBackup, importBackup } from '$lib/api/settings';
22
+ import Button from '$lib/components/ui/Button.svelte';
23
+
24
+ let project = { name: '', logoUrl: '' };
25
+ let projectSaving = false;
26
+ let toast = null;
27
+
28
+ let importFile = null;
29
+ let importing = false;
30
+ let exporting = false;
31
+ let fileInput;
32
+
33
+ function showToast(type, message) {
34
+ toast = { type, message };
35
+ setTimeout(() => (toast = null), 4000);
36
+ }
37
+
38
+ onMount(async () => {
39
+ try {
40
+ project = await fetchProject();
41
+ } catch {}
42
+ });
43
+
44
+ async function handleSaveProject() {
45
+ projectSaving = true;
46
+ try {
47
+ await saveProject(project);
48
+ showToast('success', 'Project settings saved.');
49
+ } catch {
50
+ showToast('error', 'Failed to save project settings.');
51
+ } finally {
52
+ projectSaving = false;
53
+ }
54
+ }
55
+
56
+ async function handleExport() {
57
+ exporting = true;
58
+ try {
59
+ const data = await exportBackup();
60
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
61
+ const url = URL.createObjectURL(blob);
62
+ const a = document.createElement('a');
63
+ a.href = url;
64
+ a.download = `plum-backup-${new Date().toISOString().slice(0, 10)}.json`;
65
+ a.click();
66
+ URL.revokeObjectURL(url);
67
+ showToast('success', 'Backup downloaded.');
68
+ } catch {
69
+ showToast('error', 'Export failed.');
70
+ } finally {
71
+ exporting = false;
72
+ }
73
+ }
74
+
75
+ async function handleImport() {
76
+ if (!importFile) return;
77
+ importing = true;
78
+ try {
79
+ const text = await importFile.text();
80
+ const data = JSON.parse(text);
81
+ const result = await importBackup(data);
82
+ if (result.error) throw new Error(result.error);
83
+ showToast('success', 'Import successful. Cron jobs have been re-scheduled.');
84
+ // Refresh project settings in case they were part of the backup
85
+ project = await fetchProject();
86
+ importFile = null;
87
+ if (fileInput) fileInput.value = '';
88
+ } catch (e) {
89
+ showToast('error', e.message || 'Import failed. Check the file format.');
90
+ } finally {
91
+ importing = false;
92
+ }
93
+ }
94
+
95
+ function handleFileChange(e) {
96
+ importFile = e.target.files[0] ?? null;
97
+ }
98
+ </script>
99
+
100
+ <svelte:head><title>Settings — Plum</title></svelte:head>
101
+
102
+ {#if toast}
103
+ <div class="toast alert alert-{toast.type}" transition:fly={{ y: -8, duration: 240 }}>
104
+ {toast.message}
105
+ </div>
106
+ {/if}
107
+
108
+ <div class="page-header">
109
+ <h1>Settings</h1>
110
+ <p class="subtitle">Configure your project and manage data backups</p>
111
+ </div>
112
+
113
+ <!-- Project section -->
114
+ <section class="settings-section">
115
+ <div class="section-header">
116
+ <h2 class="section-title">Project</h2>
117
+ <p class="section-desc">Identity information shown across the UI</p>
118
+ </div>
119
+
120
+ <div class="card settings-card">
121
+ <div class="field">
122
+ <label class="field-label" for="project-name">Project Name</label>
123
+ <input
124
+ id="project-name"
125
+ type="text"
126
+ class="field-input"
127
+ bind:value={project.name}
128
+ placeholder="My Test Suite"
129
+ />
130
+ </div>
131
+
132
+ <div class="field">
133
+ <label class="field-label" for="project-logo">
134
+ <span>Logo URL</span>
135
+ <span class="field-hint">Direct link to an image (PNG, SVG, JPG)</span>
136
+ </label>
137
+ <input
138
+ id="project-logo"
139
+ type="url"
140
+ class="field-input"
141
+ bind:value={project.logoUrl}
142
+ placeholder="https://example.com/logo.png"
143
+ />
144
+ </div>
145
+
146
+ {#if project.logoUrl}
147
+ <div class="logo-preview">
148
+ <span class="preview-label">Preview</span>
149
+ <img
150
+ src={project.logoUrl}
151
+ alt="Project logo preview"
152
+ class="logo-img"
153
+ on:error={(e) => (e.target.style.display = 'none')}
154
+ />
155
+ </div>
156
+ {/if}
157
+
158
+ <div class="card-footer">
159
+ <Button on:click={handleSaveProject} disabled={projectSaving}>
160
+ {projectSaving ? 'Saving…' : 'Save Project'}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+ </section>
165
+
166
+ <!-- Backup section -->
167
+ <section class="settings-section">
168
+ <div class="section-header">
169
+ <h2 class="section-title">Backup</h2>
170
+ <p class="section-desc">
171
+ Export all scheduled tests, report history, and project settings to a JSON file. Import to
172
+ restore after a data loss or migration.
173
+ </p>
174
+ </div>
175
+
176
+ <div class="card settings-card">
177
+ <div class="backup-row">
178
+ <div class="backup-block">
179
+ <p class="backup-block-title">Export</p>
180
+ <p class="backup-block-desc">
181
+ Downloads a <code>.json</code> file containing all cron jobs, report metadata, and project
182
+ settings. Report detail files (the actual test output) are stored on disk and not included.
183
+ </p>
184
+ <Button on:click={handleExport} disabled={exporting}>
185
+ {exporting ? 'Exporting…' : 'Export Backup'}
186
+ </Button>
187
+ </div>
188
+
189
+ <div class="backup-divider"></div>
190
+
191
+ <div class="backup-block">
192
+ <p class="backup-block-title">Import</p>
193
+ <p class="backup-block-desc">
194
+ Restores cron jobs, report metadata, and project settings from a previously exported
195
+ backup. Existing records with the same identifier are overwritten.
196
+ </p>
197
+ <div class="import-row">
198
+ <label class="file-label">
199
+ <input
200
+ bind:this={fileInput}
201
+ type="file"
202
+ accept=".json"
203
+ class="file-input-hidden"
204
+ on:change={handleFileChange}
205
+ />
206
+ <span class="file-btn">{importFile ? importFile.name : 'Choose file…'}</span>
207
+ </label>
208
+ <Button on:click={handleImport} disabled={!importFile || importing}>
209
+ {importing ? 'Importing…' : 'Import'}
210
+ </Button>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </section>
216
+
217
+ <style>
218
+ .page-header {
219
+ margin-bottom: 2rem;
220
+ padding-bottom: 1.5rem;
221
+ border-bottom: 1px solid var(--border);
222
+ }
223
+
224
+ .page-header h1 {
225
+ font-size: 2.5rem;
226
+ margin-bottom: 0.375rem;
227
+ }
228
+
229
+ .subtitle {
230
+ color: var(--text-muted);
231
+ font-size: 0.9375rem;
232
+ }
233
+
234
+ .toast {
235
+ margin-bottom: 1.25rem;
236
+ border-radius: var(--radius-md);
237
+ }
238
+
239
+ /* Sections */
240
+ .settings-section {
241
+ margin-bottom: 2.5rem;
242
+ }
243
+
244
+ .section-header {
245
+ margin-bottom: 1rem;
246
+ }
247
+
248
+ .section-title {
249
+ font-size: 1rem;
250
+ font-weight: 500;
251
+ color: var(--text);
252
+ margin-bottom: 0.25rem;
253
+ }
254
+
255
+ .section-desc {
256
+ font-size: 0.875rem;
257
+ color: var(--text-muted);
258
+ line-height: 1.5;
259
+ }
260
+
261
+ .settings-card {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 1.25rem;
265
+ }
266
+
267
+ /* Fields */
268
+ .field {
269
+ display: flex;
270
+ flex-direction: column;
271
+ gap: 0.375rem;
272
+ }
273
+
274
+ .field-label {
275
+ display: flex;
276
+ align-items: baseline;
277
+ gap: 0.5rem;
278
+ font-size: 0.8125rem;
279
+ font-weight: 500;
280
+ color: var(--text);
281
+ }
282
+
283
+ .field-hint {
284
+ font-size: 0.75rem;
285
+ font-weight: 400;
286
+ color: var(--text-muted);
287
+ }
288
+
289
+ /* Logo preview */
290
+ .logo-preview {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 0.5rem;
294
+ }
295
+
296
+ .preview-label {
297
+ font-size: 0.75rem;
298
+ color: var(--text-muted);
299
+ font-weight: 500;
300
+ text-transform: uppercase;
301
+ letter-spacing: 0.06em;
302
+ }
303
+
304
+ .logo-img {
305
+ max-height: 56px;
306
+ max-width: 200px;
307
+ object-fit: contain;
308
+ border-radius: var(--radius-sm);
309
+ }
310
+
311
+ .card-footer {
312
+ padding-top: 0.25rem;
313
+ border-top: 1px solid var(--border);
314
+ }
315
+
316
+ /* Backup layout */
317
+ .backup-row {
318
+ display: flex;
319
+ gap: 2rem;
320
+ }
321
+
322
+ .backup-block {
323
+ flex: 1;
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: 0.875rem;
327
+ }
328
+
329
+ .backup-divider {
330
+ width: 1px;
331
+ background: var(--border);
332
+ flex-shrink: 0;
333
+ }
334
+
335
+ .backup-block-title {
336
+ font-size: 0.875rem;
337
+ font-weight: 500;
338
+ color: var(--text);
339
+ }
340
+
341
+ .backup-block-desc {
342
+ font-size: 0.8125rem;
343
+ color: var(--text-muted);
344
+ line-height: 1.6;
345
+ }
346
+
347
+ .backup-block-desc code {
348
+ font-family: 'JetBrains Mono', monospace;
349
+ font-size: 0.78rem;
350
+ background: var(--bg-subtle);
351
+ padding: 0.1em 0.3em;
352
+ border-radius: 3px;
353
+ }
354
+
355
+ /* File input */
356
+ .import-row {
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 0.625rem;
360
+ }
361
+
362
+ .file-label {
363
+ cursor: pointer;
364
+ }
365
+
366
+ .file-input-hidden {
367
+ position: absolute;
368
+ width: 1px;
369
+ height: 1px;
370
+ opacity: 0;
371
+ pointer-events: none;
372
+ }
373
+
374
+ .file-btn {
375
+ display: inline-flex;
376
+ align-items: center;
377
+ height: 34px;
378
+ padding: 0 0.875rem;
379
+ font-size: 0.8125rem;
380
+ font-family: inherit;
381
+ color: var(--text-muted);
382
+ background: var(--bg-elevated);
383
+ border: 1px solid var(--border);
384
+ border-radius: var(--radius-sm);
385
+ cursor: pointer;
386
+ white-space: nowrap;
387
+ max-width: 180px;
388
+ overflow: hidden;
389
+ text-overflow: ellipsis;
390
+ transition:
391
+ background var(--duration-fast),
392
+ border-color var(--duration-fast);
393
+ }
394
+
395
+ .file-label:hover .file-btn {
396
+ border-color: var(--text-muted);
397
+ background: var(--bg-subtle);
398
+ }
399
+
400
+ @media (max-width: 640px) {
401
+ .backup-row {
402
+ flex-direction: column;
403
+ }
404
+
405
+ .backup-divider {
406
+ width: 100%;
407
+ height: 1px;
408
+ }
409
+ }
410
+ </style>
@@ -31,7 +31,7 @@
31
31
  "**/.prettierrc",
32
32
  "**/.husky",
33
33
  "**/*.md",
34
- "**/backend/config/settings.json",
35
- "**/backend/config/cronjobs.json"
34
+ "**/prisma/migrations/**",
35
+ "**/prisma/schema.prisma"
36
36
  ]
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"
@@ -24,11 +24,15 @@
24
24
  "devDependencies": {
25
25
  "husky": "^9.1.7",
26
26
  "license-check-and-add": "^4.0.5",
27
+ "lint-staged": "^17.0.7",
27
28
  "prettier": "^3.5.1",
28
29
  "prettier-plugin-svelte": "^3.3.3"
29
30
  },
30
31
  "dependencies": {
31
32
  "fs-extra": "^11.3.0"
32
33
  },
33
- "type": "module"
34
+ "type": "module",
35
+ "lint-staged": {
36
+ "*.{js,ts,svelte,json,md,css,html}": "prettier --write"
37
+ }
34
38
  }
@@ -1,53 +0,0 @@
1
- /*
2
- * This file is part of Plum.
3
- *
4
- * Plum is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU General Public License as published by
6
- * the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * Plum is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU General Public License
15
- * along with Plum. If not, see https://www.gnu.org/licenses/.
16
- */
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const fse = require('fs-extra');
21
- const pc = require('picocolors');
22
-
23
- const settingsFilePath = path.join(process.cwd(), 'config', 'settings.json');
24
- const scaffoldFolderPath = path.join(process.cwd(), '_scaffold');
25
- const userTestsFolderPath = path.join(process.cwd(), 'tests');
26
-
27
- if (!fs.existsSync(settingsFilePath)) {
28
- const settingsContent = JSON.stringify(
29
- {
30
- reportsHistory: 20,
31
- cronJobSchedules: [
32
- { label: 'Every minute', value: '* * * * *' },
33
- { label: 'Every hour', value: '0 * * * *' },
34
- { label: 'Every midnight', value: '0 0 * * *' },
35
- { label: 'Every Sunday', value: '0 0 * * 0' }
36
- ]
37
- },
38
- null,
39
- 2
40
- );
41
- fs.writeFileSync(settingsFilePath, settingsContent, 'utf8');
42
- console.log(pc.green('✓') + ' settings.json created with default values.');
43
- } else {
44
- console.log(pc.yellow('⚠') + ' settings.json already exists. Skipping creation.');
45
- }
46
-
47
- if (!fs.existsSync(userTestsFolderPath)) {
48
- console.log(pc.cyan('↳') + ' tests/ not found. Creating from scaffold...');
49
- fse.copySync(scaffoldFolderPath, userTestsFolderPath);
50
- console.log(pc.green('✓') + ' tests/ created from scaffold.');
51
- } else {
52
- console.log(pc.yellow('⚠') + ' tests/ already exists. Skipping creation.');
53
- }