plum-e2e 2.2.0 → 2.3.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.
@@ -25,7 +25,11 @@
25
25
  exportBackup,
26
26
  importBackup,
27
27
  fetchIntegrations,
28
- saveIntegrations
28
+ saveIntegrations,
29
+ fetchBackupConfig,
30
+ saveBackupConfig,
31
+ testBackupS3,
32
+ runBackupNow
29
33
  } from '$lib/api/settings';
30
34
  import {
31
35
  fetchRunners,
@@ -94,6 +98,25 @@
94
98
  let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
95
99
  let integrationsSaving = false;
96
100
 
101
+ let backupConfig = {
102
+ backupEnabled: false,
103
+ backupCron: '0 2 * * *',
104
+ backupS3Endpoint: '',
105
+ backupS3Region: '',
106
+ backupS3Bucket: '',
107
+ backupS3AccessKey: '',
108
+ backupS3SecretKey: '',
109
+ backupS3Prefix: ''
110
+ };
111
+ let backupConfigSaving = false;
112
+ let backupS3SecretKeySet = false;
113
+ let backupTestingS3 = false;
114
+ let backupRunningNow = false;
115
+ let backupS3TestResult = null;
116
+ let backupS3TestMessage = '';
117
+ let backupLastRunAt = null;
118
+ let backupLastStatus = '';
119
+
97
120
  let runners = [];
98
121
  let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
99
122
  let runnerFormError = '';
@@ -134,6 +157,22 @@
134
157
  try {
135
158
  integrations = await fetchIntegrations();
136
159
  } catch {}
160
+ try {
161
+ const bc = await fetchBackupConfig();
162
+ backupS3SecretKeySet = bc.backupS3SecretKeySet;
163
+ backupLastRunAt = bc.backupLastRunAt;
164
+ backupLastStatus = bc.backupLastStatus;
165
+ backupConfig = {
166
+ backupEnabled: bc.backupEnabled,
167
+ backupCron: bc.backupCron,
168
+ backupS3Endpoint: bc.backupS3Endpoint,
169
+ backupS3Region: bc.backupS3Region,
170
+ backupS3Bucket: bc.backupS3Bucket,
171
+ backupS3AccessKey: bc.backupS3AccessKey,
172
+ backupS3SecretKey: '',
173
+ backupS3Prefix: bc.backupS3Prefix
174
+ };
175
+ } catch {}
137
176
  if ($auth.user) {
138
177
  profileForm = { name: $auth.user.name, email: $auth.user.email };
139
178
  }
@@ -419,6 +458,60 @@
419
458
  }
420
459
  }
421
460
 
461
+ async function handleSaveBackupConfig() {
462
+ backupConfigSaving = true;
463
+ try {
464
+ const payload = { ...backupConfig };
465
+ if (!payload.backupS3SecretKey) delete payload.backupS3SecretKey;
466
+ const result = await saveBackupConfig(payload);
467
+ if (result.error) throw new Error(result.error);
468
+ if (backupConfig.backupS3SecretKey) backupS3SecretKeySet = true;
469
+ backupConfig = { ...backupConfig, backupS3SecretKey: '' };
470
+ showToast('success', 'Backup configuration saved.');
471
+ } catch (e) {
472
+ showToast('error', e.message || 'Failed to save backup configuration.');
473
+ } finally {
474
+ backupConfigSaving = false;
475
+ }
476
+ }
477
+
478
+ async function handleTestS3() {
479
+ backupTestingS3 = true;
480
+ backupS3TestResult = null;
481
+ backupS3TestMessage = '';
482
+ try {
483
+ const result = await testBackupS3(backupConfig);
484
+ if (result.error) throw new Error(result.error);
485
+ backupS3TestResult = 'success';
486
+ backupS3TestMessage = 'Connection successful.';
487
+ } catch (e) {
488
+ backupS3TestResult = 'error';
489
+ backupS3TestMessage = e.message || 'Connection failed.';
490
+ } finally {
491
+ backupTestingS3 = false;
492
+ }
493
+ }
494
+
495
+ async function handleRunBackupNow() {
496
+ backupRunningNow = true;
497
+ try {
498
+ const result = await runBackupNow();
499
+ if (result.error) throw new Error(result.error);
500
+ backupLastRunAt = result.lastRunAt;
501
+ backupLastStatus = result.lastStatus ?? '';
502
+ showToast('success', 'Backup uploaded to S3 successfully.');
503
+ } catch (e) {
504
+ showToast('error', e.message || 'Backup failed. Check your S3 configuration.');
505
+ const bc = await fetchBackupConfig().catch(() => null);
506
+ if (bc) {
507
+ backupLastRunAt = bc.backupLastRunAt;
508
+ backupLastStatus = bc.backupLastStatus;
509
+ }
510
+ } finally {
511
+ backupRunningNow = false;
512
+ }
513
+ }
514
+
422
515
  $: navItems = [
423
516
  { id: 'project', label: 'Project' },
424
517
  { id: 'runners', label: 'Runners' },
@@ -1108,21 +1201,23 @@
1108
1201
  <div class="content-header">
1109
1202
  <h2>Backup</h2>
1110
1203
  <p class="content-desc">
1111
- Export all scheduled tests, report history, and project settings to a JSON file. Import
1112
- to restore after a data loss or migration.
1204
+ Export your test cases, schedules, users, and project settings. Automate uploads to any
1205
+ S3-compatible storage Cloudflare R2, Backblaze B2, AWS S3, or MinIO.
1113
1206
  </p>
1114
1207
  </div>
1115
1208
 
1209
+ <!-- Manual export / import -->
1116
1210
  <div class="card settings-card">
1211
+ <p class="card-title">Manual Backup</p>
1117
1212
  <div class="backup-row">
1118
1213
  <div class="backup-block">
1119
1214
  <p class="backup-block-title">Export</p>
1120
1215
  <p class="backup-block-desc">
1121
- Downloads a <code>.json</code> file containing all cron jobs, report metadata, and project
1122
- settings. Report detail files are stored on disk and not included.
1216
+ Downloads a <code>.json</code> file with all cron jobs, test cases, test runs, users,
1217
+ runners, and project settings.
1123
1218
  </p>
1124
1219
  <Button on:click={handleExport} disabled={exporting}>
1125
- {exporting ? 'Exporting…' : 'Export Backup'}
1220
+ {exporting ? 'Exporting…' : 'Export JSON'}
1126
1221
  </Button>
1127
1222
  </div>
1128
1223
 
@@ -1131,8 +1226,8 @@
1131
1226
  <div class="backup-block">
1132
1227
  <p class="backup-block-title">Import</p>
1133
1228
  <p class="backup-block-desc">
1134
- Restores cron jobs, report metadata, and project settings from a previously exported
1135
- backup. Existing records with the same identifier are overwritten.
1229
+ Restores all data from a previously exported backup. Existing records are
1230
+ overwritten. Cron jobs are re-scheduled after import.
1136
1231
  </p>
1137
1232
  <div class="import-row">
1138
1233
  <label class="file-label">
@@ -1151,6 +1246,194 @@
1151
1246
  </div>
1152
1247
  </div>
1153
1248
  </div>
1249
+ <p class="backup-disclaimer">
1250
+ Reports are not included in backups. To back up report history, run
1251
+ <code>pg_dump</code> directly on the PostgreSQL volume.
1252
+ </p>
1253
+ </div>
1254
+
1255
+ <!-- S3 cloud backup -->
1256
+ <div class="card settings-card">
1257
+ <p class="card-title">S3 Storage</p>
1258
+ <p class="backup-block-desc" style="margin-bottom: 1.25rem;">
1259
+ Works with any S3-compatible provider — Cloudflare R2, Backblaze B2, AWS S3, or MinIO.
1260
+ Leave <strong>Endpoint URL</strong> empty for AWS S3.
1261
+ </p>
1262
+
1263
+ <div class="field-row">
1264
+ <div class="field">
1265
+ <label class="field-label" for="s3-endpoint">
1266
+ <span>Endpoint URL</span>
1267
+ <span class="field-hint">Leave blank for AWS S3</span>
1268
+ </label>
1269
+ <input
1270
+ id="s3-endpoint"
1271
+ type="url"
1272
+ class="field-input"
1273
+ bind:value={backupConfig.backupS3Endpoint}
1274
+ placeholder="https://account.r2.cloudflarestorage.com"
1275
+ />
1276
+ </div>
1277
+ <div class="field">
1278
+ <label class="field-label" for="s3-region">
1279
+ <span>Region</span>
1280
+ <span class="field-hint">Use <code>auto</code> for Cloudflare R2</span>
1281
+ </label>
1282
+ <input
1283
+ id="s3-region"
1284
+ type="text"
1285
+ class="field-input"
1286
+ bind:value={backupConfig.backupS3Region}
1287
+ placeholder="us-east-1"
1288
+ />
1289
+ </div>
1290
+ </div>
1291
+
1292
+ <div class="field-row">
1293
+ <div class="field">
1294
+ <label class="field-label" for="s3-bucket">
1295
+ <span>Bucket</span>
1296
+ </label>
1297
+ <input
1298
+ id="s3-bucket"
1299
+ type="text"
1300
+ class="field-input"
1301
+ bind:value={backupConfig.backupS3Bucket}
1302
+ placeholder="my-plum-backups"
1303
+ />
1304
+ </div>
1305
+ <div class="field">
1306
+ <label class="field-label" for="s3-prefix">
1307
+ <span>Path Prefix</span>
1308
+ <span class="field-hint">Optional folder inside the bucket</span>
1309
+ </label>
1310
+ <input
1311
+ id="s3-prefix"
1312
+ type="text"
1313
+ class="field-input"
1314
+ bind:value={backupConfig.backupS3Prefix}
1315
+ placeholder="plum/"
1316
+ />
1317
+ </div>
1318
+ </div>
1319
+
1320
+ <div class="field-row">
1321
+ <div class="field">
1322
+ <label class="field-label" for="s3-access-key">
1323
+ <span>Access Key ID</span>
1324
+ </label>
1325
+ <input
1326
+ id="s3-access-key"
1327
+ type="text"
1328
+ class="field-input"
1329
+ bind:value={backupConfig.backupS3AccessKey}
1330
+ placeholder="AKIAIOSFODNN7EXAMPLE"
1331
+ autocomplete="off"
1332
+ />
1333
+ </div>
1334
+ <div class="field">
1335
+ <label class="field-label" for="s3-secret-key">
1336
+ <span>Secret Access Key</span>
1337
+ <span class="field-hint"
1338
+ >{backupS3SecretKeySet
1339
+ ? 'A key is already saved — leave blank to keep it'
1340
+ : 'Required'}</span
1341
+ >
1342
+ </label>
1343
+ <input
1344
+ id="s3-secret-key"
1345
+ type="password"
1346
+ class="field-input"
1347
+ bind:value={backupConfig.backupS3SecretKey}
1348
+ placeholder={backupS3SecretKeySet ? '••••••••' : 'Enter secret key'}
1349
+ autocomplete="new-password"
1350
+ />
1351
+ </div>
1352
+ </div>
1353
+
1354
+ <div class="backup-actions">
1355
+ <Button variant="ghost" on:click={handleTestS3} disabled={backupTestingS3}>
1356
+ {backupTestingS3 ? 'Testing…' : 'Test Connection'}
1357
+ </Button>
1358
+ <Button on:click={handleSaveBackupConfig} disabled={backupConfigSaving}>
1359
+ {backupConfigSaving ? 'Saving…' : 'Save S3 Config'}
1360
+ </Button>
1361
+ </div>
1362
+
1363
+ {#if backupS3TestResult}
1364
+ <p
1365
+ class="s3-test-result"
1366
+ class:s3-test-success={backupS3TestResult === 'success'}
1367
+ class:s3-test-error={backupS3TestResult === 'error'}
1368
+ >
1369
+ {backupS3TestResult === 'success' ? '✓' : '✗'}
1370
+ {backupS3TestMessage}
1371
+ </p>
1372
+ {/if}
1373
+ </div>
1374
+
1375
+ <!-- Scheduled backup -->
1376
+ <div class="card settings-card">
1377
+ <p class="card-title">Scheduled Backup</p>
1378
+
1379
+ <div class="field">
1380
+ <label class="field-label backup-toggle-label" for="backup-enabled">
1381
+ <span>Enable scheduled backup</span>
1382
+ <button
1383
+ id="backup-enabled"
1384
+ class="toggle-btn"
1385
+ class:active={backupConfig.backupEnabled}
1386
+ on:click={() => (backupConfig.backupEnabled = !backupConfig.backupEnabled)}
1387
+ role="switch"
1388
+ aria-checked={backupConfig.backupEnabled}
1389
+ >
1390
+ <span class="toggle-thumb"></span>
1391
+ </button>
1392
+ </label>
1393
+ </div>
1394
+
1395
+ <div class="field">
1396
+ <label class="field-label" for="backup-cron">
1397
+ <span>Cron Expression</span>
1398
+ <span class="field-hint">
1399
+ 5-field cron — e.g. <code>0 2 * * *</code> = daily at 2 AM.
1400
+ <a href="https://crontab.guru" target="_blank" rel="noopener noreferrer"
1401
+ >Test at crontab.guru ↗</a
1402
+ >
1403
+ </span>
1404
+ </label>
1405
+ <input
1406
+ id="backup-cron"
1407
+ type="text"
1408
+ class="field-input field-input-mono"
1409
+ bind:value={backupConfig.backupCron}
1410
+ placeholder="0 2 * * *"
1411
+ />
1412
+ </div>
1413
+
1414
+ {#if backupLastRunAt}
1415
+ <p class="backup-last-run">
1416
+ Last backup: {new Date(backupLastRunAt).toLocaleString()} —
1417
+ {#if backupLastStatus?.startsWith('success:')}
1418
+ <span class="status-success"
1419
+ >uploaded to {backupLastStatus.replace('success:', '')}</span
1420
+ >
1421
+ {:else if backupLastStatus?.startsWith('error:')}
1422
+ <span class="status-error">{backupLastStatus.replace('error:', '')}</span>
1423
+ {:else}
1424
+ <span>{backupLastStatus}</span>
1425
+ {/if}
1426
+ </p>
1427
+ {/if}
1428
+
1429
+ <div class="backup-actions">
1430
+ <Button variant="ghost" on:click={handleRunBackupNow} disabled={backupRunningNow}>
1431
+ {backupRunningNow ? 'Uploading…' : 'Upload to S3 Now'}
1432
+ </Button>
1433
+ <Button on:click={handleSaveBackupConfig} disabled={backupConfigSaving}>
1434
+ {backupConfigSaving ? 'Saving…' : 'Save Schedule'}
1435
+ </Button>
1436
+ </div>
1154
1437
  </div>
1155
1438
  </div>
1156
1439
  {/if}
@@ -1520,6 +1803,96 @@
1520
1803
  border-radius: 3px;
1521
1804
  }
1522
1805
 
1806
+ .backup-disclaimer {
1807
+ margin-top: 1rem;
1808
+ padding: 0.625rem 0.875rem;
1809
+ background: var(--bg-subtle);
1810
+ border: 1px solid var(--border);
1811
+ border-radius: var(--radius-sm);
1812
+ font-size: 0.8125rem;
1813
+ color: var(--text-muted);
1814
+ line-height: 1.5;
1815
+ }
1816
+
1817
+ .backup-actions {
1818
+ display: flex;
1819
+ gap: 0.625rem;
1820
+ margin-top: 0.25rem;
1821
+ flex-wrap: wrap;
1822
+ }
1823
+
1824
+ .s3-test-result {
1825
+ margin-top: 0.625rem;
1826
+ font-size: 0.8125rem;
1827
+ font-weight: 500;
1828
+ }
1829
+
1830
+ .s3-test-success {
1831
+ color: var(--pass);
1832
+ }
1833
+
1834
+ .s3-test-error {
1835
+ color: var(--fail);
1836
+ }
1837
+
1838
+ .backup-last-run {
1839
+ font-size: 0.8125rem;
1840
+ color: var(--text-muted);
1841
+ margin-top: 0.25rem;
1842
+ }
1843
+
1844
+ .status-success {
1845
+ color: var(--pass);
1846
+ }
1847
+
1848
+ .status-error {
1849
+ color: var(--fail);
1850
+ }
1851
+
1852
+ .backup-toggle-label {
1853
+ display: flex;
1854
+ align-items: center;
1855
+ justify-content: space-between;
1856
+ cursor: default;
1857
+ }
1858
+
1859
+ .toggle-btn {
1860
+ flex-shrink: 0;
1861
+ width: 40px;
1862
+ height: 22px;
1863
+ border-radius: 100px;
1864
+ border: none;
1865
+ background: var(--border);
1866
+ cursor: pointer;
1867
+ position: relative;
1868
+ transition: background 0.2s var(--ease-out);
1869
+ }
1870
+
1871
+ .toggle-btn.active {
1872
+ background: var(--accent);
1873
+ }
1874
+
1875
+ .toggle-btn .toggle-thumb {
1876
+ position: absolute;
1877
+ top: 3px;
1878
+ left: 3px;
1879
+ width: 16px;
1880
+ height: 16px;
1881
+ border-radius: 50%;
1882
+ background: white;
1883
+ transition: transform 0.2s var(--ease-out);
1884
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
1885
+ }
1886
+
1887
+ .toggle-btn.active .toggle-thumb {
1888
+ transform: translateX(18px);
1889
+ }
1890
+
1891
+ .field-input-mono {
1892
+ font-family: 'JetBrains Mono', monospace;
1893
+ font-size: 0.8125rem;
1894
+ }
1895
+
1523
1896
  .import-row {
1524
1897
  display: flex;
1525
1898
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"