plum-e2e 2.5.1 → 2.5.3
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/.env +19 -0
- package/backend/lib/serverConfig.js +18 -12
- package/bin/plum.js +86 -16
- package/docker-compose.yml +2 -2
- package/frontend/src/lib/api/auth.js +21 -5
- package/frontend/src/routes/+layout.svelte +14 -0
- package/frontend/src/routes/login/+page.svelte +8 -1
- package/frontend/src/routes/setup/+page.svelte +8 -1
- package/package.json +1 -1
package/.env
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
19
|
+
IS_HEADLESS=true
|
|
@@ -32,7 +32,12 @@ function defaults() {
|
|
|
32
32
|
return {
|
|
33
33
|
headless: false,
|
|
34
34
|
backendPort: '3001',
|
|
35
|
-
frontendPort: '5173'
|
|
35
|
+
frontendPort: '5173',
|
|
36
|
+
// Public URLs the browser actually uses. Left blank until the user sets
|
|
37
|
+
// them (e.g. behind a reverse proxy) — resolved to a localhost default
|
|
38
|
+
// at the call site otherwise.
|
|
39
|
+
apiUrl: '',
|
|
40
|
+
uiUrl: ''
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -63,10 +68,10 @@ function loadServerConfig(dir) {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
function saveServerConfig(dir, cfg) {
|
|
66
|
-
const { headless, backendPort, frontendPort } = cfg;
|
|
71
|
+
const { headless, backendPort, frontendPort, apiUrl, uiUrl } = cfg;
|
|
67
72
|
fs.writeFileSync(
|
|
68
73
|
configPath(dir),
|
|
69
|
-
JSON.stringify({ headless, backendPort, frontendPort }, null, 2) + '\n',
|
|
74
|
+
JSON.stringify({ headless, backendPort, frontendPort, apiUrl, uiUrl }, null, 2) + '\n',
|
|
70
75
|
'utf8'
|
|
71
76
|
);
|
|
72
77
|
}
|
|
@@ -88,25 +93,26 @@ function writeEnvFile(dir, { headless }) {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
|
-
* Builds docker-compose.override.yml.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
* Builds docker-compose.override.yml. Host port remapping is handled by
|
|
97
|
+
* BACKEND_PORT/FRONTEND_PORT env vars read by docker-compose.yml itself
|
|
98
|
+
* (${BACKEND_PORT:-3001} etc) — NOT here, because Compose merges `ports:`
|
|
99
|
+
* lists across files by concatenation rather than replacing them. Defining
|
|
100
|
+
* ports in both the base file and this override would publish both values
|
|
101
|
+
* simultaneously, and fail to start if the base file's default port happens
|
|
102
|
+
* to already be taken. This override only adds volumes and tells the
|
|
103
|
+
* frontend where to reach the backend via VITE_API_URL.
|
|
94
104
|
*/
|
|
95
|
-
function buildOverrideYaml({ testsAbs, reportsAbs, backendPort,
|
|
105
|
+
function buildOverrideYaml({ testsAbs, reportsAbs, backendPort, apiUrl }) {
|
|
96
106
|
return (
|
|
97
107
|
[
|
|
98
108
|
'services:',
|
|
99
109
|
' backend:',
|
|
100
|
-
' ports:',
|
|
101
|
-
` - "${backendPort}:3001"`,
|
|
102
110
|
' volumes:',
|
|
103
111
|
` - "${reportsAbs}:/app/reports"`,
|
|
104
112
|
` - "${testsAbs}:/app/tests"`,
|
|
105
113
|
' frontend:',
|
|
106
|
-
' ports:',
|
|
107
|
-
` - "${frontendPort}:5173"`,
|
|
108
114
|
' environment:',
|
|
109
|
-
` VITE_API_URL: "http://localhost:${backendPort}"`
|
|
115
|
+
` VITE_API_URL: "${apiUrl || `http://localhost:${backendPort}`}"`
|
|
110
116
|
].join('\n') + '\n'
|
|
111
117
|
);
|
|
112
118
|
}
|
package/bin/plum.js
CHANGED
|
@@ -188,13 +188,23 @@ async function configureServer({ force }) {
|
|
|
188
188
|
const overrides = {
|
|
189
189
|
headless: getFlag(args, '--headless'),
|
|
190
190
|
backendPort: getFlag(args, '--backend-port'),
|
|
191
|
-
frontendPort: getFlag(args, '--frontend-port')
|
|
191
|
+
frontendPort: getFlag(args, '--frontend-port'),
|
|
192
|
+
apiUrl: getFlag(args, '--api-url'),
|
|
193
|
+
uiUrl: getFlag(args, '--ui-url')
|
|
192
194
|
};
|
|
193
195
|
if (overrides.headless !== undefined) cfg.headless = overrides.headless === 'true';
|
|
194
196
|
if (overrides.backendPort !== undefined) cfg.backendPort = overrides.backendPort;
|
|
195
197
|
if (overrides.frontendPort !== undefined) cfg.frontendPort = overrides.frontendPort;
|
|
198
|
+
if (overrides.apiUrl !== undefined) cfg.apiUrl = overrides.apiUrl;
|
|
199
|
+
if (overrides.uiUrl !== undefined) cfg.uiUrl = overrides.uiUrl;
|
|
196
200
|
|
|
197
|
-
const hasFlags = anyFlags(args, [
|
|
201
|
+
const hasFlags = anyFlags(args, [
|
|
202
|
+
'--headless',
|
|
203
|
+
'--backend-port',
|
|
204
|
+
'--frontend-port',
|
|
205
|
+
'--api-url',
|
|
206
|
+
'--ui-url'
|
|
207
|
+
]);
|
|
198
208
|
const interactive = force || (interactiveAllowed() && !hasFlags);
|
|
199
209
|
|
|
200
210
|
if (interactive) {
|
|
@@ -220,6 +230,27 @@ async function configureServer({ force }) {
|
|
|
220
230
|
});
|
|
221
231
|
if (clack.isCancel(frontendPort)) cancelAndExit();
|
|
222
232
|
cfg.frontendPort = frontendPort || cfg.frontendPort;
|
|
233
|
+
|
|
234
|
+
const defaultApiUrl = `http://localhost:${cfg.backendPort}`;
|
|
235
|
+
const apiUrl = await clack.text({
|
|
236
|
+
message: 'Public URL for the API (only if reverse-proxying behind a domain)',
|
|
237
|
+
placeholder: cfg.apiUrl || defaultApiUrl,
|
|
238
|
+
defaultValue: cfg.apiUrl || defaultApiUrl
|
|
239
|
+
});
|
|
240
|
+
if (clack.isCancel(apiUrl)) cancelAndExit();
|
|
241
|
+
cfg.apiUrl = apiUrl || defaultApiUrl;
|
|
242
|
+
|
|
243
|
+
const defaultUiUrl = `http://localhost:${cfg.frontendPort}`;
|
|
244
|
+
const uiUrl = await clack.text({
|
|
245
|
+
message: 'Public URL for the UI (only if reverse-proxying behind a domain)',
|
|
246
|
+
placeholder: cfg.uiUrl || defaultUiUrl,
|
|
247
|
+
defaultValue: cfg.uiUrl || defaultUiUrl
|
|
248
|
+
});
|
|
249
|
+
if (clack.isCancel(uiUrl)) cancelAndExit();
|
|
250
|
+
cfg.uiUrl = uiUrl || defaultUiUrl;
|
|
251
|
+
} else {
|
|
252
|
+
if (!cfg.apiUrl) cfg.apiUrl = `http://localhost:${cfg.backendPort}`;
|
|
253
|
+
if (!cfg.uiUrl) cfg.uiUrl = `http://localhost:${cfg.frontendPort}`;
|
|
223
254
|
}
|
|
224
255
|
|
|
225
256
|
saveServerConfig(cwd, cfg);
|
|
@@ -240,7 +271,7 @@ function applyServerConfig(cfg) {
|
|
|
240
271
|
testsAbs,
|
|
241
272
|
reportsAbs,
|
|
242
273
|
backendPort: cfg.backendPort,
|
|
243
|
-
|
|
274
|
+
apiUrl: cfg.apiUrl
|
|
244
275
|
}),
|
|
245
276
|
'utf8'
|
|
246
277
|
);
|
|
@@ -282,7 +313,7 @@ async function waitForServerReady(apiBase) {
|
|
|
282
313
|
return ready;
|
|
283
314
|
}
|
|
284
315
|
|
|
285
|
-
async function runFirstUserSetup(apiBase,
|
|
316
|
+
async function runFirstUserSetup(apiBase, uiUrl) {
|
|
286
317
|
let needsSetup = false;
|
|
287
318
|
try {
|
|
288
319
|
const res = await fetch(`${apiBase}/auth/needs-setup`);
|
|
@@ -294,7 +325,7 @@ async function runFirstUserSetup(apiBase, frontendPort) {
|
|
|
294
325
|
|
|
295
326
|
if (!interactiveAllowed()) {
|
|
296
327
|
clack.log.info(
|
|
297
|
-
`No users found. Open ${pc.cyan(
|
|
328
|
+
`No users found. Open ${pc.cyan(`${uiUrl}/setup`)} to create your first account.`
|
|
298
329
|
);
|
|
299
330
|
return;
|
|
300
331
|
}
|
|
@@ -334,28 +365,57 @@ async function runFirstUserSetup(apiBase, frontendPort) {
|
|
|
334
365
|
}
|
|
335
366
|
}
|
|
336
367
|
|
|
368
|
+
// docker compose failures (port conflicts, daemon not running, etc.) must not
|
|
369
|
+
// crash the process with a raw stack trace — that would abort serverStart()
|
|
370
|
+
// before it ever reaches the first-user prompt, with no indication why.
|
|
371
|
+
function runDockerComposeUp(cfg) {
|
|
372
|
+
try {
|
|
373
|
+
execSync('docker compose up --build -d', {
|
|
374
|
+
cwd: plumRoot,
|
|
375
|
+
stdio: 'inherit',
|
|
376
|
+
env: {
|
|
377
|
+
...process.env,
|
|
378
|
+
BACKEND_PORT: String(cfg.backendPort),
|
|
379
|
+
FRONTEND_PORT: String(cfg.frontendPort)
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
clack.log.error(
|
|
385
|
+
`Docker failed to start the stack — see the output above for the cause.\n` +
|
|
386
|
+
`A common cause is another process already using port ${cfg.backendPort} or ${cfg.frontendPort}; ` +
|
|
387
|
+
`try ${pc.cyan('plum server reconfig')} to pick different ports.`
|
|
388
|
+
);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
337
393
|
async function serverStart() {
|
|
338
394
|
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Server ')));
|
|
339
395
|
const cfg = await configureServer({ force: false });
|
|
340
396
|
applyServerConfig(cfg);
|
|
341
|
-
clack.log.info(`UI: ${pc.cyan(
|
|
397
|
+
clack.log.info(`UI: ${pc.cyan(cfg.uiUrl)}`);
|
|
342
398
|
|
|
343
|
-
|
|
399
|
+
if (!runDockerComposeUp(cfg)) {
|
|
400
|
+
clack.outro(pc.red('Plum did not start.'));
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
344
404
|
|
|
345
405
|
const apiBase = `http://127.0.0.1:${cfg.backendPort}`;
|
|
346
406
|
const ready = await waitForServerReady(apiBase);
|
|
347
407
|
|
|
348
408
|
if (ready) {
|
|
349
|
-
await runFirstUserSetup(apiBase, cfg.
|
|
409
|
+
await runFirstUserSetup(apiBase, cfg.uiUrl);
|
|
350
410
|
} else {
|
|
351
411
|
clack.log.warn(
|
|
352
412
|
`Could not confirm the backend is ready. Check ${pc.cyan('docker compose logs -f backend')}.\n` +
|
|
353
|
-
`Once it responds, open ${pc.cyan(
|
|
413
|
+
`Once it responds, open ${pc.cyan(`${cfg.uiUrl}/setup`)} to create your first account (if this is a fresh install).`
|
|
354
414
|
);
|
|
355
415
|
}
|
|
356
416
|
|
|
357
|
-
clack.log.info(`UI: ${pc.cyan(
|
|
358
|
-
clack.log.info(`API: ${pc.cyan(
|
|
417
|
+
clack.log.info(`UI: ${pc.cyan(cfg.uiUrl)}`);
|
|
418
|
+
clack.log.info(`API: ${pc.cyan(cfg.apiUrl)}`);
|
|
359
419
|
clack.outro(pc.green('Plum is running. Use "plum server stop" to shut down.'));
|
|
360
420
|
}
|
|
361
421
|
|
|
@@ -364,9 +424,13 @@ async function serverRestart() {
|
|
|
364
424
|
const { loadServerConfig } = serverConfigLib();
|
|
365
425
|
const cfg = loadServerConfig(process.cwd());
|
|
366
426
|
applyServerConfig(cfg);
|
|
367
|
-
clack.log.info(`UI: ${pc.cyan(
|
|
427
|
+
clack.log.info(`UI: ${pc.cyan(cfg.uiUrl)}`);
|
|
368
428
|
|
|
369
|
-
|
|
429
|
+
if (!runDockerComposeUp(cfg)) {
|
|
430
|
+
clack.outro(pc.red('Server did not restart.'));
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
370
434
|
|
|
371
435
|
const apiBase = `http://127.0.0.1:${cfg.backendPort}`;
|
|
372
436
|
const ready = await waitForServerReady(apiBase);
|
|
@@ -375,8 +439,8 @@ async function serverRestart() {
|
|
|
375
439
|
`Could not confirm the backend is ready. Check ${pc.cyan('docker compose logs -f backend')}.`
|
|
376
440
|
);
|
|
377
441
|
}
|
|
378
|
-
clack.log.info(`UI: ${pc.cyan(
|
|
379
|
-
clack.log.info(`API: ${pc.cyan(
|
|
442
|
+
clack.log.info(`UI: ${pc.cyan(cfg.uiUrl)}`);
|
|
443
|
+
clack.log.info(`API: ${pc.cyan(cfg.apiUrl)}`);
|
|
380
444
|
clack.outro(pc.green('Server restarted.'));
|
|
381
445
|
}
|
|
382
446
|
|
|
@@ -419,7 +483,7 @@ async function serverReconfig() {
|
|
|
419
483
|
const cfg = await configureServer({ force: true });
|
|
420
484
|
applyServerConfig(cfg);
|
|
421
485
|
clack.log.success("Saved. Run 'plum server start' to apply.");
|
|
422
|
-
clack.outro(`UI: ${pc.cyan(
|
|
486
|
+
clack.outro(`UI: ${pc.cyan(cfg.uiUrl)}`);
|
|
423
487
|
}
|
|
424
488
|
|
|
425
489
|
/* -----------------------------------------------------
|
|
@@ -1110,6 +1174,12 @@ switch (command) {
|
|
|
1110
1174
|
console.log(' --headless <bool> Run browsers headless (true/false)');
|
|
1111
1175
|
console.log(' --backend-port <n> Host port for the backend/API (default: 3001)');
|
|
1112
1176
|
console.log(' --frontend-port <n> Host port for the UI (default: 5173)');
|
|
1177
|
+
console.log(
|
|
1178
|
+
' --api-url <url> Public URL for the API (only if reverse-proxying; default: http://localhost:<backend-port>)'
|
|
1179
|
+
);
|
|
1180
|
+
console.log(
|
|
1181
|
+
' --ui-url <url> Public URL for the UI (only if reverse-proxying; default: http://localhost:<frontend-port>)'
|
|
1182
|
+
);
|
|
1113
1183
|
console.log(' server restart Rebuild Docker images and restart the server (no prompts)');
|
|
1114
1184
|
console.log(' server stop Stop the server (data preserved)');
|
|
1115
1185
|
console.log(' server reconfig Re-enter server settings without starting');
|
package/docker-compose.yml
CHANGED
|
@@ -35,7 +35,7 @@ services:
|
|
|
35
35
|
backend:
|
|
36
36
|
build: ./backend
|
|
37
37
|
ports:
|
|
38
|
-
- '3001:3001'
|
|
38
|
+
- '${BACKEND_PORT:-3001}:3001'
|
|
39
39
|
environment:
|
|
40
40
|
DATABASE_URL: 'postgresql://plum:plum@postgres:5432/plum'
|
|
41
41
|
extra_hosts:
|
|
@@ -52,7 +52,7 @@ services:
|
|
|
52
52
|
frontend:
|
|
53
53
|
build: ./frontend
|
|
54
54
|
ports:
|
|
55
|
-
- '5173:5173'
|
|
55
|
+
- '${FRONTEND_PORT:-5173}:5173'
|
|
56
56
|
depends_on:
|
|
57
57
|
- backend
|
|
58
58
|
networks:
|
|
@@ -17,15 +17,31 @@
|
|
|
17
17
|
|
|
18
18
|
import { API_BASE } from '$lib/constants';
|
|
19
19
|
|
|
20
|
+
// A misconfigured or not-yet-routed API URL can accept a connection and never
|
|
21
|
+
// respond, rather than refusing outright — a plain fetch() would then hang
|
|
22
|
+
// forever with no error to catch. AUTH_TIMEOUT_MS bounds that so the UI always
|
|
23
|
+
// resolves one way or another instead of leaving the page blank indefinitely.
|
|
24
|
+
const AUTH_TIMEOUT_MS = 8000;
|
|
25
|
+
|
|
26
|
+
async function fetchWithTimeout(url, options = {}) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
|
|
29
|
+
try {
|
|
30
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
export async function checkNeedsSetup() {
|
|
21
|
-
const res = await
|
|
37
|
+
const res = await fetchWithTimeout(`${API_BASE}/auth/needs-setup`);
|
|
22
38
|
if (!res.ok) return false;
|
|
23
39
|
const data = await res.json();
|
|
24
40
|
return data.needsSetup;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export async function setup({ name, email, password }) {
|
|
28
|
-
const res = await
|
|
44
|
+
const res = await fetchWithTimeout(`${API_BASE}/auth/setup`, {
|
|
29
45
|
method: 'POST',
|
|
30
46
|
headers: { 'Content-Type': 'application/json' },
|
|
31
47
|
body: JSON.stringify({ name, email, password })
|
|
@@ -36,7 +52,7 @@ export async function setup({ name, email, password }) {
|
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
export async function login({ email, password }) {
|
|
39
|
-
const res = await
|
|
55
|
+
const res = await fetchWithTimeout(`${API_BASE}/auth/login`, {
|
|
40
56
|
method: 'POST',
|
|
41
57
|
headers: { 'Content-Type': 'application/json' },
|
|
42
58
|
body: JSON.stringify({ email, password })
|
|
@@ -47,7 +63,7 @@ export async function login({ email, password }) {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
export async function updateProfile({ token, name, email }) {
|
|
50
|
-
const res = await
|
|
66
|
+
const res = await fetchWithTimeout(`${API_BASE}/auth/update-profile`, {
|
|
51
67
|
method: 'PUT',
|
|
52
68
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
53
69
|
body: JSON.stringify({ name, email })
|
|
@@ -58,7 +74,7 @@ export async function updateProfile({ token, name, email }) {
|
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
export async function changePassword({ token, currentPassword, newPassword }) {
|
|
61
|
-
const res = await
|
|
77
|
+
const res = await fetchWithTimeout(`${API_BASE}/auth/change-password`, {
|
|
62
78
|
method: 'POST',
|
|
63
79
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
64
80
|
body: JSON.stringify({ currentPassword, newPassword })
|
|
@@ -62,4 +62,18 @@
|
|
|
62
62
|
</PageShell>
|
|
63
63
|
<RunnerPanel />
|
|
64
64
|
{/if}
|
|
65
|
+
{:else}
|
|
66
|
+
<div class="boot-loading">Loading…</div>
|
|
65
67
|
{/if}
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.boot-loading {
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
background: var(--bg);
|
|
76
|
+
color: var(--text-muted);
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -60,7 +60,9 @@
|
|
|
60
60
|
<svelte:head><title>Sign in — Plum</title></svelte:head>
|
|
61
61
|
|
|
62
62
|
<div class="page" data-theme={$theme}>
|
|
63
|
-
{#if
|
|
63
|
+
{#if checking}
|
|
64
|
+
<p class="checking">Checking server…</p>
|
|
65
|
+
{:else}
|
|
64
66
|
<div class="card">
|
|
65
67
|
<div class="brand">
|
|
66
68
|
<span class="brand-serif">Pl</span><span class="brand-sans">um</span>
|
|
@@ -116,6 +118,11 @@
|
|
|
116
118
|
padding: 1rem;
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
.checking {
|
|
122
|
+
color: var(--text-muted);
|
|
123
|
+
font-size: 0.875rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
119
126
|
.card {
|
|
120
127
|
width: 100%;
|
|
121
128
|
max-width: 380px;
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
<svelte:head><title>Setup — Plum</title></svelte:head>
|
|
66
66
|
|
|
67
67
|
<div class="page" data-theme={$theme}>
|
|
68
|
-
{#if
|
|
68
|
+
{#if checking}
|
|
69
|
+
<p class="checking">Checking server…</p>
|
|
70
|
+
{:else}
|
|
69
71
|
<div class="card">
|
|
70
72
|
<div class="brand">
|
|
71
73
|
<span class="brand-serif">Pl</span><span class="brand-sans">um</span>
|
|
@@ -136,6 +138,11 @@
|
|
|
136
138
|
padding: 1rem;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
.checking {
|
|
142
|
+
color: var(--text-muted);
|
|
143
|
+
font-size: 0.875rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
.card {
|
|
140
147
|
width: 100%;
|
|
141
148
|
max-width: 400px;
|