create-mercato-app 0.6.2-develop.3406.1.2b403f40da → 0.6.2-develop.3446.1.bd060c6017
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/template/.env.example +7 -0
- package/template/package.json.template +6 -4
- package/template/scripts/dev-runtime.mjs +42 -14
- package/template/src/app/(backend)/backend/__tests__/layout-route-registry.test.tsx +5 -46
- package/template/src/app/(backend)/backend/layout.tsx +1 -1
- package/template/src/app/__tests__/layout.test.tsx +0 -14
- package/template/src/app/globals.css +76 -60
- package/template/src/app/layout.tsx +1 -1
- package/template/src/components/AiAssistantShellIntegration.tsx +12 -4
- package/template/src/modules/example/__integration__/TC-APP-001-metadata.spec.ts +1 -1
- package/template/src/modules/example/backend/todos/[id]/edit/page.tsx +2 -6
- package/template/src/modules/ratelimit_probe/__integration__/TC-INT-ratelimit-leakage.spec.ts +60 -0
- package/template/src/modules.ts +4 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -88,6 +88,13 @@ NEXT_PUBLIC_UMES_DEVTOOLS=true
|
|
|
88
88
|
# Enables checkout summary/help wrappers used by integration tests only.
|
|
89
89
|
NEXT_PUBLIC_OM_EXAMPLE_CHECKOUT_TEST_INJECTIONS_ENABLED=false
|
|
90
90
|
|
|
91
|
+
# Public date/time display format overrides (date-fns tokens).
|
|
92
|
+
# Leave unset, or set to "system", to use the locale-derived default.
|
|
93
|
+
# Examples: NEXT_PUBLIC_OM_DATE_FORMAT=dd.MM.yyyy
|
|
94
|
+
# NEXT_PUBLIC_OM_DATE_TIME_FORMAT=dd.MM.yyyy HH:mm
|
|
95
|
+
# NEXT_PUBLIC_OM_DATE_FORMAT=
|
|
96
|
+
# NEXT_PUBLIC_OM_DATE_TIME_FORMAT=
|
|
97
|
+
|
|
91
98
|
# Mock payment gateway webhook secret used by the example mock provider.
|
|
92
99
|
# Required for webhook verification in standalone integration and local test flows.
|
|
93
100
|
# MOCK_GATEWAY_WEBHOOK_SECRET=open-mercato-mock-dev-webhook-secret
|
|
@@ -78,16 +78,18 @@
|
|
|
78
78
|
"@uiw/react-markdown-preview": "^5.2.0",
|
|
79
79
|
"@uiw/react-md-editor": "^4.1.0",
|
|
80
80
|
"@xyflow/react": "^12.6.0",
|
|
81
|
-
"ai": "^6.0.
|
|
81
|
+
"ai": "^6.0.177",
|
|
82
82
|
"awilix": "^12.0.5",
|
|
83
83
|
"bcryptjs": "^3.0.3",
|
|
84
84
|
"class-variance-authority": "^0.7.1",
|
|
85
85
|
"clsx": "^2.1.1",
|
|
86
|
+
"date-fns": "4.1.0",
|
|
87
|
+
"date-fns-tz": "^3.2.0",
|
|
86
88
|
"dotenv": "^17.4.2",
|
|
87
89
|
"lucide-react": "^1.8.0",
|
|
88
90
|
"mammoth": "^1.9.0",
|
|
89
91
|
"newrelic": "^13.19.1",
|
|
90
|
-
"next": "16.2.
|
|
92
|
+
"next": "16.2.6",
|
|
91
93
|
"pg": "8.20.0",
|
|
92
94
|
"pdfjs-dist": "^5.4.149",
|
|
93
95
|
"react": "19.2.5",
|
|
@@ -97,11 +99,11 @@
|
|
|
97
99
|
"reflect-metadata": "^0.2.2",
|
|
98
100
|
"remark-gfm": "^4.0.1",
|
|
99
101
|
"resend": "^6.12.0",
|
|
100
|
-
"sanitize-html": "^2.17.
|
|
102
|
+
"sanitize-html": "^2.17.4",
|
|
101
103
|
"semver": "^7.7.4",
|
|
102
104
|
"tailwind-merge": "^3.5.0",
|
|
103
105
|
"zod": "4.3.6",
|
|
104
|
-
"@stripe/react-stripe-js": "^6.
|
|
106
|
+
"@stripe/react-stripe-js": "^6.3.0",
|
|
105
107
|
"@stripe/stripe-js": "^9.3.1",
|
|
106
108
|
"@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
|
|
107
109
|
"@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
|
|
@@ -76,9 +76,9 @@ const {
|
|
|
76
76
|
stripAnsi,
|
|
77
77
|
wrapListLines,
|
|
78
78
|
} = await import(resolveSplashHelpersImport())
|
|
79
|
-
const { resolveSpawnCommand } = await import(resolveSpawnUtilsImport())
|
|
79
|
+
const { resolveProjectBinary, resolveSpawnCommand } = await import(resolveSpawnUtilsImport())
|
|
80
80
|
|
|
81
|
-
const command = process.platform === 'win32' ? 'mercato.cmd' : 'mercato'
|
|
81
|
+
const command = resolveProjectBinary(process.platform === 'win32' ? 'mercato.cmd' : 'mercato')
|
|
82
82
|
const classic = process.argv.includes('--classic') || isEnabledEnvFlag(process.env.OM_DEV_CLASSIC)
|
|
83
83
|
const verbose = !classic && (process.argv.includes('--verbose') || process.env.MERCATO_DEV_OUTPUT === 'verbose')
|
|
84
84
|
const rawPassthrough = classic || verbose
|
|
@@ -506,10 +506,10 @@ function spawnMercato(args) {
|
|
|
506
506
|
return child
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
-
function waitForExit(child) {
|
|
509
|
+
function waitForExit(child, label = 'Child process') {
|
|
510
510
|
return new Promise((resolve) => {
|
|
511
511
|
child.on('exit', (code, signal) => {
|
|
512
|
-
resolve({ code, signal })
|
|
512
|
+
resolve({ label, code, signal })
|
|
513
513
|
})
|
|
514
514
|
})
|
|
515
515
|
}
|
|
@@ -535,6 +535,32 @@ function resolveChildExitCode(result, fallback = 1) {
|
|
|
535
535
|
return fallback
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
function formatChildExitStatus(result) {
|
|
539
|
+
if (typeof result?.code === 'number') {
|
|
540
|
+
return `exit code ${result.code}`
|
|
541
|
+
}
|
|
542
|
+
if (result?.signal) {
|
|
543
|
+
return `signal ${result.signal}`
|
|
544
|
+
}
|
|
545
|
+
return 'an unknown status'
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function resolveUnexpectedExitCode(result) {
|
|
549
|
+
const exitCode = resolveChildExitCode(result, 1)
|
|
550
|
+
return exitCode === 0 ? 1 : exitCode
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function reportUnexpectedChildExit(result) {
|
|
554
|
+
const message = `❌ ${result?.label ?? 'Child process'} exited unexpectedly with ${formatChildExitStatus(result)}`
|
|
555
|
+
console.error(message)
|
|
556
|
+
rememberRawLog(message)
|
|
557
|
+
publishRuntimeFailure(message, {
|
|
558
|
+
progressCurrent: splashState.progressCurrent >= runtimeProgressCurrent ? splashState.progressCurrent : runtimeProgressCurrent,
|
|
559
|
+
progressLabel: splashState.progressLabel || startupProgress.label,
|
|
560
|
+
failureLines: [...collectRuntimeFailureLines(), message].slice(-10),
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
538
564
|
function joinBaseUrl(baseUrl, pathname) {
|
|
539
565
|
return `${String(baseUrl ?? '').replace(/\/$/, '')}${pathname}`
|
|
540
566
|
}
|
|
@@ -1660,15 +1686,16 @@ async function runClassicRuntime() {
|
|
|
1660
1686
|
|
|
1661
1687
|
const watch = spawnMercato(['generate', 'watch', '--skip-initial'])
|
|
1662
1688
|
const server = spawnMercato(['server', 'dev'])
|
|
1663
|
-
const result = await Promise.race([
|
|
1689
|
+
const result = await Promise.race([
|
|
1690
|
+
waitForExit(watch, 'Generator watch'),
|
|
1691
|
+
waitForExit(server, 'App runtime'),
|
|
1692
|
+
])
|
|
1664
1693
|
if (isGracefulShutdownResult(result)) {
|
|
1665
1694
|
return
|
|
1666
1695
|
}
|
|
1667
1696
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
const childCode = resolveChildExitCode(result, 1)
|
|
1671
|
-
shutdown(childCode === 0 ? 1 : childCode)
|
|
1697
|
+
reportUnexpectedChildExit(result)
|
|
1698
|
+
shutdown(resolveUnexpectedExitCode(result))
|
|
1672
1699
|
}
|
|
1673
1700
|
|
|
1674
1701
|
if (classic) {
|
|
@@ -1683,10 +1710,11 @@ printRuntimePackagesSummary()
|
|
|
1683
1710
|
const watch = startFilteredChild(['generate', 'watch', '--skip-initial'], 'Generator watch', classifyWatchLine)
|
|
1684
1711
|
const server = startFilteredChild(['server', 'dev'], 'App runtime', classifyServerLine)
|
|
1685
1712
|
|
|
1686
|
-
const result = await Promise.race([
|
|
1713
|
+
const result = await Promise.race([
|
|
1714
|
+
waitForExit(watch, 'Generator watch'),
|
|
1715
|
+
waitForExit(server, 'App runtime'),
|
|
1716
|
+
])
|
|
1687
1717
|
if (!isGracefulShutdownResult(result)) {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
const childCode = resolveChildExitCode(result, 1)
|
|
1691
|
-
shutdown(childCode === 0 ? 1 : childCode)
|
|
1718
|
+
reportUnexpectedChildExit(result)
|
|
1719
|
+
shutdown(resolveUnexpectedExitCode(result))
|
|
1692
1720
|
}
|
|
@@ -9,11 +9,6 @@ const backendRoutes = [
|
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
const mockRegisterBackendRouteManifests = jest.fn()
|
|
12
|
-
const mockParseBooleanWithDefault = jest.fn(() => false)
|
|
13
|
-
const mockCookies = jest.fn()
|
|
14
|
-
const mockHeaders = jest.fn()
|
|
15
|
-
const mockResolveTranslations = jest.fn()
|
|
16
|
-
const mockGetAuthFromCookies = jest.fn()
|
|
17
12
|
|
|
18
13
|
jest.mock('@/.mercato/generated/backend-routes.generated', () => ({
|
|
19
14
|
backendRoutes,
|
|
@@ -25,12 +20,12 @@ jest.mock('@open-mercato/shared/modules/registry', () => ({
|
|
|
25
20
|
}))
|
|
26
21
|
|
|
27
22
|
jest.mock('next/headers', () => ({
|
|
28
|
-
cookies: (
|
|
29
|
-
headers: (
|
|
23
|
+
cookies: jest.fn(),
|
|
24
|
+
headers: jest.fn(),
|
|
30
25
|
}))
|
|
31
26
|
|
|
32
27
|
jest.mock('@open-mercato/shared/lib/auth/server', () => ({
|
|
33
|
-
getAuthFromCookies: (
|
|
28
|
+
getAuthFromCookies: jest.fn(),
|
|
34
29
|
}))
|
|
35
30
|
|
|
36
31
|
jest.mock('@open-mercato/ui/backend/AppShell', () => ({
|
|
@@ -38,7 +33,7 @@ jest.mock('@open-mercato/ui/backend/AppShell', () => ({
|
|
|
38
33
|
}))
|
|
39
34
|
|
|
40
35
|
jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
|
|
41
|
-
resolveTranslations: (
|
|
36
|
+
resolveTranslations: jest.fn(),
|
|
42
37
|
}))
|
|
43
38
|
|
|
44
39
|
jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
|
|
@@ -54,7 +49,7 @@ jest.mock('@open-mercato/shared/lib/version', () => ({
|
|
|
54
49
|
}))
|
|
55
50
|
|
|
56
51
|
jest.mock('@open-mercato/shared/lib/boolean', () => ({
|
|
57
|
-
parseBooleanWithDefault: (
|
|
52
|
+
parseBooleanWithDefault: jest.fn(() => true),
|
|
58
53
|
}))
|
|
59
54
|
|
|
60
55
|
jest.mock('@open-mercato/ui/backend/injection/PageInjectionBoundary', () => ({
|
|
@@ -78,11 +73,6 @@ describe('Backend layout route registry', () => {
|
|
|
78
73
|
beforeEach(() => {
|
|
79
74
|
jest.resetModules()
|
|
80
75
|
mockRegisterBackendRouteManifests.mockClear()
|
|
81
|
-
mockParseBooleanWithDefault.mockClear()
|
|
82
|
-
mockCookies.mockReset()
|
|
83
|
-
mockHeaders.mockReset()
|
|
84
|
-
mockResolveTranslations.mockReset()
|
|
85
|
-
mockGetAuthFromCookies.mockReset()
|
|
86
76
|
})
|
|
87
77
|
|
|
88
78
|
it('registers backend route manifests at module load', async () => {
|
|
@@ -92,35 +82,4 @@ describe('Backend layout route registry', () => {
|
|
|
92
82
|
|
|
93
83
|
expect(mockRegisterBackendRouteManifests).toHaveBeenCalledWith(backendRoutes)
|
|
94
84
|
})
|
|
95
|
-
|
|
96
|
-
it('defaults the demo mode flag to false when DEMO_MODE is unset', async () => {
|
|
97
|
-
const originalDemoMode = process.env.DEMO_MODE
|
|
98
|
-
delete process.env.DEMO_MODE
|
|
99
|
-
mockGetAuthFromCookies.mockResolvedValue(null)
|
|
100
|
-
mockCookies.mockResolvedValue({ get: () => undefined })
|
|
101
|
-
mockHeaders.mockResolvedValue({ get: () => null })
|
|
102
|
-
mockResolveTranslations.mockResolvedValue({
|
|
103
|
-
translate: (_key: string, fallback: string) => fallback,
|
|
104
|
-
locale: 'en',
|
|
105
|
-
dict: {},
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
await jest.isolateModulesAsync(async () => {
|
|
110
|
-
const { default: BackendLayout } = await import('../layout')
|
|
111
|
-
await BackendLayout({
|
|
112
|
-
children: React.createElement('div'),
|
|
113
|
-
params: Promise.resolve({}),
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
expect(mockParseBooleanWithDefault).toHaveBeenCalledWith(undefined, false)
|
|
118
|
-
} finally {
|
|
119
|
-
if (originalDemoMode === undefined) {
|
|
120
|
-
delete process.env.DEMO_MODE
|
|
121
|
-
} else {
|
|
122
|
-
process.env.DEMO_MODE = originalDemoMode
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
85
|
})
|
|
@@ -80,7 +80,7 @@ export default async function BackendLayout({
|
|
|
80
80
|
|
|
81
81
|
const collapsedCookie = cookieStore.get('om_sidebar_collapsed')?.value
|
|
82
82
|
const initialCollapsed = collapsedCookie === '1'
|
|
83
|
-
const demoModeEnabled = parseBooleanWithDefault(process.env.DEMO_MODE,
|
|
83
|
+
const demoModeEnabled = parseBooleanWithDefault(process.env.DEMO_MODE, true)
|
|
84
84
|
const deployEnv = process.env.DEPLOY_ENV
|
|
85
85
|
const baseProductName = translate('appShell.productName', 'Open Mercato')
|
|
86
86
|
const productName = deployEnv && deployEnv !== 'local'
|
|
@@ -97,18 +97,4 @@ describe('RootLayout', () => {
|
|
|
97
97
|
expect(appProviders?.props.demoModeEnabled).toBe(false)
|
|
98
98
|
expect(appProviders?.props.noticeBarsEnabled).toBe(true)
|
|
99
99
|
})
|
|
100
|
-
|
|
101
|
-
it('defaults demo mode off when DEMO_MODE is unset', async () => {
|
|
102
|
-
delete process.env.DEMO_MODE
|
|
103
|
-
delete process.env.OM_INTEGRATION_TEST
|
|
104
|
-
|
|
105
|
-
const { default: RootLayout } = await import('../layout')
|
|
106
|
-
const { AppProviders } = await import('@/components/AppProviders')
|
|
107
|
-
const tree = await RootLayout({ children: 'child' })
|
|
108
|
-
const appProviders = findElementByType(tree, AppProviders)
|
|
109
|
-
|
|
110
|
-
expect(appProviders).not.toBeNull()
|
|
111
|
-
expect(appProviders?.props.demoModeEnabled).toBe(false)
|
|
112
|
-
expect(appProviders?.props.noticeBarsEnabled).toBe(true)
|
|
113
|
-
})
|
|
114
100
|
})
|
|
@@ -244,36 +244,47 @@ TODO: Fix that latter to have reference by the package names
|
|
|
244
244
|
--sidebar-border: oklch(0.922 0 0);
|
|
245
245
|
--sidebar-ring: oklch(0.708 0 0);
|
|
246
246
|
|
|
247
|
-
/* ═══ Design System: Semantic Status Tokens — Light Mode ═══
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
--status-
|
|
262
|
-
--status-
|
|
263
|
-
--status-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
--status-
|
|
268
|
-
--status-
|
|
269
|
-
--status-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
--status-
|
|
274
|
-
--status-
|
|
275
|
-
--status-
|
|
276
|
-
|
|
247
|
+
/* ═══ Design System: Semantic Status Tokens — Light Mode ═══
|
|
248
|
+
Token values match the Tailwind v4 default palette so the
|
|
249
|
+
Alert / Notification / Toast primitive (Figma `169:2358`) is
|
|
250
|
+
pixel-perfect against the Figma `state/{x}/{base,light,lighter}`
|
|
251
|
+
variables (`#dc2626` / `#fecaca` / `#fef2f2` for error etc.).
|
|
252
|
+
Variable → token mapping:
|
|
253
|
+
state/{x}/base → status-{x}-icon (Tailwind 600)
|
|
254
|
+
state/{x}/light → status-{x}-border (Tailwind 200)
|
|
255
|
+
state/{x}/lighter → status-{x}-bg (Tailwind 50)
|
|
256
|
+
status-{x}-text uses Tailwind 900 for high contrast on the
|
|
257
|
+
light/lighter tints. */
|
|
258
|
+
|
|
259
|
+
/* Error — Tailwind red (Figma state/error/*) */
|
|
260
|
+
--status-error-bg: oklch(0.971 0.013 17.38); /* red-50 #fef2f2 */
|
|
261
|
+
--status-error-text: oklch(0.396 0.141 25.723); /* red-900 #7f1d1d */
|
|
262
|
+
--status-error-border: oklch(0.885 0.062 18.334);/* red-200 #fecaca */
|
|
263
|
+
--status-error-icon: oklch(0.577 0.245 27.325); /* red-600 #dc2626 = --destructive */
|
|
264
|
+
|
|
265
|
+
/* Success — Tailwind green (Figma state/success/*) */
|
|
266
|
+
--status-success-bg: oklch(0.982 0.018 155.826); /* green-50 #f0fdf4 */
|
|
267
|
+
--status-success-text: oklch(0.393 0.095 152.535); /* green-900 #14532d */
|
|
268
|
+
--status-success-border: oklch(0.925 0.084 155.995);/* green-200 #bbf7d0 */
|
|
269
|
+
--status-success-icon: oklch(0.627 0.194 149.214); /* green-600 #16a34a */
|
|
270
|
+
|
|
271
|
+
/* Warning — Tailwind amber (Figma state/warning/*) */
|
|
272
|
+
--status-warning-bg: oklch(0.987 0.022 95.277); /* amber-50 #fffbeb */
|
|
273
|
+
--status-warning-text: oklch(0.414 0.112 45.904); /* amber-900 #78350f */
|
|
274
|
+
--status-warning-border: oklch(0.924 0.12 95.746); /* amber-200 #fde68a */
|
|
275
|
+
--status-warning-icon: oklch(0.666 0.179 58.318); /* amber-600 #d97706 */
|
|
276
|
+
|
|
277
|
+
/* Info — Tailwind indigo (Figma state/information/*) */
|
|
278
|
+
--status-info-bg: oklch(0.962 0.018 272.314); /* indigo-50 #eef2ff */
|
|
279
|
+
--status-info-text: oklch(0.359 0.144 278.697); /* indigo-900 #312e81 */
|
|
280
|
+
--status-info-border: oklch(0.870 0.065 274.039);/* indigo-200 #c7d2fe */
|
|
281
|
+
--status-info-icon: oklch(0.511 0.262 276.966); /* indigo-600 #4f46e5 = --chart-indigo */
|
|
282
|
+
|
|
283
|
+
/* Neutral — Tailwind neutral (Figma state/faded/*) */
|
|
284
|
+
--status-neutral-bg: oklch(0.97 0 0); /* neutral-100 #f5f5f5 */
|
|
285
|
+
--status-neutral-text: oklch(0.371 0 0); /* neutral-700 #404040 */
|
|
286
|
+
--status-neutral-border: oklch(0.922 0 0);/* neutral-200 #e5e5e5 (≈ Figma #ebebeb) */
|
|
287
|
+
--status-neutral-icon: oklch(0.556 0 0); /* neutral-500 #737373 (≈ Figma #7b7b7b) */
|
|
277
288
|
|
|
278
289
|
/* Pink (hue ~340° — Figma SPEC-048 ACCENT / PINK Stage badges; Tailwind pink-500/-200/-700/-50) */
|
|
279
290
|
--status-pink-bg: oklch(0.965 0.020 340); /* pink-50 #FDF2F8 */
|
|
@@ -358,36 +369,41 @@ TODO: Fix that latter to have reference by the package names
|
|
|
358
369
|
--sidebar-border: oklch(1 0 0 / 10%);
|
|
359
370
|
--sidebar-ring: oklch(0.556 0 0);
|
|
360
371
|
|
|
361
|
-
/* ═══ Design System: Semantic Status Tokens — Dark Mode ═══
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
/*
|
|
369
|
-
--status-
|
|
370
|
-
--status-
|
|
371
|
-
--status-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
/*
|
|
375
|
-
--status-
|
|
376
|
-
--status-
|
|
377
|
-
--status-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
--status-
|
|
382
|
-
--status-
|
|
383
|
-
--status-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
/*
|
|
387
|
-
--status-
|
|
388
|
-
--status-
|
|
389
|
-
--status-
|
|
390
|
-
|
|
372
|
+
/* ═══ Design System: Semantic Status Tokens — Dark Mode ═══
|
|
373
|
+
Dark-mode counterparts mirror the Tailwind v4 dark palette
|
|
374
|
+
(`*-950` for bg, `*-300` for text, `*-800` for border, `*-500`
|
|
375
|
+
for icon) so the Alert primitive stays pixel-perfect against
|
|
376
|
+
the Figma dark variants. */
|
|
377
|
+
|
|
378
|
+
/* Error — Tailwind red (dark) */
|
|
379
|
+
--status-error-bg: oklch(0.258 0.092 26.042); /* red-950 #450a0a */
|
|
380
|
+
--status-error-text: oklch(0.808 0.114 19.571); /* red-300 #fca5a5 */
|
|
381
|
+
--status-error-border: oklch(0.444 0.177 26.899);/* red-800 #991b1b */
|
|
382
|
+
--status-error-icon: oklch(0.637 0.237 25.331); /* red-500 #ef4444 */
|
|
383
|
+
|
|
384
|
+
/* Success — Tailwind green (dark) */
|
|
385
|
+
--status-success-bg: oklch(0.266 0.065 152.934); /* green-950 #052e16 */
|
|
386
|
+
--status-success-text: oklch(0.871 0.15 154.449); /* green-300 #86efac */
|
|
387
|
+
--status-success-border: oklch(0.448 0.119 151.328);/* green-800 #166534 */
|
|
388
|
+
--status-success-icon: oklch(0.723 0.219 149.579); /* green-500 #22c55e */
|
|
389
|
+
|
|
390
|
+
/* Warning — Tailwind amber (dark) */
|
|
391
|
+
--status-warning-bg: oklch(0.279 0.077 45.635); /* amber-950 #451a03 */
|
|
392
|
+
--status-warning-text: oklch(0.879 0.169 91.605); /* amber-300 #fcd34d */
|
|
393
|
+
--status-warning-border: oklch(0.473 0.137 46.201);/* amber-800 #92400e */
|
|
394
|
+
--status-warning-icon: oklch(0.769 0.188 70.08); /* amber-500 #f59e0b */
|
|
395
|
+
|
|
396
|
+
/* Info — Tailwind indigo (dark) */
|
|
397
|
+
--status-info-bg: oklch(0.257 0.09 281.288); /* indigo-950 #1e1b4b */
|
|
398
|
+
--status-info-text: oklch(0.785 0.115 274.713); /* indigo-300 #a5b4fc */
|
|
399
|
+
--status-info-border: oklch(0.398 0.195 277.366);/* indigo-800 #3730a3 */
|
|
400
|
+
--status-info-icon: oklch(0.585 0.233 277.117); /* indigo-500 #6366f1 */
|
|
401
|
+
|
|
402
|
+
/* Neutral — Tailwind neutral (dark) */
|
|
403
|
+
--status-neutral-bg: oklch(0.205 0 0); /* neutral-900 #171717 */
|
|
404
|
+
--status-neutral-text: oklch(0.872 0 0); /* neutral-300 #d4d4d4 */
|
|
405
|
+
--status-neutral-border: oklch(0.371 0 0);/* neutral-700 #404040 */
|
|
406
|
+
--status-neutral-icon: oklch(0.708 0 0); /* neutral-400 #a3a3a3 = dark --muted-foreground */
|
|
391
407
|
|
|
392
408
|
/* Pink (dark mode) — Figma SPEC-048 ACCENT / PINK Stage badges */
|
|
393
409
|
--status-pink-bg: oklch(0.225 0.045 340);
|
|
@@ -22,7 +22,7 @@ export default async function RootLayout({
|
|
|
22
22
|
}>) {
|
|
23
23
|
const locale = await detectLocale()
|
|
24
24
|
const dict = await loadDictionary(locale)
|
|
25
|
-
const demoModeEnabled = process.env.DEMO_MODE
|
|
25
|
+
const demoModeEnabled = process.env.DEMO_MODE !== 'false'
|
|
26
26
|
const noticeBarsEnabled = process.env.OM_INTEGRATION_TEST !== 'true'
|
|
27
27
|
return (
|
|
28
28
|
<html lang={locale} suppressHydrationWarning>
|
|
@@ -14,6 +14,8 @@ type AiAssistantShellIntegrationProps = {
|
|
|
14
14
|
children: React.ReactNode
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const AiAssistantIntegrationFallback: AiAssistantIntegrationComponent = ({ children }) => <>{children}</>
|
|
18
|
+
|
|
17
19
|
export function AiAssistantShellIntegration({
|
|
18
20
|
tenantId,
|
|
19
21
|
organizationId,
|
|
@@ -23,10 +25,16 @@ export function AiAssistantShellIntegration({
|
|
|
23
25
|
|
|
24
26
|
React.useEffect(() => {
|
|
25
27
|
let cancelled = false
|
|
26
|
-
void import('@open-mercato/ai-assistant/frontend')
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
void import('@open-mercato/ai-assistant/frontend')
|
|
29
|
+
.then((module) => {
|
|
30
|
+
if (cancelled) return
|
|
31
|
+
setIntegrationComponent(() => module.AiAssistantIntegration)
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
if (cancelled) return
|
|
35
|
+
console.error('Failed to load AI assistant integration', error)
|
|
36
|
+
setIntegrationComponent(() => AiAssistantIntegrationFallback)
|
|
37
|
+
})
|
|
30
38
|
return () => {
|
|
31
39
|
cancelled = true
|
|
32
40
|
}
|
|
@@ -14,7 +14,7 @@ async function setGermanLocale(page: Page) {
|
|
|
14
14
|
])
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
test.describe('TC-APP-001:
|
|
17
|
+
test.describe('TC-APP-001: App metadata', () => {
|
|
18
18
|
test('home page exposes localized app metadata', async ({ page }) => {
|
|
19
19
|
await setGermanLocale(page)
|
|
20
20
|
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
|
@@ -8,6 +8,7 @@ import { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'
|
|
|
8
8
|
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
9
9
|
import type { TodoListItem } from '../../../../types'
|
|
10
10
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
|
+
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
11
12
|
|
|
12
13
|
type TodoItem = TodoListItem
|
|
13
14
|
type TodoCustomFieldValues = Record<`cf_${string}`, unknown>
|
|
@@ -80,13 +81,8 @@ export default function EditTodoPage({ params }: { params?: { id?: string } }) {
|
|
|
80
81
|
const item = data?.items?.[0]
|
|
81
82
|
if (!item) throw new Error(t('example.todos.form.error.notFound'))
|
|
82
83
|
// Map to form initial values
|
|
83
|
-
const cfInit: Partial<TodoCustomFieldValues> = {}
|
|
84
84
|
const extended = item as TodoItem & Record<string, unknown>
|
|
85
|
-
|
|
86
|
-
if (key.startsWith('cf_')) {
|
|
87
|
-
cfInit[key as keyof TodoCustomFieldValues] = value
|
|
88
|
-
}
|
|
89
|
-
}
|
|
85
|
+
const cfInit = extractCustomFieldEntries(extended) as Partial<TodoCustomFieldValues>
|
|
90
86
|
const init: TodoFormValues = {
|
|
91
87
|
id: item.id,
|
|
92
88
|
title: item.title,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression guard: `OM_INTEGRATION_TEST=true` must keep both the metadata-driven
|
|
7
|
+
* and the per-handler rate-limit paths bypassed.
|
|
8
|
+
*
|
|
9
|
+
* `readRateLimitConfig()` flips `RATE_LIMIT_ENABLED=false` whenever
|
|
10
|
+
* `OM_INTEGRATION_TEST=true`, which the integration runner exports via
|
|
11
|
+
* `packages/cli/src/lib/testing/integration.ts`. Without that bypass every
|
|
12
|
+
* Playwright run shares a single client IP across workers and burns through
|
|
13
|
+
* per-route point budgets — flakiness that scales with worker count.
|
|
14
|
+
*
|
|
15
|
+
* The metadata-path case depends on the dev-only `ratelimit_probe` module at
|
|
16
|
+
* `apps/mercato/src/modules/ratelimit_probe/`. That module ships only with
|
|
17
|
+
* the open-mercato monorepo and is intentionally absent from
|
|
18
|
+
* `create-mercato-app` scaffolds, so the metadata path self-skips when
|
|
19
|
+
* `/api/ratelimit_probe/ping` responds with 404.
|
|
20
|
+
*/
|
|
21
|
+
test.describe('per-route rate limits leak into OM_INTEGRATION_TEST runs', () => {
|
|
22
|
+
test('metadata.rateLimit path (apps/mercato root handler): 5 POSTs to /api/ratelimit_probe/ping all return 200 with points=3', async ({ request }) => {
|
|
23
|
+
const probe = await request.post(`${BASE_URL}/api/ratelimit_probe/ping`, {
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
data: {},
|
|
26
|
+
});
|
|
27
|
+
test.skip(
|
|
28
|
+
probe.status() === 404,
|
|
29
|
+
'ratelimit_probe module not registered (expected for create-mercato-app scaffolds)',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const statuses: number[] = [probe.status()];
|
|
33
|
+
for (let i = 0; i < 4; i += 1) {
|
|
34
|
+
const res = await request.post(`${BASE_URL}/api/ratelimit_probe/ping`, {
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
data: {},
|
|
37
|
+
});
|
|
38
|
+
statuses.push(res.status());
|
|
39
|
+
}
|
|
40
|
+
// With the OM_INTEGRATION_TEST bypass: all 5 calls return 200.
|
|
41
|
+
// Without it: rate limiter fires on the 4th call → [200, 200, 200, 429, 429].
|
|
42
|
+
expect(statuses, 'OM_INTEGRATION_TEST should bypass metadata.rateLimit').toEqual([200, 200, 200, 200, 200]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('manual checkRateLimit path (sales/quotes/accept): 11th POST 429s with points=10', async ({ request }) => {
|
|
46
|
+
// The rate limiter runs before token validation at sales/api/quotes/accept/route.ts:50-59,
|
|
47
|
+
// so an obviously invalid token still consumes points.
|
|
48
|
+
const statuses: number[] = [];
|
|
49
|
+
for (let i = 0; i < 12; i += 1) {
|
|
50
|
+
const res = await request.post(`${BASE_URL}/api/sales/quotes/fake-token-for-rate-limit-probe/accept`, {
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
data: {},
|
|
53
|
+
});
|
|
54
|
+
statuses.push(res.status());
|
|
55
|
+
}
|
|
56
|
+
// With the OM_INTEGRATION_TEST bypass: zero 429s (invalid token → 400/404, but never 429).
|
|
57
|
+
// Without it: 429 from the 11th call onward.
|
|
58
|
+
expect(statuses.filter((s) => s === 429), 'OM_INTEGRATION_TEST should bypass per-handler checkRateLimit').toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/template/src/modules.ts
CHANGED
|
@@ -68,6 +68,10 @@ if (enabledModules.some((entry) => entry.id === 'example')) {
|
|
|
68
68
|
enabledModules.push({ id: 'example_customers_sync', from: '@app' })
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (parseBooleanWithDefault(process.env.OM_ENABLE_STORAGE_S3, false)) {
|
|
72
|
+
enabledModules.push({ id: 'storage_s3', from: '@open-mercato/storage-s3' })
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
const enterpriseModulesEnabled = parseBooleanWithDefault(process.env.OM_ENABLE_ENTERPRISE_MODULES, false)
|
|
72
76
|
const enterpriseSsoEnabled = parseBooleanWithDefault(process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO, false)
|
|
73
77
|
const enterpriseSecurityEnabled = parseBooleanWithDefault(process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY, false)
|