plum-e2e 1.2.3 → 1.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.
- package/CLAUDE.md +201 -0
- package/README.md +237 -90
- package/backend/_scaffold/utils/browser.ts +5 -2
- package/backend/app.js +9 -1
- package/backend/config/scripts/generate-report.js +34 -73
- package/backend/config/scripts/run-tests.js +7 -3
- package/backend/constants/triggers.js +67 -0
- package/backend/lib/reportFilename.js +37 -0
- package/backend/lib/testChunker.js +73 -0
- package/backend/middleware/auth.js +32 -0
- package/backend/package.json +4 -2
- package/backend/prisma/migrations/20260616000000_add_runners_and_browser/migration.sql +26 -0
- package/backend/prisma/migrations/20260616000001_cron_runner_ids/migration.sql +6 -0
- package/backend/prisma/migrations/20260617000000_cron_enabled/migration.sql +1 -0
- package/backend/prisma/migrations/20260617000001_report_content/migration.sql +8 -0
- package/backend/prisma/schema.prisma +21 -1
- package/backend/routes/cron.routes.js +28 -0
- package/backend/routes/node.routes.js +121 -0
- package/backend/routes/reports.routes.js +23 -20
- package/backend/routes/runners.routes.js +83 -0
- package/backend/scripts/add-local-runner.js +120 -0
- package/backend/scripts/create-test.js +148 -0
- package/backend/server.js +16 -7
- package/backend/services/backupService.js +3 -30
- package/backend/services/cronService.js +220 -36
- package/backend/services/reportService.js +227 -55
- package/backend/services/runnerService.js +179 -0
- package/backend/websockets/socketHandler.js +162 -21
- package/bin/plum.js +191 -47
- package/docker-compose.node.yml +59 -0
- package/docker-compose.yml +2 -0
- package/frontend/package.json +1 -4
- package/frontend/src/app.css +20 -254
- package/frontend/src/app.html +1 -1
- package/frontend/src/lib/api/reports.js +17 -36
- package/frontend/src/lib/api/runners.js +61 -0
- package/frontend/src/lib/api/schedules.js +34 -5
- package/frontend/src/lib/api/settings.js +5 -5
- package/frontend/src/lib/api/tests.js +2 -19
- package/frontend/src/lib/components/icons/BrowserIcon.svelte +75 -0
- package/frontend/src/lib/components/layout/Nav.svelte +42 -47
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +913 -253
- package/frontend/src/lib/components/ui/Badge.svelte +6 -1
- package/frontend/src/lib/components/ui/ConfirmModal.svelte +98 -0
- package/frontend/{tailwind.config.js → src/lib/components/ui/EmptyState.svelte} +27 -8
- package/frontend/{postcss.config.js → src/lib/components/ui/Toast.svelte} +20 -7
- package/frontend/src/lib/constants.js +36 -0
- package/frontend/src/lib/stores/runner.js +23 -12
- package/frontend/src/lib/styles/global.css +176 -0
- package/frontend/src/lib/styles/reset.css +86 -0
- package/frontend/src/lib/styles/tokens.css +90 -0
- package/frontend/src/lib/utils/format.js +46 -0
- package/frontend/src/routes/+page.svelte +16 -35
- package/frontend/src/routes/reports/+page.svelte +84 -167
- package/frontend/src/routes/reports/{[slug] → [id]}/+page.svelte +304 -76
- package/frontend/src/routes/reports/live/+page.svelte +704 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +328 -88
- package/frontend/src/routes/settings/+page.svelte +774 -127
- package/frontend/static/favicon-32x32.png +0 -0
- package/frontend/static/favicon.ico +0 -0
- package/package.json +2 -2
- package/frontend/static/favicon.png +0 -0
package/bin/plum.js
CHANGED
|
@@ -25,6 +25,7 @@ import fse from 'fs-extra';
|
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
26
|
const __dirname = path.dirname(__filename);
|
|
27
27
|
const command = process.argv[2];
|
|
28
|
+
const subcommand = process.argv[3];
|
|
28
29
|
const plumRoot = path.resolve(__dirname, '..');
|
|
29
30
|
const userTestsPath = path.join(process.cwd(), 'tests');
|
|
30
31
|
const scaffoldTestsPath = path.join(plumRoot, 'backend', '_scaffold');
|
|
@@ -161,35 +162,31 @@ switch (command) {
|
|
|
161
162
|
// Scaffold plum.plugins.json for user-managed dependencies
|
|
162
163
|
scaffoldPluginsFile();
|
|
163
164
|
|
|
164
|
-
//
|
|
165
|
+
// Always create .vscode/settings.json for Cucumber extension config
|
|
165
166
|
{
|
|
166
|
-
|
|
167
|
+
const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
|
|
168
|
+
if (!fs.existsSync(vscodeSettingsPath)) {
|
|
169
|
+
fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
vscodeSettingsPath,
|
|
172
|
+
JSON.stringify(
|
|
173
|
+
{
|
|
174
|
+
'cucumber.glue': ['tests/step_definitions/**/*.ts'],
|
|
175
|
+
'cucumber.features': ['tests/features/**/*.feature']
|
|
176
|
+
},
|
|
177
|
+
null,
|
|
178
|
+
2
|
|
179
|
+
) + '\n',
|
|
180
|
+
'utf8'
|
|
181
|
+
);
|
|
182
|
+
console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
|
|
183
|
+
} else {
|
|
184
|
+
console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Install extension via CLI only when the code command is available
|
|
167
188
|
try {
|
|
168
189
|
execSync('code --version', { stdio: 'ignore' });
|
|
169
|
-
vscodeAvailable = true;
|
|
170
|
-
} catch {}
|
|
171
|
-
|
|
172
|
-
if (vscodeAvailable) {
|
|
173
|
-
const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
|
|
174
|
-
if (!fs.existsSync(vscodeSettingsPath)) {
|
|
175
|
-
fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
|
|
176
|
-
fs.writeFileSync(
|
|
177
|
-
vscodeSettingsPath,
|
|
178
|
-
JSON.stringify(
|
|
179
|
-
{
|
|
180
|
-
'cucumber.glue': ['tests/step_definitions/**/*.ts'],
|
|
181
|
-
'cucumber.features': ['tests/features/**/*.feature']
|
|
182
|
-
},
|
|
183
|
-
null,
|
|
184
|
-
2
|
|
185
|
-
) + '\n',
|
|
186
|
-
'utf8'
|
|
187
|
-
);
|
|
188
|
-
console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
|
|
189
|
-
} else {
|
|
190
|
-
console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
190
|
try {
|
|
194
191
|
execSync('code --install-extension cucumberopen.cucumber-official', { stdio: 'inherit' });
|
|
195
192
|
console.log('✅ Cucumber VS Code extension installed.\n');
|
|
@@ -198,8 +195,43 @@ switch (command) {
|
|
|
198
195
|
'⚠️ Could not install VS Code extension automatically. Install manually: cucumberopen.cucumber-official\n'
|
|
199
196
|
);
|
|
200
197
|
}
|
|
198
|
+
} catch {
|
|
199
|
+
console.log(
|
|
200
|
+
'ℹ️ Install the Cucumber VS Code extension manually: cucumberopen.cucumber-official\n'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Scaffold tsconfig.json so VS Code resolves Plum's types without a local node_modules
|
|
206
|
+
{
|
|
207
|
+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
208
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
209
|
+
const backendModules = path.join(plumRoot, 'backend', 'node_modules').replace(/\\/g, '/');
|
|
210
|
+
const tsconfig = {
|
|
211
|
+
compilerOptions: {
|
|
212
|
+
target: 'ES2020',
|
|
213
|
+
module: 'CommonJS',
|
|
214
|
+
moduleResolution: 'node',
|
|
215
|
+
esModuleInterop: true,
|
|
216
|
+
strict: false,
|
|
217
|
+
skipLibCheck: true,
|
|
218
|
+
baseUrl: '.',
|
|
219
|
+
paths: {
|
|
220
|
+
playwright: [`${backendModules}/playwright`],
|
|
221
|
+
'@playwright/test': [`${backendModules}/@playwright/test`],
|
|
222
|
+
'@cucumber/cucumber': [`${backendModules}/@cucumber/cucumber`],
|
|
223
|
+
dotenv: [`${backendModules}/dotenv`],
|
|
224
|
+
chai: [`${backendModules}/chai`],
|
|
225
|
+
'chai-soft-assert': [`${backendModules}/chai-soft-assert`]
|
|
226
|
+
},
|
|
227
|
+
typeRoots: [`${backendModules}/@types`]
|
|
228
|
+
},
|
|
229
|
+
include: ['tests/**/*.ts']
|
|
230
|
+
};
|
|
231
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n', 'utf8');
|
|
232
|
+
console.log('✅ tsconfig.json created for IDE type resolution.\n');
|
|
201
233
|
} else {
|
|
202
|
-
console.log('
|
|
234
|
+
console.log('⚠️ tsconfig.json already exists. Skipping.\n');
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
|
|
@@ -212,52 +244,90 @@ switch (command) {
|
|
|
212
244
|
'',
|
|
213
245
|
'Powered by [Plum](https://github.com/silverlunah/plum) — Playwright + Cucumber.',
|
|
214
246
|
'',
|
|
247
|
+
'## Getting Started',
|
|
248
|
+
'',
|
|
249
|
+
"Your project is ready. Here's what to do next:",
|
|
250
|
+
'',
|
|
251
|
+
"1. **Open `.env`** and set `BASE_URL` to your application's URL.",
|
|
252
|
+
'2. **Run the example tests** to confirm everything works:',
|
|
253
|
+
' ```bash',
|
|
254
|
+
' plum dev',
|
|
255
|
+
' ```',
|
|
256
|
+
'3. **Write your first test** — edit a file in `tests/features/` or generate a step:',
|
|
257
|
+
' ```bash',
|
|
258
|
+
' plum create-step',
|
|
259
|
+
' ```',
|
|
260
|
+
'4. **Start the full UI** (requires Docker) to trigger tests and view reports in the browser:',
|
|
261
|
+
' ```bash',
|
|
262
|
+
' plum start',
|
|
263
|
+
' ```',
|
|
264
|
+
' Then open **http://localhost:5173**.',
|
|
265
|
+
'',
|
|
266
|
+
'---',
|
|
267
|
+
'',
|
|
215
268
|
'## Commands',
|
|
216
269
|
'',
|
|
217
270
|
'| Command | Description |',
|
|
218
|
-
'
|
|
271
|
+
'| --- | --- |',
|
|
219
272
|
'| `plum dev` | Run all tests locally |',
|
|
220
273
|
'| `plum dev @tag` | Run tests matching a tag |',
|
|
221
274
|
'| `plum dev --parallel N` | Run tests across N parallel workers |',
|
|
222
|
-
'| `plum start` | Start the full UI via Docker
|
|
275
|
+
'| `plum start` | Start the full UI via Docker |',
|
|
276
|
+
'| `plum stop` | Stop the server |',
|
|
223
277
|
'| `plum create-step` | Interactively generate a new step definition |',
|
|
224
278
|
'',
|
|
279
|
+
'---',
|
|
280
|
+
'',
|
|
225
281
|
'## Configuration',
|
|
226
282
|
'',
|
|
227
283
|
'| File | Purpose |',
|
|
228
|
-
'
|
|
229
|
-
'| `.env` | Set `BASE_URL` and `IS_HEADLESS` |',
|
|
230
|
-
'| `plum.plugins.json` | Add extra npm packages
|
|
284
|
+
'| --- | --- |',
|
|
285
|
+
'| `.env` | Set `BASE_URL` (your app) and `IS_HEADLESS` (`true`/`false`) |',
|
|
286
|
+
'| `plum.plugins.json` | Add extra npm packages your tests need |',
|
|
287
|
+
'',
|
|
288
|
+
'---',
|
|
231
289
|
'',
|
|
232
290
|
'## Test Structure',
|
|
233
291
|
'',
|
|
234
292
|
'```',
|
|
235
293
|
'tests/',
|
|
236
|
-
' features/ — Gherkin .feature files',
|
|
294
|
+
' features/ — Gherkin .feature files (write your scenarios here)',
|
|
237
295
|
' step_definitions/ — TypeScript step implementations',
|
|
238
296
|
' pages/ — Page Object Models',
|
|
239
|
-
' utils/ — Browser setup, hooks, helpers',
|
|
297
|
+
' utils/ — Browser setup, hooks, shared helpers',
|
|
240
298
|
'```',
|
|
241
299
|
'',
|
|
242
|
-
'
|
|
300
|
+
'Each scenario needs a unique tag so you can run it by itself:',
|
|
243
301
|
'',
|
|
244
302
|
'```gherkin',
|
|
245
303
|
'@suite-login',
|
|
246
304
|
'Feature: Login',
|
|
247
305
|
'',
|
|
248
306
|
' @test-login-1',
|
|
249
|
-
' Scenario: User can log in',
|
|
307
|
+
' Scenario: User can log in with valid credentials',
|
|
250
308
|
' Given I am on the login page',
|
|
251
|
-
'
|
|
309
|
+
' When I enter valid credentials',
|
|
310
|
+
' Then I should see the dashboard',
|
|
252
311
|
'```',
|
|
253
312
|
'',
|
|
254
313
|
'```bash',
|
|
255
|
-
'plum dev @test-login-1 # single scenario',
|
|
256
|
-
'plum dev @suite-login # whole suite',
|
|
257
|
-
'```'
|
|
314
|
+
'plum dev @test-login-1 # run a single scenario',
|
|
315
|
+
'plum dev @suite-login # run the whole suite',
|
|
316
|
+
'```',
|
|
317
|
+
'',
|
|
318
|
+
'---',
|
|
319
|
+
'',
|
|
320
|
+
'## Cucumber & Gherkin Resources',
|
|
321
|
+
'',
|
|
322
|
+
'New to Cucumber? These links will get you up to speed quickly:',
|
|
323
|
+
'',
|
|
324
|
+
'- [Gherkin syntax reference](https://cucumber.io/docs/gherkin/reference/) — Feature files, Scenarios, Given/When/Then, tags, Scenario Outlines',
|
|
325
|
+
'- [Step definitions guide](https://cucumber.io/docs/cucumber/step-definitions/) — Connecting Gherkin steps to TypeScript code',
|
|
326
|
+
'- [Playwright docs](https://playwright.dev/docs/intro) — Browser automation API used inside page objects',
|
|
327
|
+
'- [Plum documentation](https://github.com/silverlunah/plum) — Full README and reference'
|
|
258
328
|
].join('\n');
|
|
259
329
|
fs.writeFileSync(userReadmePath, readmeContent + '\n', 'utf8');
|
|
260
|
-
console.log('✅ README.md created with
|
|
330
|
+
console.log('✅ README.md created with quick-start guide.\n');
|
|
261
331
|
} else {
|
|
262
332
|
console.log('⚠️ README.md already exists. Skipping.\n');
|
|
263
333
|
}
|
|
@@ -277,6 +347,18 @@ switch (command) {
|
|
|
277
347
|
console.log('--------------------------------------\n');
|
|
278
348
|
break;
|
|
279
349
|
|
|
350
|
+
case 'server':
|
|
351
|
+
if (subcommand === 'stop') {
|
|
352
|
+
console.log('--------------------------------------\n');
|
|
353
|
+
console.log('🛑 Stopping Plum server...');
|
|
354
|
+
execSync('docker compose down', { cwd: plumRoot, stdio: 'inherit' });
|
|
355
|
+
console.log('✅ Plum server stopped. Your data is preserved.\n');
|
|
356
|
+
console.log('--------------------------------------\n');
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
// fall through to start for 'plum server start' or 'plum server'
|
|
360
|
+
// intentional fall-through
|
|
361
|
+
|
|
280
362
|
case 'start':
|
|
281
363
|
console.log('--------------------------------------\n');
|
|
282
364
|
|
|
@@ -403,9 +485,67 @@ switch (command) {
|
|
|
403
485
|
console.log('--------------------------------------\n');
|
|
404
486
|
break;
|
|
405
487
|
|
|
488
|
+
case 'node': {
|
|
489
|
+
if (subcommand === 'stop') {
|
|
490
|
+
console.log('--------------------------------------\n');
|
|
491
|
+
console.log('🛑 Stopping Plum node...');
|
|
492
|
+
execSync('docker compose -f docker-compose.node.yml down', {
|
|
493
|
+
cwd: plumRoot,
|
|
494
|
+
stdio: 'inherit'
|
|
495
|
+
});
|
|
496
|
+
console.log('✅ Plum node stopped.\n');
|
|
497
|
+
console.log('--------------------------------------\n');
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 'plum node start' — parse --token and --primary flags
|
|
502
|
+
const nodeArgs = process.argv.slice(3);
|
|
503
|
+
const tokenIdx = nodeArgs.indexOf('--token');
|
|
504
|
+
const nodeToken = tokenIdx !== -1 ? nodeArgs[tokenIdx + 1] : process.env.NODE_TOKEN || '';
|
|
505
|
+
const primaryIdx = nodeArgs.indexOf('--primary');
|
|
506
|
+
const primaryUrl = primaryIdx !== -1 ? nodeArgs[primaryIdx + 1] : process.env.PRIMARY_URL || '';
|
|
507
|
+
|
|
508
|
+
console.log('--------------------------------------\n');
|
|
509
|
+
console.log('🚀 Starting Plum node (runner mode)...');
|
|
510
|
+
if (!nodeToken) {
|
|
511
|
+
console.log(
|
|
512
|
+
'⚠️ No --token provided. The node will accept requests without authentication.\n'
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Build override for node mode
|
|
517
|
+
const userReportsAbs = path.resolve(process.cwd(), 'reports').replace(/\\/g, '/');
|
|
518
|
+
const nodeOverridePath = path.join(plumRoot, 'docker-compose.node-override.yml');
|
|
519
|
+
|
|
520
|
+
const nodeOverride = [
|
|
521
|
+
'services:',
|
|
522
|
+
' backend:',
|
|
523
|
+
' volumes:',
|
|
524
|
+
` - "${userReportsAbs}:/app/reports"`,
|
|
525
|
+
' environment:',
|
|
526
|
+
` NODE_TOKEN: "${nodeToken}"`,
|
|
527
|
+
` PRIMARY_URL: "${primaryUrl}"`,
|
|
528
|
+
' PLUM_MODE: "node"'
|
|
529
|
+
].join('\n');
|
|
530
|
+
|
|
531
|
+
fs.writeFileSync(nodeOverridePath, nodeOverride + '\n', 'utf8');
|
|
532
|
+
|
|
533
|
+
copyEnvFile();
|
|
534
|
+
|
|
535
|
+
execSync(
|
|
536
|
+
'docker compose -f docker-compose.node.yml -f docker-compose.node-override.yml up --build',
|
|
537
|
+
{
|
|
538
|
+
cwd: plumRoot,
|
|
539
|
+
stdio: 'inherit'
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
console.log('--------------------------------------\n');
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
|
|
406
546
|
case 'create-step': {
|
|
407
547
|
const createStepScript = path.join(plumRoot, 'backend', 'config', 'scripts', 'create-step.mjs');
|
|
408
|
-
execSync(`node ${createStepScript}`, {
|
|
548
|
+
execSync(`node "${createStepScript}"`, {
|
|
409
549
|
cwd: process.cwd(),
|
|
410
550
|
stdio: 'inherit',
|
|
411
551
|
env: {
|
|
@@ -419,10 +559,14 @@ switch (command) {
|
|
|
419
559
|
default:
|
|
420
560
|
console.log('--------------------------------------\n');
|
|
421
561
|
console.log('Usage: plum <command>\n');
|
|
422
|
-
console.log(' init
|
|
423
|
-
console.log(' start Start the full UI stack via Docker');
|
|
424
|
-
console.log(' stop Stop
|
|
425
|
-
console.log('
|
|
426
|
-
console.log('
|
|
562
|
+
console.log(' init Set up a new Plum project');
|
|
563
|
+
console.log(' server start Start the full UI stack via Docker (alias: plum start)');
|
|
564
|
+
console.log(' server stop Stop the server (alias: plum stop)');
|
|
565
|
+
console.log(' node start Start a runner node (no UI, receives remote jobs)');
|
|
566
|
+
console.log(' --token <secret> Auth token the primary must send');
|
|
567
|
+
console.log(' --primary <url> URL of the primary Plum server');
|
|
568
|
+
console.log(' node stop Stop the runner node');
|
|
569
|
+
console.log(' dev Run tests locally without Docker');
|
|
570
|
+
console.log(' create-step Interactively scaffold a new step definition');
|
|
427
571
|
console.log('\n--------------------------------------\n');
|
|
428
572
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
# Runner node — backend + postgres only, no frontend UI.
|
|
19
|
+
# Start with: plum node start --token <secret> --primary http://primary:3001
|
|
20
|
+
|
|
21
|
+
services:
|
|
22
|
+
postgres:
|
|
23
|
+
image: postgres:16-alpine
|
|
24
|
+
environment:
|
|
25
|
+
POSTGRES_DB: plum
|
|
26
|
+
POSTGRES_USER: plum
|
|
27
|
+
POSTGRES_PASSWORD: plum
|
|
28
|
+
volumes:
|
|
29
|
+
- node_postgres_data:/var/lib/postgresql/data
|
|
30
|
+
networks:
|
|
31
|
+
- node-network
|
|
32
|
+
healthcheck:
|
|
33
|
+
test: ['CMD-SHELL', 'pg_isready -U plum']
|
|
34
|
+
interval: 5s
|
|
35
|
+
timeout: 5s
|
|
36
|
+
retries: 10
|
|
37
|
+
|
|
38
|
+
backend:
|
|
39
|
+
build: ./backend
|
|
40
|
+
ports:
|
|
41
|
+
- '3001:3001'
|
|
42
|
+
environment:
|
|
43
|
+
DATABASE_URL: 'postgresql://plum:plum@postgres:5432/plum'
|
|
44
|
+
PLUM_MODE: 'node'
|
|
45
|
+
volumes:
|
|
46
|
+
- ./backend/reports:/app/reports
|
|
47
|
+
- ./backend/tests:/app/tests:rw
|
|
48
|
+
depends_on:
|
|
49
|
+
postgres:
|
|
50
|
+
condition: service_healthy
|
|
51
|
+
networks:
|
|
52
|
+
- node-network
|
|
53
|
+
|
|
54
|
+
networks:
|
|
55
|
+
node-network:
|
|
56
|
+
driver: bridge
|
|
57
|
+
|
|
58
|
+
volumes:
|
|
59
|
+
node_postgres_data:
|
package/docker-compose.yml
CHANGED
package/frontend/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-frontend",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite dev",
|
|
@@ -14,10 +14,7 @@
|
|
|
14
14
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
15
15
|
"@sveltejs/kit": "^2.16.0",
|
|
16
16
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
17
|
-
"autoprefixer": "^10.4.20",
|
|
18
|
-
"postcss": "^8.5.1",
|
|
19
17
|
"svelte": "^5.0.0",
|
|
20
|
-
"tailwindcss": "^3.4.17",
|
|
21
18
|
"vite": "^6.0.0"
|
|
22
19
|
},
|
|
23
20
|
"dependencies": {
|
package/frontend/src/app.css
CHANGED
|
@@ -14,257 +14,23 @@ GNU General Public License for more details.
|
|
|
14
14
|
You should have received a copy of the GNU General Public License
|
|
15
15
|
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--warn: #d97706;
|
|
38
|
-
--warn-soft: #fef3c7;
|
|
39
|
-
--terminal-bg: #0d0c08;
|
|
40
|
-
--terminal-text: #d6d0c8;
|
|
41
|
-
|
|
42
|
-
--font-display: 'Playfair Display', Georgia, serif;
|
|
43
|
-
--font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
|
|
44
|
-
|
|
45
|
-
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
46
|
-
--duration-fast: 140ms;
|
|
47
|
-
--duration-base: 240ms;
|
|
48
|
-
--duration-slow: 400ms;
|
|
49
|
-
|
|
50
|
-
--radius-sm: 6px;
|
|
51
|
-
--radius-md: 10px;
|
|
52
|
-
--radius-lg: 16px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
[data-theme='dark'] {
|
|
56
|
-
--bg: #101009;
|
|
57
|
-
--bg-subtle: #1a1910;
|
|
58
|
-
--bg-elevated: #201f14;
|
|
59
|
-
--border: #2e2c1e;
|
|
60
|
-
--text: #f0ede6;
|
|
61
|
-
--text-muted: #706a5e;
|
|
62
|
-
--accent: #d0a5f5;
|
|
63
|
-
--accent-soft: #1b0d30;
|
|
64
|
-
--pass: #22c55e;
|
|
65
|
-
--pass-soft: #0d2118;
|
|
66
|
-
--fail: #f87171;
|
|
67
|
-
--fail-soft: #2d1010;
|
|
68
|
-
--warn: #fbbf24;
|
|
69
|
-
--warn-soft: #2a1f06;
|
|
70
|
-
--terminal-bg: #080807;
|
|
71
|
-
--terminal-text: #cbc5bd;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* ── Reset ──────────────────────────────────────────────────────────────── */
|
|
75
|
-
|
|
76
|
-
*,
|
|
77
|
-
*::before,
|
|
78
|
-
*::after {
|
|
79
|
-
box-sizing: border-box;
|
|
80
|
-
margin: 0;
|
|
81
|
-
padding: 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
html {
|
|
85
|
-
scroll-behavior: smooth;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
body {
|
|
89
|
-
font-family: var(--font-body);
|
|
90
|
-
font-weight: 300;
|
|
91
|
-
font-size: 1rem;
|
|
92
|
-
line-height: 1.65;
|
|
93
|
-
background-color: var(--bg);
|
|
94
|
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E");
|
|
95
|
-
background-size: 200px 200px;
|
|
96
|
-
color: var(--text);
|
|
97
|
-
transition:
|
|
98
|
-
background-color var(--duration-base) var(--ease-out),
|
|
99
|
-
color var(--duration-base) var(--ease-out);
|
|
100
|
-
-webkit-font-smoothing: antialiased;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
h1 {
|
|
104
|
-
font-family: var(--font-display);
|
|
105
|
-
font-weight: 400;
|
|
106
|
-
line-height: 1.1;
|
|
107
|
-
letter-spacing: -0.015em;
|
|
108
|
-
color: var(--text);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
h2,
|
|
112
|
-
h3 {
|
|
113
|
-
font-family: var(--font-display);
|
|
114
|
-
font-weight: 400;
|
|
115
|
-
line-height: 1.2;
|
|
116
|
-
color: var(--text);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
a {
|
|
120
|
-
color: inherit;
|
|
121
|
-
text-decoration: none;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
button {
|
|
125
|
-
font-family: var(--font-body);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/* ── Global form styles ─────────────────────────────────────────────────── */
|
|
129
|
-
|
|
130
|
-
.field {
|
|
131
|
-
display: flex;
|
|
132
|
-
flex-direction: column;
|
|
133
|
-
gap: 0.375rem;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.field-label {
|
|
137
|
-
display: flex;
|
|
138
|
-
justify-content: space-between;
|
|
139
|
-
font-size: 0.8125rem;
|
|
140
|
-
font-weight: 400;
|
|
141
|
-
color: var(--text-muted);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.field-hint {
|
|
145
|
-
font-size: 0.75rem;
|
|
146
|
-
color: var(--text-muted);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.field-input {
|
|
150
|
-
width: 100%;
|
|
151
|
-
padding: 0.6rem 0.875rem;
|
|
152
|
-
border: 1px solid var(--border);
|
|
153
|
-
border-radius: var(--radius-md);
|
|
154
|
-
background: var(--bg-subtle);
|
|
155
|
-
color: var(--text);
|
|
156
|
-
font-family: var(--font-body);
|
|
157
|
-
font-size: 0.875rem;
|
|
158
|
-
font-weight: 300;
|
|
159
|
-
outline: none;
|
|
160
|
-
transition:
|
|
161
|
-
border-color var(--duration-fast),
|
|
162
|
-
box-shadow var(--duration-fast);
|
|
163
|
-
appearance: none;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.field-input:focus {
|
|
167
|
-
border-color: var(--accent);
|
|
168
|
-
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.field-input:disabled {
|
|
172
|
-
opacity: 0.5;
|
|
173
|
-
cursor: not-allowed;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.field-input::placeholder {
|
|
177
|
-
color: var(--text-muted);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* ── Global table ───────────────────────────────────────────────────────── */
|
|
181
|
-
|
|
182
|
-
.data-table {
|
|
183
|
-
width: 100%;
|
|
184
|
-
border-collapse: collapse;
|
|
185
|
-
font-size: 0.875rem;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.data-table th {
|
|
189
|
-
text-align: left;
|
|
190
|
-
padding: 0.5rem 1rem;
|
|
191
|
-
font-family: var(--font-body);
|
|
192
|
-
font-weight: 500;
|
|
193
|
-
font-size: 0.7rem;
|
|
194
|
-
letter-spacing: 0.07em;
|
|
195
|
-
text-transform: uppercase;
|
|
196
|
-
color: var(--text-muted);
|
|
197
|
-
border-bottom: 1px solid var(--border);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.data-table td {
|
|
201
|
-
padding: 0.875rem 1rem;
|
|
202
|
-
border-bottom: 1px solid var(--border);
|
|
203
|
-
color: var(--text);
|
|
204
|
-
vertical-align: middle;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.data-table tbody tr:last-child td {
|
|
208
|
-
border-bottom: none;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* ── Card ───────────────────────────────────────────────────────────────── */
|
|
212
|
-
|
|
213
|
-
.card {
|
|
214
|
-
background: var(--bg-elevated);
|
|
215
|
-
border: 1px solid var(--border);
|
|
216
|
-
border-radius: var(--radius-lg);
|
|
217
|
-
padding: 1.5rem;
|
|
218
|
-
transition:
|
|
219
|
-
background var(--duration-base) var(--ease-out),
|
|
220
|
-
border-color var(--duration-base) var(--ease-out);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.card-title {
|
|
224
|
-
font-family: var(--font-display);
|
|
225
|
-
font-size: 1.25rem;
|
|
226
|
-
font-weight: 400;
|
|
227
|
-
color: var(--text);
|
|
228
|
-
margin-bottom: 0.25rem;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.card-subtitle {
|
|
232
|
-
font-size: 0.8125rem;
|
|
233
|
-
color: var(--text-muted);
|
|
234
|
-
margin-bottom: 1.25rem;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/* ── Alert ──────────────────────────────────────────────────────────────── */
|
|
238
|
-
|
|
239
|
-
.alert {
|
|
240
|
-
display: flex;
|
|
241
|
-
align-items: center;
|
|
242
|
-
gap: 0.625rem;
|
|
243
|
-
padding: 0.75rem 1rem;
|
|
244
|
-
border-radius: var(--radius-md);
|
|
245
|
-
font-size: 0.875rem;
|
|
246
|
-
font-weight: 400;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.alert-success {
|
|
250
|
-
background: var(--pass-soft);
|
|
251
|
-
color: var(--pass);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.alert-error {
|
|
255
|
-
background: var(--fail-soft);
|
|
256
|
-
color: var(--fail);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* ── Animations ─────────────────────────────────────────────────────────── */
|
|
260
|
-
|
|
261
|
-
@keyframes fadeUp {
|
|
262
|
-
from {
|
|
263
|
-
opacity: 0;
|
|
264
|
-
transform: translateY(10px);
|
|
265
|
-
}
|
|
266
|
-
to {
|
|
267
|
-
opacity: 1;
|
|
268
|
-
transform: translateY(0);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
17
|
+
/*
|
|
18
|
+
* This file is part of Plum.
|
|
19
|
+
*
|
|
20
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
21
|
+
* it under the terms of the GNU General Public License as published by
|
|
22
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
23
|
+
* (at your option) any later version.
|
|
24
|
+
*
|
|
25
|
+
* Plum is distributed in the hope that it will be useful,
|
|
26
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
27
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
28
|
+
* GNU General Public License for more details.
|
|
29
|
+
*
|
|
30
|
+
* You should have received a copy of the GNU General Public License
|
|
31
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
@import '$lib/styles/tokens.css';
|
|
35
|
+
@import '$lib/styles/reset.css';
|
|
36
|
+
@import '$lib/styles/global.css';
|