create-forgeon 0.1.25 → 0.1.26
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/package.json +1 -1
- package/src/presets/i18n.mjs +64 -23
- package/templates/base/apps/api/src/health/health.controller.ts +41 -8
- package/templates/base/apps/web/src/App.tsx +76 -35
- package/templates/base/apps/web/src/styles.css +29 -17
- package/templates/base/docs/AI/MODULE_CHECKS.md +25 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +1 -0
- package/templates/base/docs/AI/TASKS.md +8 -7
- package/templates/base/docs/README.md +1 -0
- package/templates/base/resources/i18n/en/common.json +3 -0
- package/templates/base/resources/i18n/uk/common.json +3 -0
- package/templates/module-presets/i18n/apps/web/src/App.tsx +63 -22
package/package.json
CHANGED
package/src/presets/i18n.mjs
CHANGED
|
@@ -142,29 +142,70 @@ export class AppModule {}
|
|
|
142
142
|
'health',
|
|
143
143
|
'health.controller.ts',
|
|
144
144
|
);
|
|
145
|
-
fs.writeFileSync(
|
|
146
|
-
healthControllerPath,
|
|
147
|
-
`import { Controller, Get, Query } from '@nestjs/common';
|
|
148
|
-
import {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
healthControllerPath,
|
|
147
|
+
`import { ConflictException, Controller, Get, Post, Query } from '@nestjs/common';
|
|
148
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
149
|
+
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
150
|
+
|
|
151
|
+
@Controller('health')
|
|
152
|
+
export class HealthController {
|
|
153
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
154
|
+
|
|
155
|
+
@Get()
|
|
156
|
+
getHealth(@Query('lang') lang?: string) {
|
|
157
|
+
const locale = this.resolveLocale(lang);
|
|
158
|
+
return {
|
|
159
|
+
status: 'ok',
|
|
160
|
+
message: 'OK',
|
|
161
|
+
i18n: locale === 'uk' ? 'Ukrainian' : 'English',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Get('error')
|
|
166
|
+
getErrorProbe() {
|
|
167
|
+
throw new ConflictException({
|
|
168
|
+
message: 'Email already exists',
|
|
169
|
+
details: {
|
|
170
|
+
feature: 'core-errors',
|
|
171
|
+
probe: 'health.error',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Get('validation')
|
|
177
|
+
getValidationProbe(@Query() query: EchoQueryDto) {
|
|
178
|
+
return {
|
|
179
|
+
status: 'ok',
|
|
180
|
+
validated: true,
|
|
181
|
+
value: query.value,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Post('db')
|
|
186
|
+
async getDbProbe() {
|
|
187
|
+
const token = \`\${Date.now()}-\${Math.floor(Math.random() * 1_000_000)}\`;
|
|
188
|
+
const email = \`health-probe-\${token}@example.local\`;
|
|
189
|
+
const user = await this.prisma.user.create({
|
|
190
|
+
data: { email },
|
|
191
|
+
select: { id: true, email: true, createdAt: true },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
status: 'ok',
|
|
196
|
+
feature: 'db-prisma',
|
|
197
|
+
user,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private resolveLocale(lang?: string): 'en' | 'uk' {
|
|
202
|
+
const normalized = (lang ?? '').toLowerCase();
|
|
203
|
+
return normalized.startsWith('uk') ? 'uk' : 'en';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`,
|
|
207
|
+
'utf8',
|
|
208
|
+
);
|
|
168
209
|
|
|
169
210
|
removeIfExists(
|
|
170
211
|
path.join(targetRoot, 'apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Controller, Get, Optional, Query } from '@nestjs/common';
|
|
1
|
+
import { ConflictException, Controller, Get, Optional, Post, Query } from '@nestjs/common';
|
|
2
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
2
3
|
import { I18nService } from 'nestjs-i18n';
|
|
3
4
|
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
4
5
|
|
|
5
6
|
@Controller('health')
|
|
6
7
|
export class HealthController {
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly prisma: PrismaService,
|
|
10
|
+
@Optional() private readonly i18n?: I18nService,
|
|
11
|
+
) {}
|
|
8
12
|
|
|
9
13
|
@Get()
|
|
10
14
|
getHealth(@Query('lang') lang?: string) {
|
|
@@ -16,16 +20,45 @@ export class HealthController {
|
|
|
16
20
|
};
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
@Get('
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
@Get('error')
|
|
24
|
+
getErrorProbe() {
|
|
25
|
+
throw new ConflictException({
|
|
26
|
+
message: 'Email already exists',
|
|
27
|
+
details: {
|
|
28
|
+
feature: 'core-errors',
|
|
29
|
+
probe: 'health.error',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
@Get('validation')
|
|
35
|
+
getValidationProbe(@Query() query: EchoQueryDto) {
|
|
36
|
+
return {
|
|
37
|
+
status: 'ok',
|
|
38
|
+
validated: true,
|
|
39
|
+
value: query.value,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Post('db')
|
|
44
|
+
async getDbProbe() {
|
|
45
|
+
const token = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
|
46
|
+
const email = `health-probe-${token}@example.local`;
|
|
47
|
+
const user = await this.prisma.user.create({
|
|
48
|
+
data: { email },
|
|
49
|
+
select: { id: true, email: true, createdAt: true },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
status: 'ok',
|
|
54
|
+
feature: 'db-prisma',
|
|
55
|
+
user,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private translate(key: string, lang?: string): string {
|
|
25
60
|
if (!this.i18n) {
|
|
26
61
|
if (key === 'common.ok') return 'OK';
|
|
27
|
-
if (key === 'common.checkApiHealth') return 'Check API health';
|
|
28
|
-
if (key === 'common.language') return 'Language';
|
|
29
62
|
if (key === 'common.languages.english') return 'English';
|
|
30
63
|
if (key === 'common.languages.ukrainian') return 'Ukrainian';
|
|
31
64
|
return key;
|
|
@@ -1,37 +1,78 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import './styles.css';
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
|
|
4
|
+
type ProbeResult = {
|
|
5
|
+
statusCode: number;
|
|
6
|
+
body: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
|
|
11
|
+
const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
|
|
12
|
+
const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
|
|
13
|
+
const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
|
|
14
|
+
const [networkError, setNetworkError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
|
|
17
|
+
const response = await fetch(url, init);
|
|
18
|
+
let body: unknown = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
body = await response.json();
|
|
22
|
+
} catch {
|
|
23
|
+
body = { message: 'Non-JSON response' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
statusCode: response.status,
|
|
28
|
+
body,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const runProbe = async (
|
|
33
|
+
setter: (value: ProbeResult | null) => void,
|
|
34
|
+
url: string,
|
|
35
|
+
init?: RequestInit,
|
|
36
|
+
) => {
|
|
37
|
+
setNetworkError(null);
|
|
38
|
+
try {
|
|
39
|
+
const result = await requestProbe(url, init);
|
|
40
|
+
setter(result);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setNetworkError(err instanceof Error ? err.message : 'Unknown error');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renderResult = (title: string, result: ProbeResult | null) => (
|
|
47
|
+
<section>
|
|
48
|
+
<h3>{title}</h3>
|
|
49
|
+
{result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
|
|
50
|
+
</section>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<main className="page">
|
|
55
|
+
<h1>Forgeon Fullstack Scaffold</h1>
|
|
56
|
+
<p>Default frontend preset: React + Vite + TypeScript.</p>
|
|
57
|
+
<div className="actions">
|
|
58
|
+
<button onClick={() => runProbe(setHealthResult, '/api/health')}>Check API health</button>
|
|
59
|
+
<button onClick={() => runProbe(setErrorProbeResult, '/api/health/error')}>
|
|
60
|
+
Check error envelope
|
|
61
|
+
</button>
|
|
62
|
+
<button onClick={() => runProbe(setValidationProbeResult, '/api/health/validation')}>
|
|
63
|
+
Check validation (expect 400)
|
|
64
|
+
</button>
|
|
65
|
+
<button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>
|
|
66
|
+
Check database (create user)
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
{renderResult('Health response', healthResult)}
|
|
70
|
+
{renderResult('Error probe response', errorProbeResult)}
|
|
71
|
+
{renderResult('Validation probe response', validationProbeResult)}
|
|
72
|
+
{renderResult('DB probe response', dbProbeResult)}
|
|
73
|
+
{networkError ? <p className="error">{networkError}</p> : null}
|
|
74
|
+
</main>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
3
77
|
|
|
4
|
-
type HealthResponse = {
|
|
5
|
-
status: string;
|
|
6
|
-
message: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export default function App() {
|
|
10
|
-
const [data, setData] = useState<HealthResponse | null>(null);
|
|
11
|
-
const [error, setError] = useState<string | null>(null);
|
|
12
|
-
|
|
13
|
-
const checkApi = async () => {
|
|
14
|
-
setError(null);
|
|
15
|
-
try {
|
|
16
|
-
const response = await fetch('/api/health');
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
throw new Error(`HTTP ${response.status}`);
|
|
19
|
-
}
|
|
20
|
-
const payload = (await response.json()) as HealthResponse;
|
|
21
|
-
setData(payload);
|
|
22
|
-
} catch (err) {
|
|
23
|
-
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<main className="page">
|
|
29
|
-
<h1>Forgeon Fullstack Scaffold</h1>
|
|
30
|
-
<p>Default frontend preset: React + Vite + TypeScript.</p>
|
|
31
|
-
<button onClick={checkApi}>Check API health</button>
|
|
32
|
-
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
|
|
33
|
-
{error ? <p className="error">{error}</p> : null}
|
|
34
|
-
</main>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
78
|
|
|
@@ -8,23 +8,35 @@ body {
|
|
|
8
8
|
color: #0f172a;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.page {
|
|
12
|
-
max-width: 720px;
|
|
13
|
-
margin: 3rem auto;
|
|
14
|
-
padding: 0 1rem;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
border-radius: 0.5rem;
|
|
11
|
+
.page {
|
|
12
|
+
max-width: 720px;
|
|
13
|
+
margin: 3rem auto;
|
|
14
|
+
padding: 0 1rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.actions {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
20
|
+
gap: 0.6rem;
|
|
21
|
+
margin: 1rem 0 1.25rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
button {
|
|
25
|
+
padding: 0.6rem 1rem;
|
|
26
|
+
border: 0;
|
|
27
|
+
border-radius: 0.5rem;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
h3 {
|
|
32
|
+
margin: 1rem 0 0.5rem;
|
|
33
|
+
font-size: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pre {
|
|
37
|
+
background: #e2e8f0;
|
|
38
|
+
padding: 1rem;
|
|
39
|
+
border-radius: 0.5rem;
|
|
28
40
|
overflow: auto;
|
|
29
41
|
}
|
|
30
42
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# MODULE CHECKS
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Define mandatory runtime verification hooks for Forgeon modules.
|
|
6
|
+
|
|
7
|
+
If a module can be validated through a safe API call, it must provide:
|
|
8
|
+
|
|
9
|
+
1. A probe endpoint in API (`/api/health/*`).
|
|
10
|
+
2. A probe trigger on default web page (`apps/web/src/App.tsx`).
|
|
11
|
+
3. A visible result block in UI with HTTP status and JSON body.
|
|
12
|
+
|
|
13
|
+
## Current Baseline Probes
|
|
14
|
+
|
|
15
|
+
- `core-errors`: `GET /api/health/error` (returns error envelope, expected `409`)
|
|
16
|
+
- `core-validation`: `GET /api/health/validation` without `value` (expected `400`)
|
|
17
|
+
- `db-prisma`: `POST /api/health/db` (creates probe user and returns it, expected `201`)
|
|
18
|
+
|
|
19
|
+
## Rules For Future Modules
|
|
20
|
+
|
|
21
|
+
- Probe path should be explicit and feature-scoped (`/api/health/<feature>`).
|
|
22
|
+
- Probe must be deterministic and documented (expected status + payload shape).
|
|
23
|
+
- If probe writes data, it must use clearly marked probe/test records.
|
|
24
|
+
- Probe should not require hidden setup beyond documented env/dependencies.
|
|
25
|
+
- `create-forgeon add <module>` must wire both API probe and web probe UI when feasible.
|
|
@@ -63,3 +63,4 @@ Must contain:
|
|
|
63
63
|
- Contracts package can be imported from both sides without circular dependencies.
|
|
64
64
|
- Contracts package exports are stable from `dist/index` entrypoint.
|
|
65
65
|
- Module has docs under `docs/AI/MODULES/<module-id>.md`.
|
|
66
|
+
- If module behavior can be runtime-checked, it also includes API+Web probe hooks (see `docs/AI/MODULE_CHECKS.md`).
|
|
@@ -48,11 +48,12 @@ Create or update create-forgeon preset flow:
|
|
|
48
48
|
## Add Fullstack Module
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
Implement `create-forgeon add <module-id>` for a fullstack feature.
|
|
52
|
-
Requirements:
|
|
53
|
-
- split module into contracts/api/web packages
|
|
54
|
-
- contracts is source of truth for routes, DTOs, errors
|
|
55
|
-
- add
|
|
56
|
-
-
|
|
57
|
-
|
|
51
|
+
Implement `create-forgeon add <module-id>` for a fullstack feature.
|
|
52
|
+
Requirements:
|
|
53
|
+
- split module into contracts/api/web packages
|
|
54
|
+
- contracts is source of truth for routes, DTOs, errors
|
|
55
|
+
- if feasible, add module probe hooks in API (`/api/health/*`) and web diagnostics UI
|
|
56
|
+
- add docs note under docs/AI/MODULES/<module-id>.md
|
|
57
|
+
- keep backward compatibility
|
|
58
|
+
```
|
|
58
59
|
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
- `AI/PROJECT.md` - project overview and run modes
|
|
4
4
|
- `AI/ARCHITECTURE.md` - monorepo design and extension model
|
|
5
5
|
- `AI/MODULE_SPEC.md` - fullstack module contract (`contracts/api/web`)
|
|
6
|
+
- `AI/MODULE_CHECKS.md` - required runtime probe hooks for modules
|
|
6
7
|
- `AI/VALIDATION.md` - DTO/env validation standards
|
|
7
8
|
- `AI/TASKS.md` - ready-to-use Codex prompts
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ok": "OK",
|
|
3
3
|
"checkApiHealth": "Check API health",
|
|
4
|
+
"checkErrorEnvelope": "Check error envelope",
|
|
5
|
+
"checkValidation": "Check validation (expect 400)",
|
|
6
|
+
"checkDatabase": "Check database (create user)",
|
|
4
7
|
"language": "Language",
|
|
5
8
|
"languages": {
|
|
6
9
|
"english": "English",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ok": "OK",
|
|
3
3
|
"checkApiHealth": "Перевірити API health",
|
|
4
|
+
"checkErrorEnvelope": "Перевірити error envelope",
|
|
5
|
+
"checkValidation": "Перевірити валідацію (очікуємо 400)",
|
|
6
|
+
"checkDatabase": "Перевірити базу даних (створити користувача)",
|
|
4
7
|
"language": "Мова",
|
|
5
8
|
"languages": {
|
|
6
9
|
"english": "Англійська",
|
|
@@ -5,10 +5,9 @@ import * as i18nWeb from '@forgeon/i18n-web';
|
|
|
5
5
|
import type { I18nLocale } from '@forgeon/i18n-web';
|
|
6
6
|
import './styles.css';
|
|
7
7
|
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
i18n: string;
|
|
8
|
+
type ProbeResult = {
|
|
9
|
+
statusCode: number;
|
|
10
|
+
body: unknown;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
function localeLabelKey(locale: I18nLocale): string {
|
|
@@ -19,8 +18,11 @@ export default function App() {
|
|
|
19
18
|
const { t } = useTranslation(['common']);
|
|
20
19
|
const { I18N_LOCALES, getInitialLocale, persistLocale, toLangQuery } = i18nWeb;
|
|
21
20
|
const [locale, setLocale] = useState<I18nLocale>(getInitialLocale);
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
21
|
+
const [healthResult, setHealthResult] = useState<ProbeResult | null>(null);
|
|
22
|
+
const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
|
|
23
|
+
const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
|
|
24
|
+
const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
|
|
25
|
+
const [networkError, setNetworkError] = useState<string | null>(null);
|
|
24
26
|
|
|
25
27
|
const changeLocale = (nextLocale: I18nLocale) => {
|
|
26
28
|
setLocale(nextLocale);
|
|
@@ -28,24 +30,49 @@ export default function App() {
|
|
|
28
30
|
void i18n.changeLanguage(nextLocale);
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
+
const requestProbe = async (path: string, init?: RequestInit): Promise<ProbeResult> => {
|
|
34
|
+
const response = await fetch(`/api${path}${toLangQuery(locale)}`, {
|
|
35
|
+
...init,
|
|
36
|
+
headers: {
|
|
37
|
+
...(init?.headers ?? {}),
|
|
38
|
+
'Accept-Language': locale,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let body: unknown = null;
|
|
43
|
+
try {
|
|
44
|
+
body = await response.json();
|
|
45
|
+
} catch {
|
|
46
|
+
body = { message: 'Non-JSON response' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
statusCode: response.status,
|
|
51
|
+
body,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const runProbe = async (
|
|
56
|
+
setter: (value: ProbeResult | null) => void,
|
|
57
|
+
path: string,
|
|
58
|
+
init?: RequestInit,
|
|
59
|
+
) => {
|
|
60
|
+
setNetworkError(null);
|
|
33
61
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
'Accept-Language': locale,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(`HTTP ${response.status}`);
|
|
41
|
-
}
|
|
42
|
-
const payload = (await response.json()) as HealthResponse;
|
|
43
|
-
setData(payload);
|
|
62
|
+
const result = await requestProbe(path, init);
|
|
63
|
+
setter(result);
|
|
44
64
|
} catch (err) {
|
|
45
|
-
|
|
65
|
+
setNetworkError(err instanceof Error ? err.message : 'Unknown error');
|
|
46
66
|
}
|
|
47
67
|
};
|
|
48
68
|
|
|
69
|
+
const renderResult = (title: string, result: ProbeResult | null) => (
|
|
70
|
+
<section>
|
|
71
|
+
<h3>{title}</h3>
|
|
72
|
+
{result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
|
|
49
76
|
return (
|
|
50
77
|
<main className="page">
|
|
51
78
|
<h1>Forgeon Fullstack Scaffold</h1>
|
|
@@ -62,9 +89,23 @@ export default function App() {
|
|
|
62
89
|
</option>
|
|
63
90
|
))}
|
|
64
91
|
</select>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
<div className="actions">
|
|
93
|
+
<button onClick={() => runProbe(setHealthResult, '/health')}>{t('common:checkApiHealth')}</button>
|
|
94
|
+
<button onClick={() => runProbe(setErrorProbeResult, '/health/error')}>
|
|
95
|
+
{t('common:checkErrorEnvelope')}
|
|
96
|
+
</button>
|
|
97
|
+
<button onClick={() => runProbe(setValidationProbeResult, '/health/validation')}>
|
|
98
|
+
{t('common:checkValidation')}
|
|
99
|
+
</button>
|
|
100
|
+
<button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>
|
|
101
|
+
{t('common:checkDatabase')}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
{renderResult('Health response', healthResult)}
|
|
105
|
+
{renderResult('Error probe response', errorProbeResult)}
|
|
106
|
+
{renderResult('Validation probe response', validationProbeResult)}
|
|
107
|
+
{renderResult('DB probe response', dbProbeResult)}
|
|
108
|
+
{networkError ? <p className="error">{networkError}</p> : null}
|
|
68
109
|
</main>
|
|
69
110
|
);
|
|
70
111
|
}
|