plum-e2e 2.5.11 → 2.5.13

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.
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE "Project" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';
@@ -82,6 +82,7 @@ model Project {
82
82
  id Int @id @default(autoincrement())
83
83
  name String @default("")
84
84
  logoUrl String @default("")
85
+ timezone String @default("UTC")
85
86
  testCasePrefix String @default("TC")
86
87
  testSuitePrefix String @default("TS")
87
88
  caseSeqNext Int @default(0)
@@ -33,8 +33,8 @@ router.get('/project', async (req, res, next) => {
33
33
 
34
34
  router.post('/project', async (req, res, next) => {
35
35
  try {
36
- const { name, logoUrl } = req.body;
37
- const project = await settingsService.updateProject({ name, logoUrl });
36
+ const { name, logoUrl, timezone } = req.body;
37
+ const project = await settingsService.updateProject({ name, logoUrl, timezone });
38
38
  res.json(project);
39
39
  } catch (e) {
40
40
  next(e);
@@ -58,20 +58,20 @@ const runBackup = async () => {
58
58
  }
59
59
  };
60
60
 
61
- const schedule = (cronExpr, enabled) => {
61
+ const schedule = (cronExpr, enabled, timezone) => {
62
62
  if (scheduledJob) {
63
63
  scheduledJob.stop();
64
64
  scheduledJob = null;
65
65
  }
66
66
  if (!enabled || !cronExpr || !cron.validate(cronExpr)) return;
67
- scheduledJob = cron.schedule(cronExpr, runBackup);
68
- console.log(`⏰ Backup scheduled: ${cronExpr}`);
67
+ scheduledJob = cron.schedule(cronExpr, runBackup, { timezone: timezone || 'UTC' });
68
+ console.log(`⏰ Backup scheduled: ${cronExpr} (${timezone || 'UTC'})`);
69
69
  };
70
70
 
71
71
  const init = async () => {
72
72
  try {
73
73
  const project = await prisma.project.findUnique({ where: { id: 1 } });
74
- schedule(project?.backupCron, project?.backupEnabled);
74
+ schedule(project?.backupCron, project?.backupEnabled, project?.timezone);
75
75
  } catch (err) {
76
76
  console.error('Failed to initialize backup cron:', err.message);
77
77
  }
@@ -52,6 +52,7 @@ const exportAll = async () => {
52
52
  ? {
53
53
  name: project.name,
54
54
  logoUrl: project.logoUrl,
55
+ timezone: project.timezone,
55
56
  testCasePrefix: project.testCasePrefix,
56
57
  testSuitePrefix: project.testSuitePrefix,
57
58
  discordWebhookUrl: project.discordWebhookUrl,
@@ -244,19 +244,22 @@ async function runCronJob(job) {
244
244
  // Public: scheduling
245
245
  // ---------------------------------------------------------------------------
246
246
 
247
- function scheduleJob(job) {
247
+ async function scheduleJob(job) {
248
248
  const { taskName, cronExpression } = job;
249
249
  if (scheduledJobs[taskName]) {
250
250
  scheduledJobs[taskName].stop();
251
251
  delete scheduledJobs[taskName];
252
252
  }
253
253
  if (job.enabled === false) return; // disabled jobs are not scheduled
254
- scheduledJobs[taskName] = cron.schedule(cronExpression, () => runCronJob(job));
254
+ const project = await prisma.project.findUnique({ where: { id: 1 } });
255
+ scheduledJobs[taskName] = cron.schedule(cronExpression, () => runCronJob(job), {
256
+ timezone: project?.timezone || 'UTC'
257
+ });
255
258
  }
256
259
 
257
260
  const init = async () => {
258
261
  const jobs = await prisma.cronJob.findMany();
259
- for (const job of jobs) scheduleJob(job);
262
+ for (const job of jobs) await scheduleJob(job);
260
263
  console.log(`⏰ Scheduled ${jobs.length} cron job(s) from database`);
261
264
  };
262
265
 
@@ -303,7 +306,7 @@ const addCronJob = async ({
303
306
  runnerId: null
304
307
  }
305
308
  });
306
- scheduleJob(job);
309
+ await scheduleJob(job);
307
310
  return { status: 201, message: `Cron job "${taskName}" added` };
308
311
  };
309
312
 
@@ -359,7 +362,7 @@ const updateCronJob = async (
359
362
  }
360
363
  });
361
364
 
362
- scheduleJob(updated);
365
+ await scheduleJob(updated);
363
366
  return { status: 200, message: 'Cron job updated' };
364
367
  };
365
368
 
@@ -379,7 +382,7 @@ const toggleCronJob = async (taskName, enabled) => {
379
382
  data: { enabled }
380
383
  });
381
384
 
382
- scheduleJob(updated); // re-schedules if enabled, stops and removes if disabled
385
+ await scheduleJob(updated); // re-schedules if enabled, stops and removes if disabled
383
386
  return { status: 200, enabled: updated.enabled };
384
387
  };
385
388
 
@@ -26,12 +26,26 @@ const getProject = async () => {
26
26
  return project;
27
27
  };
28
28
 
29
- const updateProject = async ({ name, logoUrl }) => {
30
- return prisma.project.upsert({
29
+ const updateProject = async ({ name, logoUrl, timezone }) => {
30
+ const data = {
31
+ name: name ?? '',
32
+ logoUrl: logoUrl ?? '',
33
+ ...(timezone !== undefined && { timezone })
34
+ };
35
+ const project = await prisma.project.upsert({
31
36
  where: { id: 1 },
32
- create: { id: 1, name: name ?? '', logoUrl: logoUrl ?? '' },
33
- update: { name: name ?? '', logoUrl: logoUrl ?? '' }
37
+ create: { id: 1, ...data },
38
+ update: data
34
39
  });
40
+
41
+ if (timezone !== undefined) {
42
+ // Cron jobs read the timezone at schedule time, so a change here must
43
+ // re-schedule everything for the new offset to take effect immediately.
44
+ await require('./cronService').reload();
45
+ await require('./backupCronService').reload();
46
+ }
47
+
48
+ return project;
35
49
  };
36
50
 
37
51
  const getTestPrefixes = async () => {
package/bin/plum.js CHANGED
@@ -513,7 +513,6 @@ async function serverUpdate() {
513
513
  // `plum update` happens to be run from.
514
514
  const { getInstalls } = globalRegistryLib();
515
515
  const { loadNodeConfig } = nodeRegisterLib();
516
- const { loadRegistry, isAlive } = runnerProcessLib();
517
516
 
518
517
  let restartedAnything = false;
519
518
 
@@ -533,15 +532,16 @@ async function serverUpdate() {
533
532
  }
534
533
  }
535
534
 
536
- const registry = loadRegistry();
537
535
  for (const dir of getInstalls('node')) {
538
536
  const nodeCfg = loadNodeConfig(dir);
539
- const running = !!(
540
- nodeCfg.id &&
541
- registry[String(nodeCfg.id)]?.pid &&
542
- isAlive(registry[String(nodeCfg.id)].pid)
543
- );
544
- if (!running) continue;
537
+ if (!nodeCfg.id) continue;
538
+ // Always attempt the restart rather than gating on the local PID
539
+ // registry: that registry goes stale (manager restarts, pre-existing
540
+ // installs from before this tracking existed, etc.), and skipping the
541
+ // restart in those cases silently leaves the OLD node process running
542
+ // mismatched code — it still answers /api/ping so it looks "online"
543
+ // while actually being unreachable. `plum node restart` itself falls
544
+ // back to port-based PID discovery, so it's safe to call unconditionally.
545
545
  clack.log.step(`Restarting node runner at ${dir}…`);
546
546
  try {
547
547
  execSync('plum node restart', { stdio: 'inherit', cwd: dir });
@@ -757,7 +757,11 @@ async function nodeRestart() {
757
757
  return;
758
758
  }
759
759
 
760
- const stopped = stopNode(String(cfg.id));
760
+ // Passing the port lets stopNode fall back to port-based PID discovery when
761
+ // the local registry entry is missing or stale (e.g. after `plum update`
762
+ // reinstalls in a fresh process) — otherwise a still-running old process is
763
+ // left bound to the port while a new one starts up alongside it.
764
+ const stopped = stopNode(String(cfg.id), Number(cfg.port));
761
765
  if (stopped) {
762
766
  clack.log.success(`Stopped runner "${cfg.name ?? cfg.id}".`);
763
767
  } else {
@@ -1179,7 +1183,7 @@ switch (command) {
1179
1183
  const cfg = loadNodeConfig(process.cwd());
1180
1184
 
1181
1185
  if (cfg.id) {
1182
- const stopped = stopNode(String(cfg.id));
1186
+ const stopped = stopNode(String(cfg.id), Number(cfg.port));
1183
1187
  if (stopped) {
1184
1188
  clack.log.success(`Stopped runner "${cfg.name ?? cfg.id}".`);
1185
1189
  } else if (cfg.pid) {
@@ -24,15 +24,15 @@ function authHeaders() {
24
24
 
25
25
  export async function fetchProject() {
26
26
  const res = await fetch(`${API_BASE}/settings/project`);
27
- if (!res.ok) return { name: '', logoUrl: '' };
27
+ if (!res.ok) return { name: '', logoUrl: '', timezone: 'UTC' };
28
28
  return res.json();
29
29
  }
30
30
 
31
- export async function saveProject({ name, logoUrl }) {
31
+ export async function saveProject({ name, logoUrl, timezone }) {
32
32
  const res = await fetch(`${API_BASE}/settings/project`, {
33
33
  method: 'POST',
34
34
  headers: { 'Content-Type': 'application/json' },
35
- body: JSON.stringify({ name, logoUrl })
35
+ body: JSON.stringify({ name, logoUrl, timezone })
36
36
  });
37
37
  return res.json();
38
38
  }
@@ -42,6 +42,7 @@ export const WORKERS_MIN = 1;
42
42
  export const WORKERS_MAX = 10;
43
43
  export const RUN_PICKER_LIMIT = 200;
44
44
  export const RUN_TAG_DISPLAY_LIMIT = 5;
45
+ export const CASE_HISTORY_BARS_MAX = 20;
45
46
 
46
47
  export const BUILTIN_RUNNER_ID = 'built-in';
47
48
  export const BUILTIN_RUNNER_LABEL = 'Built-in';
@@ -68,10 +68,18 @@
68
68
  } catch {}
69
69
  }
70
70
 
71
- let project = { name: '', logoUrl: '' };
71
+ let project = { name: '', logoUrl: '', timezone: 'UTC' };
72
72
  let projectSaving = false;
73
73
  let toast = null;
74
74
 
75
+ const TIMEZONES = (() => {
76
+ try {
77
+ return Intl.supportedValuesOf('timeZone');
78
+ } catch {
79
+ return ['UTC'];
80
+ }
81
+ })();
82
+
75
83
  let prefixes = { testCasePrefix: 'TC', testSuitePrefix: 'TS' };
76
84
  let prefixesSaving = false;
77
85
  let migrateForm = { testCasePrefix: '', testSuitePrefix: '' };
@@ -679,6 +687,18 @@
679
687
  </div>
680
688
  {/if}
681
689
 
690
+ <div class="field">
691
+ <label class="field-label" for="project-timezone">
692
+ <span>Timezone</span>
693
+ <span class="field-hint">Used to schedule cron jobs and backups</span>
694
+ </label>
695
+ <select id="project-timezone" class="field-input" bind:value={project.timezone}>
696
+ {#each TIMEZONES as tz}
697
+ <option value={tz}>{tz}</option>
698
+ {/each}
699
+ </select>
700
+ </div>
701
+
682
702
  <!-- Dark mode toggle -->
683
703
  <div class="toggle-row">
684
704
  <div class="toggle-info">
@@ -34,7 +34,7 @@
34
34
  import Toast from '$lib/components/ui/Toast.svelte';
35
35
  import Button from '$lib/components/ui/Button.svelte';
36
36
  import Pagination from '$lib/components/ui/Pagination.svelte';
37
- import { TOAST_TIMEOUT_MS, SUITE_CASES_PER_PAGE } from '$lib/constants';
37
+ import { TOAST_TIMEOUT_MS, SUITE_CASES_PER_PAGE, CASE_HISTORY_BARS_MAX } from '$lib/constants';
38
38
 
39
39
  const suiteId = $page.params.id;
40
40
 
@@ -257,6 +257,12 @@
257
257
  if (r === 'blocked') return 'warn';
258
258
  return 'muted';
259
259
  }
260
+
261
+ // selectedCase.history arrives newest-first; bars read oldest → newest, left → right.
262
+ function recentHistory(history) {
263
+ if (!history || history.length === 0) return [];
264
+ return history.slice(0, CASE_HISTORY_BARS_MAX).slice().reverse();
265
+ }
260
266
  </script>
261
267
 
262
268
  <svelte:head
@@ -623,6 +629,16 @@
623
629
  <p class="detail-case-desc">{selectedCase.description}</p>
624
630
  {/if}
625
631
  <p class="detail-meta">Created by {selectedCase.createdBy.name}</p>
632
+ {#if selectedCase.history && selectedCase.history.length > 0}
633
+ <div class="case-history-bars">
634
+ {#each recentHistory(selectedCase.history) as h (h.id)}
635
+ <span
636
+ class="history-bar {resultClass(h.result)}"
637
+ title="{h.result} — {new Date(h.executedAt).toLocaleString()}"
638
+ ></span>
639
+ {/each}
640
+ </div>
641
+ {/if}
626
642
  {/if}
627
643
  </div>
628
644
 
@@ -1112,6 +1128,37 @@
1112
1128
  color: var(--text-muted);
1113
1129
  }
1114
1130
 
1131
+ .case-history-bars {
1132
+ display: flex;
1133
+ align-items: flex-end;
1134
+ gap: 2px;
1135
+ height: 16px;
1136
+ margin-top: 0.5rem;
1137
+ }
1138
+
1139
+ .history-bar {
1140
+ width: 4px;
1141
+ height: 100%;
1142
+ border-radius: 1px;
1143
+ background: var(--border);
1144
+ }
1145
+
1146
+ .history-bar.pass {
1147
+ background: var(--pass);
1148
+ }
1149
+
1150
+ .history-bar.fail {
1151
+ background: var(--fail);
1152
+ }
1153
+
1154
+ .history-bar.warn {
1155
+ background: var(--warn);
1156
+ }
1157
+
1158
+ .history-bar.muted {
1159
+ background: var(--text-muted);
1160
+ }
1161
+
1115
1162
  .detail-tabs {
1116
1163
  display: flex;
1117
1164
  border-bottom: 1px solid var(--border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.5.11",
3
+ "version": "2.5.13",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"